1. 程式人生 > >Shiro學習(17)OAuth2集成

Shiro學習(17)OAuth2集成

ans -c 後臺 異常檢測 創建客戶端 下載 完成 weibo blank

目前很多開放平臺如新浪微博開放平臺都在使用提供開放API接口供開發者使用,隨之帶來了第三方應用要到開放平臺進行授權的問題,OAuth就是幹這個的,OAuth2是OAuth協議的下一個版本,相比OAuth1,OAuth2整個授權流程更簡單安全了,但不兼容OAuth1,具體可以到OAuth2官網http://oauth.net/2/查看,OAuth2協議規範可以參考http://tools.ietf.org/html/rfc6749。目前有好多參考實現供選擇,可以到其官網查看下載。

本文使用Apache Oltu,其之前的名字叫Apache Amber ,是Java版的參考實現。使用文檔可參考https://cwiki.apache.org/confluence/display/OLTU/Documentation。

OAuth角色

資源擁有者(resource owner):能授權訪問受保護資源的一個實體,可以是一個人,那我們稱之為最終用戶;如新浪微博用戶zhangsan;

資源服務器(resource server):存儲受保護資源,客戶端通過access token請求資源,資源服務器響應受保護資源給客戶端;存儲著用戶zhangsan的微博等信息。

授權服務器(authorization server):成功驗證資源擁有者並獲取授權之後,授權服務器頒發授權令牌(Access Token)給客戶端。

客戶端(client):如新浪微博客戶端weico、微格等第三方應用,也可以是它自己的官方應用;其本身不存儲資源,而是資源擁有者授權通過後,使用它的授權(授權令牌)訪問受保護資源,然後客戶端把相應的數據展示出來/提交到服務器。“客戶端”術語不代表任何特定實現(如應用運行在一臺服務器、桌面、手機或其他設備)。

OAuth2協議流程

技術分享

1、客戶端從資源擁有者那請求授權。授權請求可以直接發給資源擁有者,或間接的通過授權服務器這種中介,後者更可取。

2、客戶端收到一個授權許可,代表資源服務器提供的授權。

3、客戶端使用它自己的私有證書及授權許可到授權服務器驗證。

4、如果驗證成功,則下發一個訪問令牌。

5、客戶端使用訪問令牌向資源服務器請求受保護資源。

6、資源服務器會驗證訪問令牌的有效性,如果成功則下發受保護資源。

更多流程的解釋請參考OAuth2的協議規範http://tools.ietf.org/html/rfc6749。

服務器端

本文把授權服務器和資源服務器整合在一起實現。

POM依賴

此處我們使用apache oltu oauth2服務端實現,需要引入authzserver(授權服務器依賴)和resourceserver(資源服務器依賴)。

Java代碼 技術分享
  1. <dependency>
  2. <groupId>org.apache.oltu.oauth2</groupId>
  3. <artifactId>org.apache.oltu.oauth2.authzserver</artifactId>
  4. <version>0.31</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.apache.oltu.oauth2</groupId>
  8. <artifactId>org.apache.oltu.oauth2.resourceserver</artifactId>
  9. <version>0.31</version>
  10. </dependency>

其他的請參考pom.xml。

數據字典

用戶(oauth2_user)

名稱

類型

長度

描述

id

bigint

10

編號 主鍵

username

varchar

100

用戶名

password

varchar

100

密碼

salt

varchar

50

客戶端(oauth2_client)

名稱

類型

長度

描述

id

bigint

10

編號 主鍵

client_name

varchar

100

客戶端名稱

client_id

varchar

100

客戶端id

client_secret

varchar

100

客戶端安全key

用戶表存儲著認證/資源服務器的用戶信息,即資源擁有者;比如用戶名/密碼;客戶端表存儲客戶端的的客戶端id及客戶端安全key;在進行授權時使用。

表及數據SQL

具體請參考

sql/ shiro-schema.sql (表結構)

sql/ shiro-data.sql (初始數據)

默認用戶名/密碼是admin/123456。

實體

具體請參考com.github.zhangkaitao.shiro.chapter17.entity包下的實體,此處就不列舉了。

DAO

具體請參考com.github.zhangkaitao.shiro.chapter17.dao包下的DAO接口及實現。

Service

具體請參考com.github.zhangkaitao.shiro.chapter17.service包下的Service接口及實現。以下是出了基本CRUD之外的關鍵接口:

Java代碼 技術分享
  1. public interface UserService {
  2. public User createUser(User user);// 創建用戶
  3. public User updateUser(User user);// 更新用戶
  4. public void deleteUser(Long userId);// 刪除用戶
  5. public void changePassword(Long userId, String newPassword); //修改密碼
  6. User findOne(Long userId);// 根據id查找用戶
  7. List<User> findAll();// 得到所有用戶
  8. public User findByUsername(String username);// 根據用戶名查找用戶
  9. }
Java代碼 技術分享
  1. public interface ClientService {
  2. public Client createClient(Client client);// 創建客戶端
  3. public Client updateClient(Client client);// 更新客戶端
  4. public void deleteClient(Long clientId);// 刪除客戶端
  5. Client findOne(Long clientId);// 根據id查找客戶端
  6. List<Client> findAll();// 查找所有
  7. Client findByClientId(String clientId);// 根據客戶端id查找客戶端
  8. Client findByClientSecret(String clientSecret);//根據客戶端安全KEY查找客戶端
  9. }
Java代碼 技術分享
  1. public interface OAuthService {
  2. public void addAuthCode(String authCode, String username);// 添加 auth code
  3. public void addAccessToken(String accessToken, String username); // 添加 access token
  4. boolean checkAuthCode(String authCode); // 驗證auth code是否有效
  5. boolean checkAccessToken(String accessToken); // 驗證access token是否有效
  6. String getUsernameByAuthCode(String authCode);// 根據auth code獲取用戶名
  7. String getUsernameByAccessToken(String accessToken);// 根據access token獲取用戶名
  8. long getExpireIn();//auth code / access token 過期時間
  9. public boolean checkClientId(String clientId);// 檢查客戶端id是否存在
  10. public boolean checkClientSecret(String clientSecret);// 堅持客戶端安全KEY是否存在
  11. }

此處通過OAuthService實現進行auth code和access token的維護。

後端數據維護控制器

具體請參考com.github.zhangkaitao.shiro.chapter17.web.controller包下的IndexController、LoginController、UserController和ClientController,其用於維護後端的數據,如用戶及客戶端數據;即相當於後臺管理。

授權控制器AuthorizeController

Java代碼 技術分享
  1. @Controller
  2. public class AuthorizeController {
  3. @Autowired
  4. private OAuthService oAuthService;
  5. @Autowired
  6. private ClientService clientService;
  7. @RequestMapping("/authorize")
  8. public Object authorize(Model model, HttpServletRequest request)
  9. throws URISyntaxException, OAuthSystemException {
  10. try {
  11. //構建OAuth 授權請求
  12. OAuthAuthzRequest oauthRequest = new OAuthAuthzRequest(request);
  13. //檢查傳入的客戶端id是否正確
  14. if (!oAuthService.checkClientId(oauthRequest.getClientId())) {
  15. OAuthResponse response = OAuthASResponse
  16. .errorResponse(HttpServletResponse.SC_BAD_REQUEST)
  17. .setError(OAuthError.TokenResponse.INVALID_CLIENT)
  18. .setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
  19. .buildJSONMessage();
  20. return new ResponseEntity(
  21. response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
  22. }
  23. Subject subject = SecurityUtils.getSubject();
  24. //如果用戶沒有登錄,跳轉到登陸頁面
  25. if(!subject.isAuthenticated()) {
  26. if(!login(subject, request)) {//登錄失敗時跳轉到登陸頁面
  27. model.addAttribute("client",
  28. clientService.findByClientId(oauthRequest.getClientId()));
  29. return "oauth2login";
  30. }
  31. }
  32. String username = (String)subject.getPrincipal();
  33. //生成授權碼
  34. String authorizationCode = null;
  35. //responseType目前僅支持CODE,另外還有TOKEN
  36. String responseType = oauthRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE);
  37. if (responseType.equals(ResponseType.CODE.toString())) {
  38. OAuthIssuerImpl oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
  39. authorizationCode = oauthIssuerImpl.authorizationCode();
  40. oAuthService.addAuthCode(authorizationCode, username);
  41. }
  42. //進行OAuth響應構建
  43. OAuthASResponse.OAuthAuthorizationResponseBuilder builder =
  44. OAuthASResponse.authorizationResponse(request,
  45. HttpServletResponse.SC_FOUND);
  46. //設置授權碼
  47. builder.setCode(authorizationCode);
  48. //得到到客戶端重定向地址
  49. String redirectURI = oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI);
  50. //構建響應
  51. final OAuthResponse response = builder.location(redirectURI).buildQueryMessage();
  52. //根據OAuthResponse返回ResponseEntity響應
  53. HttpHeaders headers = new HttpHeaders();
  54. headers.setLocation(new URI(response.getLocationUri()));
  55. return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
  56. } catch (OAuthProblemException e) {
  57. //出錯處理
  58. String redirectUri = e.getRedirectUri();
  59. if (OAuthUtils.isEmpty(redirectUri)) {
  60. //告訴客戶端沒有傳入redirectUri直接報錯
  61. return new ResponseEntity(
  62. "OAuth callback url needs to be provided by client!!!", HttpStatus.NOT_FOUND);
  63. }
  64. //返回錯誤消息(如?error=)
  65. final OAuthResponse response =
  66. OAuthASResponse.errorResponse(HttpServletResponse.SC_FOUND)
  67. .error(e).location(redirectUri).buildQueryMessage();
  68. HttpHeaders headers = new HttpHeaders();
  69. headers.setLocation(new URI(response.getLocationUri()));
  70. return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
  71. }
  72. }
  73. private boolean login(Subject subject, HttpServletRequest request) {
  74. if("get".equalsIgnoreCase(request.getMethod())) {
  75. return false;
  76. }
  77. String username = request.getParameter("username");
  78. String password = request.getParameter("password");
  79. if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
  80. return false;
  81. }
  82. UsernamePasswordToken token = new UsernamePasswordToken(username, password);
  83. try {
  84. subject.login(token);
  85. return true;
  86. } catch (Exception e) {
  87. request.setAttribute("error", "登錄失敗:" + e.getClass().getName());
  88. return false;
  89. }
  90. }
  91. }

如上代碼的作用:

1、首先通過如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訪問授權頁面;

2、該控制器首先檢查clientId是否正確;如果錯誤將返回相應的錯誤信息;

3、然後判斷用戶是否登錄了,如果沒有登錄首先到登錄頁面登錄;

4、登錄成功後生成相應的auth code即授權碼,然後重定向到客戶端地址,如http://localhost:9080/chapter17-client/oauth2-login?code=52b1832f5dff68122f4f00ae995da0ed;在重定向到的地址中會帶上code參數(授權碼),接著客戶端可以根據授權碼去換取access token。

訪問令牌控制器AccessTokenController

