OAuth2 Java Shiro 客户端
http://jinnianshilongnian.iteye.com/blog/2038646
客户端
客户端流程:如果需要登录首先跳到oauth2服务端进行登录授权,成功后服务端返回auth code,然后客户端使用auth code去服务器端换取access token,最好根据access token获取用户信息进行客户端的登录绑定。这个可以参照如很多网站的新浪微博登录功能,或其他的第三方帐号登录功能。
POM依赖
此处我们使用apache oltu oauth2客户端实现。
1. <dependency>
2. <groupId>org.apache.oltu.oauth2</groupId>
3. <artifactId>org.apache.oltu.oauth2.client</artifactId>
4. 0.31</version>
5. </dependency>
其他的请参考pom.xml。
OAuth2Token
类似于UsernamePasswordToken和CasToken;用于存储oauth2服务端返回的auth code。
1. public class OAuth2Token implements
2. private
3. private
4. public
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进行相应的登录逻辑。
1. public class OAuth2AuthenticationFilter extends
2. //oauth2 authc code参数名
3. private String authcCodeParam = "code";
4. //客户端id
5. private
6. //服务器端登录成功/失败后重定向到的客户端地址
7. private
8. //oauth2服务器响应类型
9. private String responseType = "code";
10. private
11. //省略setter
12. protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws
13. HttpServletRequest httpRequest = (HttpServletRequest) request;
14. String code = httpRequest.getParameter(authcCodeParam);
15. return new
16. }
17. protected boolean
18. return false;
19. }
20. protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws
21. "error");
22. "error_description");
23. if(!StringUtils.isEmpty(error)) {//如果服务端返回了错误
24. "?error=" + error + "error_descriptinotallow="
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
37. }
38.
39. //登录成功后的回调方法 重定向到成功页面
40. protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws
41. issueSuccessRedirect(request, response);
42. return false;
43. }
44.
45. //登录失败后的回调
46. protected boolean
47. ServletResponse response) {
48. Subject subject = getSubject(request, response);
49. if
50. try { //如果身份验证成功了 则也重定向到成功页面
51. issueSuccessRedirect(request, response);
52. catch
53. e.printStackTrace();
54. }
55. else
56. try { //登录失败时重定向到失败页面
57. WebUtils.issueRedirect(request, response, failureUrl);
58. catch
59. e.printStackTrace();
60. }
61. }
62. return false;
63. }
64. }
该拦截器的作用:
1、首先判断有没有服务端返回的error参数,如果有则直接重定向到失败页面;
2、接着如果用户还没有身份验证,判断是否有auth code参数(即是不是服务端授权之后返回的),如果没有则重定向到服务端进行授权;
3、否则调用executeLogin进行登录,通过auth code创建OAuth2Token提交给Subject进行登录;
4、登录成功将回调onLoginSuccess方法重定向到成功页面;
5、登录失败则回调onLoginFailure重定向到失败页面。
OAuth2Realm
1. public class OAuth2Realm extends
2. private
3. private
4. private
5. private
6. private
7. //省略setter
8. public boolean
9. return token instanceof OAuth2Token; //表示此Realm只支持OAuth2Token类型
10. }
11. protected
12. new
13. return
14. }
15. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws
16. OAuth2Token oAuth2Token = (OAuth2Token) token;
17. //获取 auth code
18. // 提取用户名
19. SimpleAuthenticationInfo authenticationInfo =
20. new
21. return
22. }
23. private
24. try
25. new OAuthClient(new
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
40. .setAccessToken(accessToken).buildQueryMessage();
41. OAuthResourceResponse resourceResponse = oAuthClient.resource(
42. class);
43. String username = resourceResponse.getBody();
44. return
45. catch
46. throw new
47. }
48. }
49. }
此Realm首先只支持OAuth2Token类型的Token;然后通过传入的auth code去换取access token;再根据access token去获取用户信息(用户名),然后根据此信息创建AuthenticationInfo;如果需要AuthorizationInfo信息,可以根据此处获取的用户名再根据自己的业务规则去获取。
Spring shiro配置(spring-config-shiro.xml)
1. <bean id="oAuth2Realm"
2. class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2Realm">
3. "cachingEnabled" value="true"/>
4. "authenticationCachingEnabled" value="true"/>
5. "authenticationCacheName" value="authenticationCache"/>
6. "authorizationCachingEnabled" value="true"/>
7. "authorizationCacheName" value="authorizationCache"/>
8. "clientId" value="c1ebe466-1cdc-4bd3-ab69-77c3561b9dee"/>
9. "clientSecret" value="d8346ea2-6017-43ed-ad68-19c0f971738b"/>
10. "accessTokenUrl"
11. "http://localhost:8080/chapter17-server/accessToken"/>
12. "userInfoUrl" value="http://localhost:8080/chapter17-server/userInfo"/>
13. "redirectUrl" value="http://localhost:9080/chapter17-client/oauth2-login"/>
14. </bean>
此OAuth2Realm需要配置在服务端申请的clientId和clientSecret;及用于根据auth code换取access token的accessTokenUrl地址;及用于根据access token换取用户信息(受保护资源)的userInfoUrl地址。
1. <bean id="oAuth2AuthenticationFilter"
2. class="com.github.zhangkaitao.shiro.chapter18.oauth2.OAuth2AuthenticationFilter">
3. "authcCodeParam" value="code"/>
4. "failureUrl" value="/oauth2Failure.jsp"/>
5. </bean>
此OAuth2AuthenticationFilter用于拦截服务端重定向回来的auth code。
1. <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
2. "securityManager" ref="securityManager"/>
3. "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"/>
4. "successUrl" value="/"/>
5. "filters">
6. <util:map>
7. "oauth2Authc" value-ref="oAuth2AuthenticationFilter"/>
8. </util:map>
9. </property>
10. "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&response_type=code&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及异常错误码的设计。
示例源代码:https://github.com/zhangkaitao/shiro-example;