入门客AI创业平台(我带你入门,你带我飞行)
博文笔记

OAuth2 Java Shiro 服务器端

创建时间:2016-10-25 投稿人: 浏览次数:3803
转自:http://jinnianshilongnian.iteye.com/blog/2038646

目前很多开放平台如新浪微博开放平台都在使用提供开放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.   
  24.       Subject subject = SecurityUtils.getSubject();  
  25.       //如果用户没有登录,跳转到登陆页面  
  26.       if(!subject.isAuthenticated()) {  
  27.         if(!login(subject, request)) {//登录失败时跳转到登陆页面  
  28.           model.addAttribute("client",      
  29.               clientService.findByClientId(oauthRequest.getClientId()));  
  30.           return "oauth2login";  
  31.         }  
  32.       }  
  33.   
  34.       String username = (String)subject.getPrincipal();  
  35.       //生成授权码  
  36.       String authorizationCode = null;  
  37.       //responseType目前仅支持CODE,另外还有TOKEN  
  38.       String responseType = oauthRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE);  
  39.       if (responseType.equals(ResponseType.CODE.toString())) {  
  40.         OAuthIssuerImpl oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());  
  41.         authorizationCode = oauthIssuerImpl.authorizationCode();  
  42.         oAuthService.addAuthCode(authorizationCode, username);  
  43.       }  
  44.       //进行OAuth响应构建  
  45.       OAuthASResponse.OAuthAuthorizationResponseBuilder builder =  
  46.         OAuthASResponse.authorizationResponse(request,   
  47.                                            HttpServletResponse.SC_FOUND);  
  48.       //设置授权码  
  49.       builder.setCode(authorizationCode);  
  50.       //得到到客户端重定向地址  
  51.       String redirectURI = oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI);  
  52.   
  53.       //构建响应  
  54.       final OAuthResponse response = builder.location(redirectURI).buildQueryMessage();  
  55.       //根据OAuthResponse返回ResponseEntity响应  
  56.       HttpHeaders headers = new HttpHeaders();  
  57.       headers.setLocation(new URI(response.getLocationUri()));  
  58.       return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));  
  59.     } catch (OAuthProblemException e) {  
  60.       //出错处理  
  61.       String redirectUri = e.getRedirectUri();  
  62.       if (OAuthUtils.isEmpty(redirectUri)) {  
  63.         //告诉客户端没有传入redirectUri直接报错  
  64.         return new ResponseEntity(  
  65.           "OAuth callback url needs to be provided by client!!!", HttpStatus.NOT_FOUND);  
  66.       }  
  67.       //返回错误消息(如?error=)  
  68.       final OAuthResponse response =  
  69.               OAuthASResponse.errorResponse(HttpServletResponse.SC_FOUND)  
  70.                       .error(e).location(redirectUri).buildQueryMessage();  
  71.       HttpHeaders headers = new HttpHeaders();  
  72.       headers.setLocation(new URI(response.getLocationUri()));  
  73.       return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));  
  74.     }  
  75.   }  
  76.   
  77.   private boolean login(Subject subject, HttpServletRequest request) {  
  78.     if("get".equalsIgnoreCase(request.getMethod())) {  
  79.       return false;  
  80.     }  
  81.     String username = request.getParameter("username");  
  82.     String password = request.getParameter("password");  
  83.   
  84.     if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {  
  85.       return false;  
  86.     }  
  87.   
  88.     UsernamePasswordToken token = new UsernamePasswordToken(username, password);  
  89.     try {  
  90.       subject.login(token);  
  91.       return true;  
  92.     } catch (Exception e) {  
  93.       request.setAttribute("error", "登录失败:" + e.getClass().getName());  
  94.       return false;  
  95.     }  
  96.   }  
  97. }   

