目前很多開(kāi)放平臺(tái)如新浪微博開(kāi)放平臺(tái)都在使用提供開(kāi)放 API 接口供開(kāi)發(fā)者使用,隨之帶來(lái)了第三方應(yīng)用要到開(kāi)放平臺(tái)進(jìn)行授權(quán)的問(wèn)題,OAuth 就是干這個(gè)的,OAuth2 是 OAuth 協(xié)議的下一個(gè)版本,相比 OAuth1,OAuth2 整個(gè)授權(quán)流程更簡(jiǎn)單安全了,但不兼容 OAuth1,具體可以到 OAuth2 官網(wǎng) http://oauth.net/2/ 查看,OAuth2 協(xié)議規(guī)范可以參考 http://tools.ietf.org/html/rfc6749。目前有好多參考實(shí)現(xiàn)供選擇,可以到其官網(wǎng)查看下載。
本文使用 [Apache Oltu](),其之前的名字叫 Apache Amber ,是 Java 版的參考實(shí)現(xiàn)。使用文檔可參考 https://cwiki.apache.org/confluence/display/OLTU/Documentation。
資源擁有者(resource owner):能授權(quán)訪(fǎng)問(wèn)受保護(hù)資源的一個(gè)實(shí)體,可以是一個(gè)人,那我們稱(chēng)之為最終用戶(hù);如新浪微博用戶(hù) zhangsan;
資源服務(wù)器(resource server):存儲(chǔ)受保護(hù)資源,客戶(hù)端通過(guò) access token 請(qǐng)求資源,資源服務(wù)器響應(yīng)受保護(hù)資源給客戶(hù)端;存儲(chǔ)著用戶(hù) zhangsan 的微博等信息。
授權(quán)服務(wù)器(authorization server):成功驗(yàn)證資源擁有者并獲取授權(quán)之后,授權(quán)服務(wù)器頒發(fā)授權(quán)令牌(Access Token)給客戶(hù)端。
客戶(hù)端(client):如新浪微博客戶(hù)端 weico、微格等第三方應(yīng)用,也可以是它自己的官方應(yīng)用;其本身不存儲(chǔ)資源,而是資源擁有者授權(quán)通過(guò)后,使用它的授權(quán)(授權(quán)令牌)訪(fǎng)問(wèn)受保護(hù)資源,然后客戶(hù)端把相應(yīng)的數(shù)據(jù)展示出來(lái) / 提交到服務(wù)器。“客戶(hù)端” 術(shù)語(yǔ)不代表任何特定實(shí)現(xiàn)(如應(yīng)用運(yùn)行在一臺(tái)服務(wù)器、桌面、手機(jī)或其他設(shè)備)。
更多流程的解釋請(qǐng)參考 OAuth2 的協(xié)議規(guī)范 http://tools.ietf.org/html/rfc6749。
本文把授權(quán)服務(wù)器和資源服務(wù)器整合在一起實(shí)現(xiàn)。
此處我們使用 apache oltu oauth2 服務(wù)端實(shí)現(xiàn),需要引入 authzserver(授權(quán)服務(wù)器依賴(lài))和 resourceserver(資源服務(wù)器依賴(lài))。
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.authzserver</artifactId>
<version>0.31</version>
</dependency>
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.resourceserver</artifactId>
<version>0.31</version>
</dependency>
其他的請(qǐng)參考 pom.xml。
用戶(hù) (oauth2_user)
名稱(chēng) |
類(lèi)型 |
長(zhǎng)度 |
描述 |
id |
bigint |
10 |
編號(hào) 主鍵 |
username |
varchar |
100 |
用戶(hù)名 |
password |
varchar |
100 |
密碼 |
salt |
varchar |
50 |
鹽 |
客戶(hù)端 (oauth2_client)
名稱(chēng) |
類(lèi)型 |
長(zhǎng)度 |
描述 |
id |
bigint |
10 |
編號(hào) 主鍵 |
client_name |
varchar |
100 |
客戶(hù)端名稱(chēng) |
client_id |
varchar |
100 |
客戶(hù)端 id |
client_secret |
varchar |
100 |
客戶(hù)端安全 key |
用戶(hù)表存儲(chǔ)著認(rèn)證 / 資源服務(wù)器的用戶(hù)信息,即資源擁有者;比如用戶(hù)名 / 密碼;客戶(hù)端表存儲(chǔ)客戶(hù)端的的客戶(hù)端 id 及客戶(hù)端安全 key;在進(jìn)行授權(quán)時(shí)使用。
具體請(qǐng)參考
默認(rèn)用戶(hù)名 / 密碼是 admin/123456。
具體請(qǐng)參考 com.github.zhangkaitao.shiro.chapter17.entity 包下的實(shí)體,此處就不列舉了。
具體請(qǐng)參考 com.github.zhangkaitao.shiro.chapter17.dao 包下的 DAO 接口及實(shí)現(xiàn)。
具體請(qǐng)參考 com.github.zhangkaitao.shiro.chapter17.service 包下的 Service 接口及實(shí)現(xiàn)。以下是出了基本 CRUD 之外的關(guān)鍵接口:
public interface UserService {
public User createUser(User user);// 創(chuàng)建用戶(hù)
public User updateUser(User user);// 更新用戶(hù)
public void deleteUser(Long userId);// 刪除用戶(hù)
public void changePassword(Long userId, String newPassword); //修改密碼
User findOne(Long userId);// 根據(jù)id查找用戶(hù)
List<User> findAll();// 得到所有用戶(hù)
public User findByUsername(String username);// 根據(jù)用戶(hù)名查找用戶(hù)
}
public interface ClientService {
public Client createClient(Client client);// 創(chuàng)建客戶(hù)端
public Client updateClient(Client client);// 更新客戶(hù)端
public void deleteClient(Long clientId);// 刪除客戶(hù)端
Client findOne(Long clientId);// 根據(jù)id查找客戶(hù)端
List<Client> findAll();// 查找所有
Client findByClientId(String clientId);// 根據(jù)客戶(hù)端id查找客戶(hù)端
Client findByClientSecret(String clientSecret);//根據(jù)客戶(hù)端安全KEY查找客戶(hù)端
}
public interface OAuthService {
public void addAuthCode(String authCode, String username);// 添加 auth code
public void addAccessToken(String accessToken, String username); // 添加 access token
boolean checkAuthCode(String authCode); // 驗(yàn)證auth code是否有效
boolean checkAccessToken(String accessToken); // 驗(yàn)證access token是否有效
String getUsernameByAuthCode(String authCode);// 根據(jù)auth code獲取用戶(hù)名
String getUsernameByAccessToken(String accessToken);// 根據(jù)access token獲取用戶(hù)名
long getExpireIn();//auth code / access token 過(guò)期時(shí)間
public boolean checkClientId(String clientId);// 檢查客戶(hù)端id是否存在
public boolean checkClientSecret(String clientSecret);// 堅(jiān)持客戶(hù)端安全KEY是否存在
}
此處通過(guò) OAuthService 實(shí)現(xiàn)進(jìn)行 auth code 和 access token 的維護(hù)。
具體請(qǐng)參考 com.github.zhangkaitao.shiro.chapter17.web.controller 包下的 IndexController、LoginController、UserController 和 ClientController,其用于維護(hù)后端的數(shù)據(jù),如用戶(hù)及客戶(hù)端數(shù)據(jù);即相當(dāng)于后臺(tái)管理。
@Controller
public class AuthorizeController {
@Autowired
private OAuthService oAuthService;
@Autowired
private ClientService clientService;
@RequestMapping("/authorize")
public Object authorize(Model model, HttpServletRequest request)
throws URISyntaxException, OAuthSystemException {
try {
//構(gòu)建OAuth 授權(quán)請(qǐng)求
OAuthAuthzRequest oauthRequest = new OAuthAuthzRequest(request);
//檢查傳入的客戶(hù)端id是否正確
if (!oAuthService.checkClientId(oauthRequest.getClientId())) {
OAuthResponse response = OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
.setError(OAuthError.TokenResponse.INVALID_CLIENT)
.setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage();
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
}
Subject subject = SecurityUtils.getSubject();
//如果用戶(hù)沒(méi)有登錄,跳轉(zhuǎn)到登陸頁(yè)面
if(!subject.isAuthenticated()) {
if(!login(subject, request)) {//登錄失敗時(shí)跳轉(zhuǎn)到登陸頁(yè)面
model.addAttribute("client",
clientService.findByClientId(oauthRequest.getClientId()));
return "oauth2login";
}
}
String username = (String)subject.getPrincipal();
//生成授權(quán)碼
String authorizationCode = null;
//responseType目前僅支持CODE,另外還有TOKEN
String responseType = oauthRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE);
if (responseType.equals(ResponseType.CODE.toString())) {
OAuthIssuerImpl oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
authorizationCode = oauthIssuerImpl.authorizationCode();
oAuthService.addAuthCode(authorizationCode, username);
}
//進(jìn)行OAuth響應(yīng)構(gòu)建
OAuthASResponse.OAuthAuthorizationResponseBuilder builder =
OAuthASResponse.authorizationResponse(request,
HttpServletResponse.SC_FOUND);
//設(shè)置授權(quán)碼
builder.setCode(authorizationCode);
//得到到客戶(hù)端重定向地址
String redirectURI = oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI);
//構(gòu)建響應(yīng)
final OAuthResponse response = builder.location(redirectURI).buildQueryMessage();
//根據(jù)OAuthResponse返回ResponseEntity響應(yīng)
HttpHeaders headers = new HttpHeaders();
headers.setLocation(new URI(response.getLocationUri()));
return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
} catch (OAuthProblemException e) {
//出錯(cuò)處理
String redirectUri = e.getRedirectUri();
if (OAuthUtils.isEmpty(redirectUri)) {
//告訴客戶(hù)端沒(méi)有傳入redirectUri直接報(bào)錯(cuò)
return new ResponseEntity(
"OAuth callback url needs to be provided by client!!!", HttpStatus.NOT_FOUND);
}
//返回錯(cuò)誤消息(如?error=)
final OAuthResponse response =
OAuthASResponse.errorResponse(HttpServletResponse.SC_FOUND)
.error(e).location(redirectUri).buildQueryMessage();
HttpHeaders headers = new HttpHeaders();
headers.setLocation(new URI(response.getLocationUri()));
return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
}
}
private boolean login(Subject subject, HttpServletRequest request) {
if("get".equalsIgnoreCase(request.getMethod())) {
return false;
}
String username = request.getParameter("username");
String password = request.getParameter("password");
if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
return false;
}
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
return true;
} catch (Exception e) {
request.setAttribute("error", "登錄失敗:" + e.getClass().getName());
return false;
}
}
}
如上代碼的作用:
http://localhost:8080/chapter17-server/authorize?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&response_type=code&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login
訪(fǎng)問(wèn)授權(quán)頁(yè)面;http://localhost:9080/chapter17-client/oauth2-login?code=52b1832f5dff68122f4f00ae995da0ed
;在重定向到的地址中會(huì)帶上 code 參數(shù)(授權(quán)碼),接著客戶(hù)端可以根據(jù)授權(quán)碼去換取 access token。 @RestController
public class AccessTokenController {
@Autowired
private OAuthService oAuthService;
@Autowired
private UserService userService;
@RequestMapping("/accessToken")
public HttpEntity token(HttpServletRequest request)
throws URISyntaxException, OAuthSystemException {
try {
//構(gòu)建OAuth請(qǐng)求
OAuthTokenRequest oauthRequest = new OAuthTokenRequest(request);
//檢查提交的客戶(hù)端id是否正確
if (!oAuthService.checkClientId(oauthRequest.getClientId())) {
OAuthResponse response = OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
.setError(OAuthError.TokenResponse.INVALID_CLIENT)
.setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage();
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
}
// 檢查客戶(hù)端安全KEY是否正確
if (!oAuthService.checkClientSecret(oauthRequest.getClientSecret())) {
OAuthResponse response = OAuthASResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setError(OAuthError.TokenResponse.UNAUTHORIZED_CLIENT)
.setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage();
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
}
String authCode = oauthRequest.getParam(OAuth.OAUTH_CODE);
// 檢查驗(yàn)證類(lèi)型,此處只檢查AUTHORIZATION_CODE類(lèi)型,其他的還有PASSWORD或REFRESH_TOKEN
if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(
GrantType.AUTHORIZATION_CODE.toString())) {
if (!oAuthService.checkAuthCode(authCode)) {
OAuthResponse response = OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
.setError(OAuthError.TokenResponse.INVALID_GRANT)
.setErrorDescription("錯(cuò)誤的授權(quán)碼")
.buildJSONMessage();
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
}
}
//生成Access Token
OAuthIssuer oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
final String accessToken = oauthIssuerImpl.accessToken();
oAuthService.addAccessToken(accessToken,
oAuthService.getUsernameByAuthCode(authCode));
//生成OAuth響應(yīng)
OAuthResponse response = OAuthASResponse
.tokenResponse(HttpServletResponse.SC_OK)
.setAccessToken(accessToken)
.setExpiresIn(String.valueOf(oAuthService.getExpireIn()))
.buildJSONMessage();
//根據(jù)OAuthResponse生成ResponseEntity
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
} catch (OAuthProblemException e) {
//構(gòu)建錯(cuò)誤響應(yīng)
OAuthResponse res = OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e)
.buildJSONMessage();
return new ResponseEntity(res.getBody(), HttpStatus.valueOf(res.getResponseStatus()));
}
}
}
如上代碼的作用:
http://localhost:8080/chapter17-server/accessToken
,POST 提交如下數(shù)據(jù):client_id= c1ebe466-1cdc-4bd3-ab69-77c3561b9dee& client_secret= d8346ea2-6017-43ed-ad68-19c0f971738b&grant_type=authorization_code&code=828beda907066d058584f37bcfd597b6&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login
訪(fǎng)問(wèn);@RestController
public class UserInfoController {
@Autowired
private OAuthService oAuthService;
@RequestMapping("/userInfo")
public HttpEntity userInfo(HttpServletRequest request) throws OAuthSystemException {
try {
//構(gòu)建OAuth資源請(qǐng)求
OAuthAccessResourceRequest oauthRequest =
new OAuthAccessResourceRequest(request, ParameterStyle.QUERY);
//獲取Access Token
String accessToken = oauthRequest.getAccessToken();
//驗(yàn)證Access Token
if (!oAuthService.checkAccessToken(accessToken)) {
// 如果不存在/過(guò)期了,返回未驗(yàn)證錯(cuò)誤,需重新驗(yàn)證
OAuthResponse oauthResponse = OAuthRSResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setRealm(Constants.RESOURCE_SERVER_NAME)
.setError(OAuthError.ResourceResponse.INVALID_TOKEN)
.buildHeaderMessage();
HttpHeaders headers = new HttpHeaders();
headers.add(OAuth.HeaderType.WWW_AUTHENTICATE,
oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED);
}
//返回用戶(hù)名
String username = oAuthService.getUsernameByAccessToken(accessToken);
return new ResponseEntity(username, HttpStatus.OK);
} catch (OAuthProblemException e) {
//檢查是否設(shè)置了錯(cuò)誤碼
String errorCode = e.getError();
if (OAuthUtils.isEmpty(errorCode)) {
OAuthResponse oauthResponse = OAuthRSResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setRealm(Constants.RESOURCE_SERVER_NAME)
.buildHeaderMessage();
HttpHeaders headers = new HttpHeaders();
headers.add(OAuth.HeaderType.WWW_AUTHENTICATE,
oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED);
}
OAuthResponse oauthResponse = OAuthRSResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setRealm(Constants.RESOURCE_SERVER_NAME)
.setError(e.getError())
.setErrorDescription(e.getDescription())
.setErrorUri(e.getUri())
.buildHeaderMessage();
HttpHeaders headers = new HttpHeaders();
headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, 、
oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
}
}
如上代碼的作用:
http://localhost:8080/chapter17-server/userInfo? access_token=828beda907066d058584f37bcfd597b6
進(jìn)行訪(fǎng)問(wèn);具體請(qǐng)參考 resources/spring*.xml
,此處只列舉 spring-config-shiro.xml 中的 shiroFilter 的 filterChainDefinitions 屬性:
<property name="filterChainDefinitions">
<value>
/ = anon
/login = authc
/logout = logout
/authorize=anon
/accessToken=anon
/userInfo=anon
/** = user
</value>
</property>
對(duì)于 oauth2 的幾個(gè)地址 /authorize、/accessToken、/userInfo 都是匿名可訪(fǎng)問(wèn)的。
其他源碼請(qǐng)直接下載文檔查看。
訪(fǎng)問(wèn) localhost:8080/chapter17-server/
,登錄后進(jìn)行客戶(hù)端管理和用戶(hù)管理。
客戶(hù)端管理就是進(jìn)行客戶(hù)端的注冊(cè),如新浪微博的第三方應(yīng)用就需要到新浪微博開(kāi)發(fā)平臺(tái)進(jìn)行注冊(cè);用戶(hù)管理就是進(jìn)行如新浪微博用戶(hù)的管理。
對(duì)于授權(quán)服務(wù)和資源服務(wù)的實(shí)現(xiàn)可以參考新浪微博開(kāi)發(fā)平臺(tái)的實(shí)現(xiàn):
客戶(hù)端流程:如果需要登錄首先跳到 oauth2 服務(wù)端進(jìn)行登錄授權(quán),成功后服務(wù)端返回 auth code,然后客戶(hù)端使用 auth code 去服務(wù)器端換取 access token,最好根據(jù) access token 獲取用戶(hù)信息進(jìn)行客戶(hù)端的登錄綁定。這個(gè)可以參照如很多網(wǎng)站的新浪微博登錄功能,或其他的第三方帳號(hào)登錄功能。
此處我們使用 apache oltu oauth2 客戶(hù)端實(shí)現(xiàn)。
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.client</artifactId>
<version>0.31</version>
</dependency>
其他的請(qǐng)參考 pom.xml。
類(lèi)似于 UsernamePasswordToken 和 CasToken;用于存儲(chǔ) oauth2 服務(wù)端返回的 auth code。
public class OAuth2Token implements AuthenticationToken {
private String authCode;
private String principal;
public OAuth2Token(String authCode) {
this.authCode = authCode;
}
//省略getter/setter
}
該 filter 的作用類(lèi)似于 FormAuthenticationFilter 用于 oauth2 客戶(hù)端的身份驗(yàn)證控制;如果當(dāng)前用戶(hù)還沒(méi)有身份驗(yàn)證,首先會(huì)判斷 url 中是否有 code(服務(wù)端返回的 auth code),如果沒(méi)有則重定向到服務(wù)端進(jìn)行登錄并授權(quán),然后返回 auth code;接著 OAuth2AuthenticationFilter 會(huì)用 auth code 創(chuàng)建 OAuth2Token,然后提交給 Subject.login 進(jìn)行登錄;接著 OAuth2Realm 會(huì)根據(jù) OAuth2Token 進(jìn)行相應(yīng)的登錄邏輯。
public class OAuth2AuthenticationFilter extends AuthenticatingFilter {
//oauth2 authc code參數(shù)名
private String authcCodeParam = "code";
//客戶(hù)端id
private String clientId;
//服務(wù)器端登錄成功/失敗后重定向到的客戶(hù)端地址
private String redirectUrl;
//oauth2服務(wù)器響應(yīng)類(lèi)型
private String responseType = "code";
private String failureUrl;
//省略setter
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String code = httpRequest.getParameter(authcCodeParam);
return new OAuth2Token(code);
}
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return false;
}
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
String error = request.getParameter("error");
String errorDescription = request.getParameter("error_description");
if(!StringUtils.isEmpty(error)) {//如果服務(wù)端返回了錯(cuò)誤
WebUtils.issueRedirect(request, response, failureUrl + "?error=" + error + "error_description=" + errorDescription);
return false;
}
Subject subject = getSubject(request, response);
if(!subject.isAuthenticated()) {
if(StringUtils.isEmpty(request.getParameter(authcCodeParam))) {
//如果用戶(hù)沒(méi)有身份驗(yàn)證,且沒(méi)有auth code,則重定向到服務(wù)端授權(quán)
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
//執(zhí)行父類(lèi)里的登錄邏輯,調(diào)用Subject.login登錄
return executeLogin(request, response);
}
//登錄成功后的回調(diào)方法 重定向到成功頁(yè)面
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
issueSuccessRedirect(request, response);
return false;
}
//登錄失敗后的回調(diào)
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request,
ServletResponse response) {
Subject subject = getSubject(request, response);
if (subject.isAuthenticated() || subject.isRemembered()) {
try { //如果身份驗(yàn)證成功了 則也重定向到成功頁(yè)面
issueSuccessRedirect(request, response);
} catch (Exception e) {
e.printStackTrace();
}
} else {
try { //登錄失敗時(shí)重定向到失敗頁(yè)面
WebUtils.issueRedirect(request, response, failureUrl);
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
}
該攔截器的作用:
public class OAuth2Realm extends AuthorizingRealm {
private String clientId;
private String clientSecret;
private String accessTokenUrl;
private String userInfoUrl;
private String redirectUrl;
//省略setter
public boolean supports(AuthenticationToken token) {
return token instanceof OAuth2Token; //表示此Realm只支持OAuth2Token類(lèi)型
}
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
return authorizationInfo;
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
OAuth2Token oAuth2Token = (OAuth2Token) token;
String code = oAuth2Token.getAuthCode(); //獲取 auth code
String username = extractUsername(code); // 提取用戶(hù)名
SimpleAuthenticationInfo authenticationInfo =
new SimpleAuthenticationInfo(username, code, getName());
return authenticationInfo;
}
private String extractUsername(String code) {
try {
OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
OAuthClientRequest accessTokenRequest = OAuthClientRequest
.tokenLocation(accessTokenUrl)
.setGrantType(GrantType.AUTHORIZATION_CODE)
.setClientId(clientId).setClientSecret(clientSecret)
.setCode(code).setRedirectURI(redirectUrl)
.buildQueryMessage();
//獲取access token
OAuthAccessTokenResponse oAuthResponse =
oAuthClient.accessToken(accessTokenRequest, OAuth.HttpMethod.POST);
String accessToken = oAuthResponse.getAccessToken();
Long expiresIn = oAuthResponse.getExpiresIn();
//獲取user info
OAuthClientRequest userInfoRequest =
new OAuthBearerClientRequest(userInfoUrl)
.setAccessToken(accessToken).buildQueryMessage();
OAuthResourceResponse resourceResponse = oAuthClient.resource(
userInfoRequest, OAuth.HttpMethod.GET, OAuthResourceResponse.class);
String username = resourceResponse.getBody();
return username;
} catch (Exception e) {
throw new OAuth2AuthenticationException(e);
}
}
}
此 Realm 首先只支持 OAuth2Token 類(lèi)型的 Token;然后通過(guò)傳入的 auth code 去換取 access token;再根據(jù) access token 去獲取用戶(hù)信息(用戶(hù)名),然后根據(jù)此信息創(chuàng)建 AuthenticationInfo;如果需要 AuthorizationInfo 信息,可以根據(jù)此處獲取的用戶(hù)名再根據(jù)自己的業(yè)務(wù)規(guī)則去獲取。
<bean id="oAuth2Realm" class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2Realm">
<property name="cachingEnabled" value="true"/>
<property name="authenticationCachingEnabled" value="true"/>
<property name="authenticationCacheName" value="authenticationCache"/>
<property name="authorizationCachingEnabled" value="true"/>
<property name="authorizationCacheName" value="authorizationCache"/>
<property name="clientId" value="c1ebe466-1cdc-4bd3-ab69-77c3561b9dee"/>
<property name="clientSecret" value="d8346ea2-6017-43ed-ad68-19c0f971738b"/>
<property name="accessTokenUrl"
value="http://localhost:8080/chapter17-server/accessToken"/>
<property name="userInfoUrl" value="http://localhost:8080/chapter17-server/userInfo"/>
<property name="redirectUrl" value="http://localhost:9080/chapter17-client/oauth2-login"/>
</bean>
此 OAuth2Realm 需要配置在服務(wù)端申請(qǐng)的 clientId 和 clientSecret;及用于根據(jù) auth code 換取 access token 的 accessTokenUrl 地址;及用于根據(jù) access token 換取用戶(hù)信息(受保護(hù)資源)的 userInfoUrl 地址。
<bean id="oAuth2AuthenticationFilter" class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2AuthenticationFilter">
<property name="authcCodeParam" value="code"/>
<property name="failureUrl" value="/oauth2Failure.jsp"/>
</bean>
此 OAuth2AuthenticationFilter 用于攔截服務(wù)端重定向回來(lái)的 auth code。
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="http://localhost:8080/chapter17-server/authorize?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&response_type=code&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login"/>
<property name="successUrl" value="/"/>
<property name="filters">
<util:map>
<entry key="oauth2Authc" value-ref="oAuth2AuthenticationFilter"/>
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/ = anon
/oauth2Failure.jsp = anon
/oauth2-login = oauth2Authc
/logout = logout
/** = user
</value>
</property>
</bean>
此處設(shè)置 loginUrl 為 http://localhost:8080/chapter17-server/authorize?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&response_type=code&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login"
;其會(huì)自動(dòng)設(shè)置到所有的 AccessControlFilter,如 oAuth2AuthenticationFilter;另外 /oauth2-login = oauth2Authc 表示 /oauth2-login 地址使用 oauth2Authc 攔截器攔截并進(jìn)行 oauth2 客戶(hù)端授權(quán)。
1、首先訪(fǎng)問(wèn) http://localhost:9080/chapter17-client/
,然后點(diǎn)擊登錄按鈕進(jìn)行登錄,會(huì)跳到如下頁(yè)面:
2、輸入用戶(hù)名進(jìn)行登錄并授權(quán);
3、如果登錄成功,服務(wù)端會(huì)重定向到客戶(hù)端,即之前客戶(hù)端提供的地址 http://localhost:9080/chapter17-client/oauth2-login?code=473d56015bcf576f2ca03eac1a5bcc11
,并帶著 auth code 過(guò)去;
4、客戶(hù)端的 OAuth2AuthenticationFilter 會(huì)收集此 auth code,并創(chuàng)建 OAuth2Token 提交給 Subject 進(jìn)行客戶(hù)端登錄;
5、客戶(hù)端的 Subject 會(huì)委托給 OAuth2Realm 進(jìn)行身份驗(yàn)證;此時(shí) OAuth2Realm 會(huì)根據(jù) auth code 換取 access token,再根據(jù) access token 獲取受保護(hù)的用戶(hù)信息;然后進(jìn)行客戶(hù)端登錄。
到此 OAuth2 的集成就完成了,此處的服務(wù)端和客戶(hù)端相對(duì)比較簡(jiǎn)單,沒(méi)有進(jìn)行一些異常檢測(cè),請(qǐng)參考如新浪微博進(jìn)行相應(yīng) API 及異常錯(cuò)誤碼的設(shè)計(jì)。
更多建議: