Shiro学习(17)OAuth2集成

目前很多开放平台如新浪微博开放平台都在使用提供开放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及异常错误码的设计。

时间: 2024-08-26 16:11:50

Shiro学习(17)OAuth2集成的相关文章

2017.2.16 开涛shiro教程-第十七章-OAuth2集成

原博客地址:http://jinnianshilongnian.iteye.com/blog/2018398 根据下载的pdf学习. 开涛shiro教程-第十七章-OAuth2集成 1.OAuth2介绍 (1)应用场景 很多开放平台,比如新浪微博开放平台,都在使用开发API接口供开发者使用.即带来了,第三方应用要到开放平台授权的问题.OAuth就是做这个的. 1 OAuth2官网:http://oauth.net/2/ 2 OAuth2协议:http://tools.ietf.org/html/

Shiro学习笔记(5)——web集成

Web集成 shiro配置文件shiroini 界面 webxml最关键 Servlet 測试 基于 Basic 的拦截器身份验证 Web集成 大多数情况.web项目都会集成spring.shiro在普通web项目和spring项目中的配置是不一样的.关于spring-shiro集成,能够參考Shiro学习笔记(3)--授权(Authorization) 中的JSP标签授权部分演示样例代码 本次介绍普通的web项目,不使用不论什么框架. shiro配置文件(shiro.ini) 创建web项目.

Shiro学习(23)多项目集中权限管理

在做一些企业内部项目时或一些互联网后台时:可能会涉及到集中权限管理,统一进行多项目的权限管理:另外也需要统一的会话管理,即实现单点身份认证和授权控制. 学习本章之前,请务必先学习<第十章 会话管理>和<第十六章 综合实例>,本章代码都是基于这两章的代码基础上完成的. 本章示例是同域名的场景下完成的,如果跨域请参考<第十五章 单点登录>和<第十七章 OAuth2集成>了解使用CAS或OAuth2实现跨域的身份验证和授权.另外比如客户端/服务器端的安全校验可参考

Shiro学习(总结)

声明:本文原文地址:http://www.iteye.com/blogs/subjects/shiro 感谢开涛提供的博文,让我学到了很多,在这里由衷的感谢你,同时我强烈的推荐开涛的博文,他的博文力争完美,讲解细致,是基于实际的项目进行讲解,有理有据值得学习和借鉴.同时,在这里自荐一下,也希望大家也能够多多的关注我的博文,希望你的技术有所提高,大家共同学习共同进步. 下面是开涛整理的Shiro的目录,供大家学习. Shiro目录 第一章  Shiro简介 第二章  身份验证 第三章  授权 第四

shiro学习笔记_0600_自定义realm实现授权

博客shiro学习笔记_0400_自定义Realm实现身份认证 介绍了认证,这里介绍授权. 1,仅仅通过配置文件来指定权限不够灵活且不方便.在实际的应用中大多数情况下都是将用户信息,角色信息,权限信息 保存到了数据库中.所以需要从数据库中去获取相关的数据信息.可以使用 shiro 提供的JdbcRealm来实现,,也可以自定义realm来实现.使用jdbcRealm往往也不够灵活.所以在实际应用中大多数情况都是自定义Realm来实现. 2,自定义Realm 需要继承 AuthorizingRea

ndk学习17: jni之Java调用C&C++

一.Hello World 1. 定义函数原型 native关键字定义的函数即为jni函数 2.生成头文件 切换到src目录执行: (这个过程可以写脚本自动完成,比如自动拷贝到jni目录) javah -jni 包名.类名 在根目录下生成: org_bing_testjni_MainActivity.h 3. 工程中添加jni代码 工程右键->添加native code->输入名字->finish 多了如下文 新建一个Application.mk,配置相关选项(详细查看ndk有关Appl

Apache Shiro学习笔记(六)FilterChain

鲁春利的工作笔记,好记性不如烂笔头 Apache Shiro学习笔记(七)IniWebEnvironment

ExtJS4.2学习(17)表单基本输入控件Ext.form.Field

Ext.form.Field是所有表单输入控件的基类,其他的输入控件都是基于Ext.form.Field扩展得来的. Ext.form.Field中定义了输入控件通用的属性和功能函数,这些通用的属性和功能函数大致分为3大类:页面显示样式.控件参数配置和数据有效性检验.我们先来看看表单输入控件可以使用的校验显示方式.默认情况下,这些输入控件会监听blur事件,如果数据校验失败,就会根据msgTarget中的设置显示错误信息.通常,msgTarget会被设置为qtip,即使用QuickTip显示错误

Shiro学习详解

1.Shiro基本架构 一.什么是Shiro Apache Shiro是一个强大易用的Java安全框架,提供了认证.授权.加密和会话管理等功能: 认证 - 用户身份识别,常被称为用户“登录”: 授权 - 访问控制: 密码加密 - 保护或隐藏数据防止被偷窥: 会话管理 - 每用户相关的时间敏感的状态. 对于任何一个应用程序,Shiro都可以提供全面的安全管理服务.并且相对于其他安全框架,Shiro要简单的多. 二.Shiro的架构介绍 首先,来了解一下Shiro的三个核心组件:Subject, S

Shiro学习笔记(2)——身份验证之Realm

环境准备 什么是Realm 为什么要用Realm 自定义Realm 多个Realm 配置Authenticator和AuthenticationStrategy 自定义AuthenticationStrategy验证策略 多个Realm验证顺序 环境准备 创建java工程 需要的jar包 大家也可以使用maven,参考官网 什么是Realm 在我所看的学习资料中,关于Realm的定义,写了整整一长串,但是对于初学者来说,看定义实在是太头疼了. 对于什么是Realm,我使用过之后,个人总结一下:s