Java代碼 技術分享
  1. @RestController
  2. public class AccessTokenController {
  3. @Autowired
  4. private OAuthService oAuthService;
  5. @Autowired
  6. private UserService userService;
  7. @RequestMapping("/accessToken")
  8. public HttpEntity token(HttpServletRequest request)
  9. throws URISyntaxException, OAuthSystemException {
  10. try {
  11. //構建OAuth請求
  12. OAuthTokenRequest oauthRequest = new OAuthTokenRequest(request);
  13. //檢查提交的客戶端id是否正確
  14. if (!oAuthService.checkClientId(oauthRequest.getClientId())) {
  15. OAuthResponse response = OAuthASResponse
  16. .errorResponse(HttpServletResponse.SC_BAD_REQUEST)
  17. .setError(OAuthError.TokenResponse.INVALID_CLIENT)
  18. .setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
  19. .buildJSONMessage();
  20. return new ResponseEntity(
  21. response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
  22. }
  23. // 檢查客戶端安全KEY是否正確
  24. if (!oAuthService.checkClientSecret(oauthRequest.getClientSecret())) {
  25. OAuthResponse response = OAuthASResponse
  26. .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
  27. .setError(OAuthError.TokenResponse.UNAUTHORIZED_CLIENT)
  28. .setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
  29. .buildJSONMessage();
  30. return new ResponseEntity(
  31. response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
  32. }
  33. String authCode = oauthRequest.getParam(OAuth.OAUTH_CODE);
  34. // 檢查驗證類型,此處只檢查AUTHORIZATION_CODE類型,其他的還有PASSWORD或REFRESH_TOKEN
  35. if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(
  36. GrantType.AUTHORIZATION_CODE.toString())) {
  37. if (!oAuthService.checkAuthCode(authCode)) {
  38. OAuthResponse response = OAuthASResponse
  39. .errorResponse(HttpServletResponse.SC_BAD_REQUEST)
  40. .setError(OAuthError.TokenResponse.INVALID_GRANT)
  41. .setErrorDescription("錯誤的授權碼")
  42. .buildJSONMessage();
  43. return new ResponseEntity(
  44. response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
  45. }
  46. }
  47. //生成Access Token
  48. OAuthIssuer oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
  49. final String accessToken = oauthIssuerImpl.accessToken();
  50. oAuthService.addAccessToken(accessToken,
  51. oAuthService.getUsernameByAuthCode(authCode));
  52. //生成OAuth響應
  53. OAuthResponse response = OAuthASResponse
  54. .tokenResponse(HttpServletResponse.SC_OK)
  55. .setAccessToken(accessToken)
  56. .setExpiresIn(String.valueOf(oAuthService.getExpireIn()))
  57. .buildJSONMessage();
  58. //根據OAuthResponse生成ResponseEntity
  59. return new ResponseEntity(
  60. response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
  61. } catch (OAuthProblemException e) {
  62. //構建錯誤響應
  63. OAuthResponse res = OAuthASResponse
  64. .errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e)
  65. .buildJSONMessage();
  66. return new ResponseEntity(res.getBody(), HttpStatus.valueOf(res.getResponseStatus()));
  67. }
  68. }
  69. }

如上代碼的作用:

1、首先通過如http://localhost:8080/chapter17-server/accessToken,POST提交如下數據: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訪問;

2、該控制器會驗證client_id、client_secret、auth code的正確性,如果錯誤會返回相應的錯誤;

3、如果驗證通過會生成並返回相應的訪問令牌access token。

資源控制器UserInfoController

Java代碼 技術分享
  1. @RestController
  2. public class UserInfoController {
  3. @Autowired
  4. private OAuthService oAuthService;
  5. @RequestMapping("/userInfo")
  6. public HttpEntity userInfo(HttpServletRequest request) throws OAuthSystemException {
  7. try {
  8. //構建OAuth資源請求
  9. OAuthAccessResourceRequest oauthRequest =
  10. new OAuthAccessResourceRequest(request, ParameterStyle.QUERY);
  11. //獲取Access Token
  12. String accessToken = oauthRequest.getAccessToken();
  13. //驗證Access Token
  14. if (!oAuthService.checkAccessToken(accessToken)) {
  15. // 如果不存在/過期了,返回未驗證錯誤,需重新驗證
  16. OAuthResponse oauthResponse = OAuthRSResponse
  17. .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
  18. .setRealm(Constants.RESOURCE_SERVER_NAME)
  19. .setError(OAuthError.ResourceResponse.INVALID_TOKEN)
  20. .buildHeaderMessage();
  21. HttpHeaders headers = new HttpHeaders();
  22. headers.add(OAuth.HeaderType.WWW_AUTHENTICATE,
  23. oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
  24. return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED);
  25. }
  26. //返回用戶名
  27. String username = oAuthService.getUsernameByAccessToken(accessToken);
  28. return new ResponseEntity(username, HttpStatus.OK);
  29. } catch (OAuthProblemException e) {
  30. //檢查是否設置了錯誤碼
  31. String errorCode = e.getError();
  32. if (OAuthUtils.isEmpty(errorCode)) {
  33. OAuthResponse oauthResponse = OAuthRSResponse
  34. .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
  35. .setRealm(Constants.RESOURCE_SERVER_NAME)
  36. .buildHeaderMessage();
  37. HttpHeaders headers = new HttpHeaders();
  38. headers.add(OAuth.HeaderType.WWW_AUTHENTICATE,
  39. oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
  40. return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED);
  41. }
  42. OAuthResponse oauthResponse = OAuthRSResponse
  43. .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
  44. .setRealm(Constants.RESOURCE_SERVER_NAME)
  45. .setError(e.getError())
  46. .setErrorDescription(e.getDescription())
  47. .setErrorUri(e.getUri())
  48. .buildHeaderMessage();
  49. HttpHeaders headers = new HttpHeaders();
  50. headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, 、
  51. oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
  52. return new ResponseEntity(HttpStatus.BAD_REQUEST);
  53. }
  54. }
  55. }

如上代碼的作用:

1、首先通過如http://localhost:8080/chapter17-server/userInfo? access_token=828beda907066d058584f37bcfd597b6進行訪問;

2、該控制器會驗證access token的有效性;如果無效了將返回相應的錯誤,客戶端再重新進行授權;

3、如果有效,則返回當前登錄用戶的用戶名。

spring配置文件

具體請參考resources/spring*.xml,此處只列舉spring-config-shiro.xml中的shiroFilter的filterChainDefinitions屬性:

Java代碼 技術分享
  1. <property name="filterChainDefinitions">
  2. <value>
  3. / = anon
  4. /login = authc
  5. /logout = logout
  6. /authorize=anon
  7. /accessToken=anon
  8. /userInfo=anon
  9. /** = user
  10. </value>
  11. </property>

對於oauth2的幾個地址/authorize、/accessToken、/userInfo都是匿名可訪問的。

其他源碼請直接下載文檔查看。

服務器維護

訪問localhost:8080/chapter17-server/,登錄後進行客戶端管理和用戶管理。

客戶端管理就是進行客戶端的註冊,如新浪微博的第三方應用就需要到新浪微博開發平臺進行註冊;用戶管理就是進行如新浪微博用戶的管理。

對於授權服務和資源服務的實現可以參考新浪微博開發平臺的實現:

http://open.weibo.com/wiki/授權機制說明

http://open.weibo.com/wiki/微博API

客戶端

客戶端流程:如果需要登錄首先跳到oauth2服務端進行登錄授權,成功後服務端返回auth code,然後客戶端使用auth code去服務器端換取access token,最好根據access token獲取用戶信息進行客戶端的登錄綁定。這個可以參照如很多網站的新浪微博登錄功能,或其他的第三方帳號登錄功能。

POM依賴

此處我們使用apache oltu oauth2客戶端實現。

Java代碼 技術分享
  1. <dependency>
  2. <groupId>org.apache.oltu.oauth2</groupId>
  3. <artifactId>org.apache.oltu.oauth2.client</artifactId>
  4. <version>0.31</version>
  5. </dependency>

其他的請參考pom.xml。

OAuth2Token

類似於UsernamePasswordToken和CasToken;用於存儲oauth2服務端返回的auth code。

Java代碼 技術分享
  1. public class OAuth2Token implements AuthenticationToken {
  2. private String authCode;
  3. private String principal;
  4. public OAuth2Token(String authCode) {
  5. this.authCode = authCode;
  6. }
  7. //省略getter/setter
  8. }

OAuth2AuthenticationFilter

該filter的作用類似於FormAuthenticationFilter用於oauth2客戶端的身份驗證控制;如果當前用戶還沒有身份驗證,首先會判斷url中是否有code(服務端返回的auth code),如果沒有則重定向到服務端進行登錄並授權,然後返回auth code;接著OAuth2AuthenticationFilter會用auth code創建OAuth2Token,然後提交給Subject.login進行登錄;接著OAuth2Realm會根據OAuth2Token進行相應的登錄邏輯。

Java代碼 技術分享
  1. public class OAuth2AuthenticationFilter extends AuthenticatingFilter {
  2. //oauth2 authc code參數名
  3. private String authcCodeParam = "code";
  4. //客戶端id
  5. private String clientId;
  6. //服務器端登錄成功/失敗後重定向到的客戶端地址
  7. private String redirectUrl;
  8. //oauth2服務器響應類型
  9. private String responseType = "code";
  10. private String failureUrl;
  11. //省略setter
  12. protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
  13. HttpServletRequest httpRequest = (HttpServletRequest) request;
  14. String code = httpRequest.getParameter(authcCodeParam);
  15. return new OAuth2Token(code);
  16. }
  17. protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
  18. return false;
  19. }
  20. protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
  21. String error = request.getParameter("error");
  22. String errorDescription = request.getParameter("error_description");
  23. if(!StringUtils.isEmpty(error)) {//如果服務端返回了錯誤
  24. WebUtils.issueRedirect(request, response, failureUrl + "?error=" + error + "error_description=" + errorDescription);
  25. return false;
  26. }
  27. Subject subject = getSubject(request, response);
  28. if(!subject.isAuthenticated()) {
  29. if(StringUtils.isEmpty(request.getParameter(authcCodeParam))) {
  30. //如果用戶沒有身份驗證,且沒有auth code,則重定向到服務端授權
  31. saveRequestAndRedirectToLogin(request, response);
  32. return false;
  33. }
  34. }
  35. //執行父類裏的登錄邏輯,調用Subject.login登錄
  36. return executeLogin(request, response);
  37. }
  38. //登錄成功後的回調方法 重定向到成功頁面
  39. protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
  40. issueSuccessRedirect(request, response);
  41. return false;
  42. }
  43. //登錄失敗後的回調
  44. protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request,
  45. ServletResponse response) {
  46. Subject subject = getSubject(request, response);
  47. if (subject.isAuthenticated() || subject.isRemembered()) {
  48. try { //如果身份驗證成功了 則也重定向到成功頁面
  49. issueSuccessRedirect(request, response);
  50. } catch (Exception e) {
  51. e.printStackTrace();
  52. }
  53. } else {
  54. try { //登錄失敗時重定向到失敗頁面
  55. WebUtils.issueRedirect(request, response, failureUrl);
  56. } catch (IOException e) {
  57. e.printStackTrace();
  58. }
  59. }
  60. return false;
  61. }
  62. }

該攔截器的作用:

1、首先判斷有沒有服務端返回的error參數,如果有則直接重定向到失敗頁面;

2、接著如果用戶還沒有身份驗證,判斷是否有auth code參數(即是不是服務端授權之後返回的),如果沒有則重定向到服務端進行授權;

3、否則調用executeLogin進行登錄,通過auth code創建OAuth2Token提交給Subject進行登錄;

4、登錄成功將回調onLoginSuccess方法重定向到成功頁面;

5、登錄失敗則回調onLoginFailure重定向到失敗頁面。

OAuth2Realm

Java代碼 技術分享
  1. public class OAuth2Realm extends AuthorizingRealm {
  2. private String clientId;
  3. private String clientSecret;
  4. private String accessTokenUrl;
  5. private String userInfoUrl;
  6. private String redirectUrl;
  7. //省略setter
  8. public boolean supports(AuthenticationToken token) {
  9. return token instanceof OAuth2Token; //表示此Realm只支持OAuth2Token類型
  10. }
  11. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  12. SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
  13. return authorizationInfo;
  14. }
  15. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  16. OAuth2Token oAuth2Token = (OAuth2Token) token;
  17. String code = oAuth2Token.getAuthCode(); //獲取 auth code
  18. String username = extractUsername(code); // 提取用戶名
  19. SimpleAuthenticationInfo authenticationInfo =
  20. new SimpleAuthenticationInfo(username, code, getName());
  21. return authenticationInfo;
  22. }
  23. private String extractUsername(String code) {
  24. try {
  25. OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
  26. OAuthClientRequest accessTokenRequest = OAuthClientRequest
  27. .tokenLocation(accessTokenUrl)
  28. .setGrantType(GrantType.AUTHORIZATION_CODE)
  29. .setClientId(clientId).setClientSecret(clientSecret)
  30. .setCode(code).setRedirectURI(redirectUrl)
  31. .buildQueryMessage();
  32. //獲取access token
  33. OAuthAccessTokenResponse oAuthResponse =
  34. oAuthClient.accessToken(accessTokenRequest, OAuth.HttpMethod.POST);
  35. String accessToken = oAuthResponse.getAccessToken();
  36. Long expiresIn = oAuthResponse.getExpiresIn();
  37. //獲取user info
  38. OAuthClientRequest userInfoRequest =
  39. new OAuthBearerClientRequest(userInfoUrl)
  40. .setAccessToken(accessToken).buildQueryMessage();
  41. OAuthResourceResponse resourceResponse = oAuthClient.resource(
  42. userInfoRequest, OAuth.HttpMethod.GET, OAuthResourceResponse.class);
  43. String username = resourceResponse.getBody();
  44. return username;
  45. } catch (Exception e) {
  46. throw new OAuth2AuthenticationException(e);
  47. }
  48. }
  49. }

此Realm首先只支持OAuth2Token類型的Token;然後通過傳入的auth code去換取access token;再根據access token去獲取用戶信息(用戶名),然後根據此信息創建AuthenticationInfo;如果需要AuthorizationInfo信息,可以根據此處獲取的用戶名再根據自己的業務規則去獲取。

Spring shiro配置(spring-config-shiro.xml)

Java代碼 技術分享
  1. <bean id="oAuth2Realm"
  2. class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2Realm">
  3. <property name="cachingEnabled" value="true"/>
  4. <property name="authenticationCachingEnabled" value="true"/>
  5. <property name="authenticationCacheName" value="authenticationCache"/>
  6. <property name="authorizationCachingEnabled" value="true"/>
  7. <property name="authorizationCacheName" value="authorizationCache"/>
  8. <property name="clientId" value="c1ebe466-1cdc-4bd3-ab69-77c3561b9dee"/>
  9. <property name="clientSecret" value="d8346ea2-6017-43ed-ad68-19c0f971738b"/>
  10. <property name="accessTokenUrl"
  11. value="http://localhost:8080/chapter17-server/accessToken"/>
  12. <property name="userInfoUrl" value="http://localhost:8080/chapter17-server/userInfo"/>
  13. <property name="redirectUrl" value="http://localhost:9080/chapter17-client/oauth2-login"/>
  14. </bean>

此OAuth2Realm需要配置在服務端申請的clientId和clientSecret;及用於根據auth code換取access token的accessTokenUrl地址;及用於根據access token換取用戶信息(受保護資源)的userInfoUrl地址。

Java代碼 技術分享
  1. <bean id="oAuth2AuthenticationFilter"
  2. class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2AuthenticationFilter">
  3. <property name="authcCodeParam" value="code"/>
  4. <property name="failureUrl" value="/oauth2Failure.jsp"/>
  5. </bean>

此OAuth2AuthenticationFilter用於攔截服務端重定向回來的auth code。

Java代碼 技術分享
  1. <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
  2. <property name="securityManager" ref="securityManager"/>
  3. <property name="loginUrl" value="http://localhost:8080/chapter17-server/authorize?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&amp;response_type=code&amp;redirect_uri=http://localhost:9080/chapter17-client/oauth2-login"/>
  4. <property name="successUrl" value="/"/>
  5. <property name="filters">
  6. <util:map>
  7. <entry key="oauth2Authc" value-ref="oAuth2AuthenticationFilter"/>
  8. </util:map>
  9. </property>
  10. <property name="filterChainDefinitions">
  11. <value>
  12. / = anon
  13. /oauth2Failure.jsp = anon
  14. /oauth2-login = oauth2Authc
  15. /logout = logout
  16. /** = user
  17. </value>
  18. </property>
  19. </bean>

此處設置loginUrl為http://localhost:8080/chapter17-server/authorize

?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&amp;response_type=code&amp;redirect_uri=http://localhost:9080/chapter17-client/oauth2-login";其會自動設置到所有的AccessControlFilter,如oAuth2AuthenticationFilter;另外/oauth2-login = oauth2Authc表示/oauth2-login地址使用oauth2Authc攔截器攔截並進行oauth2客戶端授權。

測試

1、首先訪問http://localhost:9080/chapter17-client/,然後點擊登錄按鈕進行登錄,會跳到如下頁面:

技術分享

2、輸入用戶名進行登錄並授權;

3、如果登錄成功,服務端會重定向到客戶端,即之前客戶端提供的地址http://localhost:9080/chapter17-client/oauth2-login?code=473d56015bcf576f2ca03eac1a5bcc11,並帶著auth code過去;

4、客戶端的OAuth2AuthenticationFilter會收集此auth code,並創建OAuth2Token提交給Subject進行客戶端登錄;

5、客戶端的Subject會委托給OAuth2Realm進行身份驗證;此時OAuth2Realm會根據auth code換取access token,再根據access token獲取受保護的用戶信息;然後進行客戶端登錄。

到此OAuth2的集成就完成了,此處的服務端和客戶端相對比較簡單,沒有進行一些異常檢測,請參考如新浪微博進行相應API及異常錯誤碼的設計。

Shiro學習(17)OAuth2集成