如上代码的作用:

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.   
  14.       //检查提交的客户端id是否正确  
  15.       if (!oAuthService.checkClientId(oauthRequest.getClientId())) {  
  16.         OAuthResponse response = OAuthASResponse  
  17.                 .errorResponse(HttpServletResponse.SC_BAD_REQUEST)  
  18.                 .setError(OAuthError.TokenResponse.INVALID_CLIENT)  
  19.                 .setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)  
  20.                 .buildJSONMessage();  
  21.        return new ResponseEntity(  
  22.          response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));  
  23.       }  
  24.   
  25.     // 检查客户端安全KEY是否正确  
  26.       if (!oAuthService.checkClientSecret(oauthRequest.getClientSecret())) {  
  27.         OAuthResponse response = OAuthASResponse  
  28.               .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)  
  29.               .setError(OAuthError.TokenResponse.UNAUTHORIZED_CLIENT)  
  30.               .setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)  
  31.               .buildJSONMessage();  
  32.       return new ResponseEntity(  
  33.           response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));  
  34.       }  
  35.     
  36.       String authCode = oauthRequest.getParam(OAuth.OAUTH_CODE);  
  37.       // 检查验证类型,此处只检查AUTHORIZATION_CODE类型,其他的还有PASSWORD或REFRESH_TOKEN  
  38.       if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(  
  39.          GrantType.AUTHORIZATION_CODE.toString())) {  
  40.          if (!oAuthService.checkAuthCode(authCode)) {  
  41.             OAuthResponse response = OAuthASResponse  
  42.                 .errorResponse(HttpServletResponse.SC_BAD_REQUEST)  
  43.                 .setError(OAuthError.TokenResponse.INVALID_GRANT)  
  44.                 .setErrorDescription("错误的授权码")  
  45.               .buildJSONMessage();  
  46.            return new ResponseEntity(  
  47.              response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));  
  48.          }  
  49.       }  
  50.   
  51.       //生成Access Token  
  52.       OAuthIssuer oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());  
  53.       final String accessToken = oauthIssuerImpl.accessToken();  
  54.       oAuthService.addAccessToken(accessToken,  
  55.           oAuthService.getUsernameByAuthCode(authCode));  
  56.   
  57.       //生成OAuth响应  
  58.       OAuthResponse response = OAuthASResponse  
  59.               .tokenResponse(HttpServletResponse.SC_OK)  
  60.               .setAccessToken(accessToken)  
  61.               .setExpiresIn(String.valueOf(oAuthService.getExpireIn()))  
  62.               .buildJSONMessage();  
  63.   
  64.       //根据OAuthResponse生成ResponseEntity  
  65.       return new ResponseEntity(  
  66.           response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));  
  67.     } catch (OAuthProblemException e) {  
  68.       //构建错误响应  
  69.       OAuthResponse res = OAuthASResponse  
  70.               .errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e)  
  71.               .buildJSONMessage();  
  72.      return new ResponseEntity(res.getBody(), HttpStatus.valueOf(res.getResponseStatus()));  
  73.    }  
  74.  }  
  75. }   

如上代码的作用:

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.   
  6.   @RequestMapping("/userInfo")  
  7.   public HttpEntity userInfo(HttpServletRequest request) throws OAuthSystemException {  
  8.     try {  
  9.       //构建OAuth资源请求  
  10.       OAuthAccessResourceRequest oauthRequest =   
  11.             new OAuthAccessResourceRequest(request, ParameterStyle.QUERY);  
  12.       //获取Access Token  
  13.       String accessToken = oauthRequest.getAccessToken();  
  14.   
  15.       //验证Access Token  
  16.       if (!oAuthService.checkAccessToken(accessToken)) {  
  17.         // 如果不存在/过期了,返回未验证错误,需重新验证  
  18.       OAuthResponse oauthResponse = OAuthRSResponse  
  19.               .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)  
  20.               .setRealm(Constants.RESOURCE_SERVER_NAME)  
  21.               .setError(OAuthError.ResourceResponse.INVALID_TOKEN)  
  22.               .buildHeaderMessage();  
  23.   
  24.         HttpHeaders headers = new HttpHeaders();  
  25.         headers.add(OAuth.HeaderType.WWW_AUTHENTICATE,   
  26.           oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));  
  27.       return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED);  
  28.       }  
  29.       //返回用户名  
  30.       String username = oAuthService.getUsernameByAccessToken(accessToken);  
  31.       return new ResponseEntity(username, HttpStatus.OK);  
  32.     } catch (OAuthProblemException e) {  
  33.       //检查是否设置了错误码  
  34.       String errorCode = e.getError();  
  35.       if (OAuthUtils.isEmpty(errorCode)) {  
  36.         OAuthResponse oauthResponse = OAuthRSResponse  
  37.                .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)  
  38.                .setRealm(Constants.RESOURCE_SERVER_NAME)  
  39.                .buildHeaderMessage();  
  40.   
  41.         HttpHeaders headers = new HttpHeaders();  
  42.         headers.add(OAuth.HeaderType.WWW_AUTHENTICATE,   
  43.           oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));  
  44.         return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED);  
  45.       }  
  46.   
  47.       OAuthResponse oauthResponse = OAuthRSResponse  
  48.                .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)  
  49.                .setRealm(Constants.RESOURCE_SERVER_NAME)  
  50.                .setError(e.getError())  
  51.                .setErrorDescription(e.getDescription())  
  52.                .setErrorUri(e.getUri())  
  53.                .buildHeaderMessage();  
  54.   
  55.       HttpHeaders headers = new HttpHeaders();  
  56.       headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, 、  
  57.         oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));  
  58.       return new ResponseEntity(HttpStatus.BAD_REQUEST);  
  59.     }  
  60.   }  
  61. }   

如上代码的作用:

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.   
  7.       /authorize=anon  
  8.       /accessToken=anon  
  9.       /userInfo=anon  
  10.   
  11.       /** = user  
  12.     </value>  
  13. </property>   

对于oauth2的几个地址/authorize、/accessToken、/userInfo都是匿名可访问的。

 

其他源码请直接下载文档查看。

 

服务器维护

访问localhost:8080/chapter17-server/,登录后进行客户端管理和用户管理。

客户端管理就是进行客户端的注册,如新浪微博的第三方应用就需要到新浪微博开发平台进行注册;用户管理就是进行如新浪微博用户的管理。

 

对于授权服务和资源服务的实现可以参考新浪微博开发平台的实现:

http://open.weibo.com/wiki/授权机制说明 

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


声明:该文观点仅代表作者本人,入门客AI创业平台信息发布平台仅提供信息存储空间服务,如有疑问请联系rumenke@qq.com。