`
jinnianshilongnian
  • 浏览: 21431873 次
  • 性别: Icon_minigender_1
博客专栏
5c8dac6a-21dc-3466-8abb-057664ab39c7
跟我学spring3
浏览量:2404600
D659df3e-4ad7-3b12-8b9a-1e94abd75ac3
Spring杂谈
浏览量:2997292
43989fe4-8b6b-3109-aaec-379d27dd4090
跟开涛学SpringMVC...
浏览量:5631162
1df97887-a9e1-3328-b6da-091f51f886a1
Servlet3.1规范翻...
浏览量:257464
4f347843-a078-36c1-977f-797c7fc123fc
springmvc杂谈
浏览量:1593032
22722232-95c1-34f2-b8e1-d059493d3d98
hibernate杂谈
浏览量:248918
45b32b6f-7468-3077-be40-00a5853c9a48
跟我学Shiro
浏览量:5847128
Group-logo
跟我学Nginx+Lua开...
浏览量:697981
5041f67a-12b2-30ba-814d-b55f466529d5
亿级流量网站架构核心技术
浏览量:780313
社区版块
存档分类
最新评论

第十七章 OAuth2集成——《跟我学Shiro》

阅读更多

 

目录贴: 跟我学Shiro目录贴

 

 

目前很多开放平台如新浪微博开放平台都在使用提供开放API接口供开发者使用,随之带来了第三方应用要到开放平台进行授权的问题,OAuth就是干这个的,OAuth2OAuth协议的下一个版本,相比OAuth1OAuth2整个授权流程更简单安全了,但不兼容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(资源服务器依赖)。 

<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> 

其他的请参考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之外的关键接口: 

public interface UserService {
    public User createUser(User user);// 创建用户
    public User updateUser(User user);// 更新用户
    public void deleteUser(Long userId);// 删除用户
    public void changePassword(Long userId, String newPassword); //修改密码
    User findOne(Long userId);// 根据id查找用户
    List<User> findAll();// 得到所有用户
    public User findByUsername(String username);// 根据用户名查找用户
}
public interface ClientService {
    public Client createClient(Client client);// 创建客户端
    public Client updateClient(Client client);// 更新客户端
    public void deleteClient(Long clientId);// 删除客户端
    Client findOne(Long clientId);// 根据id查找客户端
    List<Client> findAll();// 查找所有
    Client findByClientId(String clientId);// 根据客户端id查找客户端
    Client findByClientSecret(String clientSecret);//根据客户端安全KEY查找客户端
}
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); // 验证auth code是否有效
   boolean checkAccessToken(String accessToken); // 验证access token是否有效
   String getUsernameByAuthCode(String authCode);// 根据auth code获取用户名
   String getUsernameByAccessToken(String accessToken);// 根据access token获取用户名
   long getExpireIn();//auth code / access token 过期时间
   public boolean checkClientId(String clientId);// 检查客户端id是否存在
   public boolean checkClientSecret(String clientSecret);// 坚持客户端安全KEY是否存在
} 

此处通过OAuthService实现进行auth codeaccess token的维护。

 

后端数据维护控制器

具体请参考com.github.zhangkaitao.shiro.chapter17.web.controller包下的IndexControllerLoginControllerUserControllerClientController,其用于维护后端的数据,如用户及客户端数据;即相当于后台管理。

 

授权控制器AuthorizeController      

@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 {
      //构建OAuth 授权请求
      OAuthAuthzRequest oauthRequest = new OAuthAuthzRequest(request);
      //检查传入的客户端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();
      //如果用户没有登录,跳转到登陆页面
      if(!subject.isAuthenticated()) {
        if(!login(subject, request)) {//登录失败时跳转到登陆页面
          model.addAttribute("client",    
              clientService.findByClientId(oauthRequest.getClientId()));
          return "oauth2login";
        }
      }

      String username = (String)subject.getPrincipal();
      //生成授权码
      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);
      }
      //进行OAuth响应构建
      OAuthASResponse.OAuthAuthorizationResponseBuilder builder =
        OAuthASResponse.authorizationResponse(request, 
                                           HttpServletResponse.SC_FOUND);
      //设置授权码
      builder.setCode(authorizationCode);
      //得到到客户端重定向地址
      String redirectURI = oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI);

      //构建响应
      final OAuthResponse response = builder.location(redirectURI).buildQueryMessage();
      //根据OAuthResponse返回ResponseEntity响应
      HttpHeaders headers = new HttpHeaders();
      headers.setLocation(new URI(response.getLocationUri()));
      return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
    } catch (OAuthProblemException e) {
      //出错处理
      String redirectUri = e.getRedirectUri();
      if (OAuthUtils.isEmpty(redirectUri)) {
        //告诉客户端没有传入redirectUri直接报错
        return new ResponseEntity(
          "OAuth callback url needs to be provided by client!!!", HttpStatus.NOT_FOUND);
      }
      //返回错误消息(如?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;
    }
  }
} 

如上代码的作用:

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  

@RestController
public class AccessTokenController {
  @Autowired
  private OAuthService oAuthService;
  @Autowired
  private UserService userService;
  @RequestMapping("/accessToken")
  public HttpEntity token(HttpServletRequest request)
          throws URISyntaxException, OAuthSystemException {
    try {
      //构建OAuth请求
      OAuthTokenRequest oauthRequest = new OAuthTokenRequest(request);

      //检查提交的客户端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()));
      }

    // 检查客户端安全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);
      // 检查验证类型,此处只检查AUTHORIZATION_CODE类型,其他的还有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("错误的授权码")
              .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响应
      OAuthResponse response = OAuthASResponse
              .tokenResponse(HttpServletResponse.SC_OK)
              .setAccessToken(accessToken)
              .setExpiresIn(String.valueOf(oAuthService.getExpireIn()))
              .buildJSONMessage();

      //根据OAuthResponse生成ResponseEntity
      return new ResponseEntity(
          response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
    } catch (OAuthProblemException e) {
      //构建错误响应
      OAuthResponse res = OAuthASResponse
              .errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e)
              .buildJSONMessage();
     return new ResponseEntity(res.getBody(), HttpStatus.valueOf(res.getResponseStatus()));
   }
 }
} 

如上代码的作用:

1、首先通过如http://localhost:8080/chapter17-server/accessTokenPOST提交如下数据: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_idclient_secretauth code的正确性,如果错误会返回相应的错误;

3、如果验证通过会生成并返回相应的访问令牌access token

 

资源控制器UserInfoController  

@RestController
public class UserInfoController {
  @Autowired
  private OAuthService oAuthService;

  @RequestMapping("/userInfo")
  public HttpEntity userInfo(HttpServletRequest request) throws OAuthSystemException {
    try {
      //构建OAuth资源请求
      OAuthAccessResourceRequest oauthRequest = 
            new OAuthAccessResourceRequest(request, ParameterStyle.QUERY);
      //获取Access Token
      String accessToken = oauthRequest.getAccessToken();

      //验证Access Token
      if (!oAuthService.checkAccessToken(accessToken)) {
        // 如果不存在/过期了,返回未验证错误,需重新验证
      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);
      }
      //返回用户名
      String username = oAuthService.getUsernameByAccessToken(accessToken);
      return new ResponseEntity(username, HttpStatus.OK);
    } catch (OAuthProblemException e) {
      //检查是否设置了错误码
      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);
    }
  }
} 

如上代码的作用:

1、首先通过如http://localhost:8080/chapter17-server/userInfo? access_token=828beda907066d058584f37bcfd597b6进行访问;

2、该控制器会验证access token的有效性;如果无效了将返回相应的错误,客户端再重新进行授权;

3、如果有效,则返回当前登录用户的用户名。

 

Spring配置文件

具体请参考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> 

对于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客户端实现。     

<dependency>
  <groupId>org.apache.oltu.oauth2</groupId>
  <artifactId>org.apache.oltu.oauth2.client</artifactId>
  <version>0.31</version>
</dependency> 

其他的请参考pom.xml

 

OAuth2Token

类似于UsernamePasswordToken和CasToken;用于存储oauth2服务端返回的auth code。  

public class OAuth2Token implements AuthenticationToken {
    private String authCode;
    private String principal;
    public OAuth2Token(String authCode) {
        this.authCode = authCode;
    }
    //省略getter/setter
} 

  

OAuth2AuthenticationFilter

该filter的作用类似于FormAuthenticationFilter用于oauth2客户端的身份验证控制;如果当前用户还没有身份验证,首先会判断url中是否有code(服务端返回的auth code),如果没有则重定向到服务端进行登录并授权,然后返回auth code;接着OAuth2AuthenticationFilter会用auth code创建OAuth2Token,然后提交给Subject.login进行登录;接着OAuth2Realm会根据OAuth2Token进行相应的登录逻辑。  

public class OAuth2AuthenticationFilter extends AuthenticatingFilter {
    //oauth2 authc code参数名
    private String authcCodeParam = "code";
    //客户端id
    private String clientId;
    //服务器端登录成功/失败后重定向到的客户端地址
    private String redirectUrl;
    //oauth2服务器响应类型
    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)) {//如果服务端返回了错误
            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))) {
                //如果用户没有身份验证,且没有auth code,则重定向到服务端授权
                saveRequestAndRedirectToLogin(request, response);
                return false;
            }
        }
        //执行父类里的登录逻辑,调用Subject.login登录
        return executeLogin(request, response);
    }

    //登录成功后的回调方法 重定向到成功页面
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,  ServletResponse response) throws Exception {
        issueSuccessRedirect(request, response);
        return false;
    }

    //登录失败后的回调 
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request,
                                     ServletResponse response) {
        Subject subject = getSubject(request, response);
        if (subject.isAuthenticated() || subject.isRemembered()) {
            try { //如果身份验证成功了 则也重定向到成功页面
                issueSuccessRedirect(request, response);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            try { //登录失败时重定向到失败页面
                WebUtils.issueRedirect(request, response, failureUrl);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
} 

该拦截器的作用:

1、首先判断有没有服务端返回的error参数,如果有则直接重定向到失败页面;

2、接着如果用户还没有身份验证,判断是否有auth code参数(即是不是服务端授权之后返回的),如果没有则重定向到服务端进行授权;

3、否则调用executeLogin进行登录,通过auth code创建OAuth2Token提交给Subject进行登录;

4、登录成功将回调onLoginSuccess方法重定向到成功页面;

5、登录失败则回调onLoginFailure重定向到失败页面。

 

OAuth2Realm  

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类型
    }
    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); // 提取用户名
        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类型的Token;然后通过传入的auth code去换取access token;再根据access token去获取用户信息(用户名),然后根据此信息创建AuthenticationInfo;如果需要AuthorizationInfo信息,可以根据此处获取的用户名再根据自己的业务规则去获取。

 

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

<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需要配置在服务端申请的clientIdclientSecret;及用于根据auth code换取access tokenaccessTokenUrl地址;及用于根据access token换取用户信息(受保护资源)的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用于拦截服务端重定向回来的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&amp;response_type=code&amp;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>

此处设置loginUrlhttp://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及异常错误码的设计。   

    

 

 

示例源代码:https://github.com/zhangkaitao/shiro-example;可加群 231889722 探讨Spring/Shiro技术。

        

  

27
1
分享到:
评论
33 楼 zdw0730x 2018-11-03  
请问大家一个问题,server端的授权控制器AuthorizeController,client第一次访问时会带上client_id,redirect_url等参数,server检查没有登录后跳转到登录页面,client会输入userName和password再次访问该controller。我的疑问是此时client应该只会持有userName和password参数,不会继续持有之前的client_id,redirect_url等参数,那server是怎么找回第一次访问时的client_id,redirect_url等参数的呢?我知道server有个保存请求url的方法,可以实现登录后找回并继续之前的请求url,但这里我没看见有这样的步骤。
32 楼 u013264924 2018-08-14  
用这个地址host:9080/chapter17-client/做调用时,通过authcode获取accessToken,再通过accessToken获取用户信息之后,会发起第二次重复调用,第二次code为空,调用服务端报错。请问一下这是怎么回事?
31 楼 lvyuan1234 2018-03-06  
yukun_sun 写道
貌似完整的 流程 没走完,拿到 授权code后,还需要重定向到授权服务器 去获取授权令牌吧,会用上client_secret,qq的 貌似 就是这样的

流程走完了,在extractUsername方法里面已经有那这授权code去请求授权令牌accessToken了,然后有用token去请求资源resource了;只是浏览器控制台网络请求里面看不到这两个请求,是因为用OAuthClient封装的请求无法在浏览器显示吗
30 楼 alan_ou 2017-12-21  
OAuth2AuthenticationFilter#onLoginSuccess中调用了issueSuccessRedirect(request, response);这个方法不一定使用successUrl:登录成功后如果之前有保存的请求,则重定向到之前的那个请求,否则转到默认的成功页面;这边登录之前访问的是/oauth2-login,so,会拦截两次。第二次请求oauth2-login时,code为空,所以报错OAuthProblemException{error='invalid_request', description='Missing parameters: code', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}},解决方法:在onLoginSuccess调用issueSuccessRedirect(request, response)前加上WebUtils.getAndClearSavedRequest(request)把之前重定向的那个登录url清除掉,直接进成功页面,是正确的流程。
29 楼 yukun_sun 2017-11-08  
貌似完整的 流程 没走完,拿到 授权code后,还需要重定向到授权服务器 去获取授权令牌吧,会用上client_secret,qq的 貌似 就是这样的
28 楼 lf008 2017-04-14  
兵古1992 写道
zqb666kkk 写道
zqb666kkk 写道
xiaozhi7616 写道
worldfather168 写道
worldfather168 写道
worldfather168 写道
michnus 写道
例子好像有bug,为何server登录成功后跳转到client,client的OAuth2AuthenticationFilter 执行了两次的拦截,第二次请求oauth2-login时,code为空

OAuth2AuthenticationFilter#onLoginSuccess中调用了issueSuccessRedirect(request, response);这个方法不一定使用successUrl:登录成功后如果之前有保存的请求,则重定向到之前的那个请求,否则转到默认的成功页面;这边登录之前访问的是/oauth2-login,so,会拦截两次。

onAccessDenied的时候saveRequestAndRedirectToLogin,onLoginSuccess的时候issueSuccessRedirect,有SavedRequest

这是把服务端客户端在一个web环境才会这样,好像。


Server和client分别部署在两个Tomcat下也同样会报错,OAuthProblemException{error='invalid_request', description='Missing parameters: code', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}

我也遇到 OAuthProblemException{error='invalid_request', description='Missing parameters: code', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}这个错误了 请问怎么解决啊 博主!

我调试发现 extractUsername这个方法进了两次 第一次的时候 .setCode(code)里的code不为null 第二次进来的时候 code 就为null了 这是怎么回事


其实是在第一次登陆的时候保存了名为 "shiroSavedRequest"的session,该会话的successUrl是oauth2-login,所以在第一次调用onLoginSuccess的时候,会继续访问oauth2-login。解决方法是在第一次登陆之后,加上WebUtils.getAndClearSavedRequest(request);清楚shiroSavedRequest会话。就ok了

兄弟你部起来可以用吗,我搭了博主提供的demo,跳转都不正常,你能提供个可用的的吗
27 楼 w846492130_1 2017-04-14  
弱弱的问一句,我如果把客户端的Oauth2Realm中的extractUsername里面的http请求改成RMI,是不是也能实现相同的效果啊
26 楼 兵古1992 2017-04-04  
zqb666kkk 写道
zqb666kkk 写道
xiaozhi7616 写道
worldfather168 写道
worldfather168 写道
worldfather168 写道
michnus 写道
例子好像有bug,为何server登录成功后跳转到client,client的OAuth2AuthenticationFilter 执行了两次的拦截,第二次请求oauth2-login时,code为空

OAuth2AuthenticationFilter#onLoginSuccess中调用了issueSuccessRedirect(request, response);这个方法不一定使用successUrl:登录成功后如果之前有保存的请求,则重定向到之前的那个请求,否则转到默认的成功页面;这边登录之前访问的是/oauth2-login,so,会拦截两次。

onAccessDenied的时候saveRequestAndRedirectToLogin,onLoginSuccess的时候issueSuccessRedirect,有SavedRequest

这是把服务端客户端在一个web环境才会这样,好像。


Server和client分别部署在两个Tomcat下也同样会报错,OAuthProblemException{error='invalid_request', description='Missing parameters: code', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}

我也遇到 OAuthProblemException{error='invalid_request', description='Missing parameters: code', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}这个错误了 请问怎么解决啊 博主!

我调试发现 extractUsername这个方法进了两次 第一次的时候 .setCode(code)里的code不为null 第二次进来的时候 code 就为null了 这是怎么回事


其实是在第一次登陆的时候保存了名为 "shiroSavedRequest"的session,该会话的successUrl是oauth2-login,所以在第一次调用onLoginSuccess的时候,会继续访问oauth2-login。解决方法是在第一次登陆之后,加上WebUtils.getAndClearSavedRequest(request);清楚shiroSavedRequest会话。就ok了
25 楼 dirkkovacevic 2016-05-09  
请问这个框架支持https吗
24 楼 pxhssg 2016-04-07  
pxhssg 写道
zqb666kkk 写道
OAuthProblemException{error='unsupported_response_type', description='Invalid response! Response body is not application/json encoded', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}

请问这个错误怎么解决




请问你解决了吗



我搞不定,不知道什么问题
23 楼 pxhssg 2016-04-07  
zqb666kkk 写道
OAuthProblemException{error='unsupported_response_type', description='Invalid response! Response body is not application/json encoded', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}

请问这个错误怎么解决




请问你解决了吗
22 楼 zcc_heu 2015-11-29  
谁的可以运行了,求发给我一个eclipse工程项目。邮箱1215271893@qq.com
21 楼 sljackson 2015-11-17  
引用
level one
引用
level two
20 楼 sljackson 2015-11-17  
zqb666kkk 写道
zqb666kkk 写道
xiaozhi7616 写道
worldfather168 写道
worldfather168 写道
worldfather168 写道
michnus 写道
例子好像有bug,为何server登录成功后跳转到client,client的OAuth2AuthenticationFilter 执行了两次的拦截,第二次请求oauth2-login时,code为空

OAuth2AuthenticationFilter#onLoginSuccess中调用了issueSuccessRedirect(request, response);这个方法不一定使用successUrl:登录成功后如果之前有保存的请求,则重定向到之前的那个请求,否则转到默认的成功页面;这边登录之前访问的是/oauth2-login,so,会拦截两次。

onAccessDenied的时候saveRequestAndRedirectToLogin,onLoginSuccess的时候issueSuccessRedirect,有SavedRequest

这是把服务端客户端在一个web环境才会这样,好像。


Server和client分别部署在两个Tomcat下也同样会报错,OAuthProblemException{error='invalid_request', description='Missing parameters: code', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}

我也遇到 OAuthProblemException{error='invalid_request', description='Missing parameters: code', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}这个错误了 请问怎么解决啊 博主!

我调试发现 extractUsername这个方法进了两次 第一次的时候 .setCode(code)里的code不为null 第二次进来的时候 code 就为null了 这是怎么回事

是bug
19 楼 zqb666kkk 2015-09-20  
OAuthProblemException{error='unsupported_response_type', description='Invalid response! Response body is not application/json encoded', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}

请问这个错误怎么解决
18 楼 zqb666kkk 2015-09-18  
zqb666kkk 写道
zqb666kkk 写道
xiaozhi7616 写道
worldfather168 写道
worldfather168 写道
worldfather168 写道
michnus 写道
例子好像有bug,为何server登录成功后跳转到client,client的OAuth2AuthenticationFilter 执行了两次的拦截,第二次请求oauth2-login时,code为空

OAuth2AuthenticationFilter#onLoginSuccess中调用了issueSuccessRedirect(request, response);这个方法不一定使用successUrl:登录成功后如果之前有保存的请求,则重定向到之前的那个请求,否则转到默认的成功页面;这边登录之前访问的是/oauth2-login,so,会拦截两次。

onAccessDenied的时候saveRequestAndRedirectToLogin,onLoginSuccess的时候issueSuccessRedirect,有SavedRequest

这是把服务端客户端在一个web环境才会这样,好像。


Server和client分别部署在两个Tomcat下也同样会报错,OAuthProblemException{error='invalid_request', description='Missing parameters: code', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}

我也遇到 OAuthProblemException{error='invalid_request', description='Missing parameters: code', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}这个错误了 请问怎么解决啊 博主!

我调试发现 extractUsername这个方法进了两次 第一次的时候 .setCode(code)里的code不为null 第二次进来的时候 code 就为null了 这是怎么回事

我自己前面加了 
 Subject currentUser = SecurityUtils.getSubject();  
        Session session = currentUser.getSession();
        if(code!=null){
            session.setAttribute("code", code);
        }else{
        	code=(String) session.getAttribute("code");
        }
        
        String username = extractUsername(code);


这个解决这个错误了 开涛兄 留了这个坑也不说明下
17 楼 zqb666kkk 2015-09-18  
zqb666kkk 写道
xiaozhi7616 写道
worldfather168 写道
worldfather168 写道
worldfather168 写道
michnus 写道
例子好像有bug,为何server登录成功后跳转到client,client的OAuth2AuthenticationFilter 执行了两次的拦截,第二次请求oauth2-login时,code为空

OAuth2AuthenticationFilter#onLoginSuccess中调用了issueSuccessRedirect(request, response);这个方法不一定使用successUrl:登录成功后如果之前有保存的请求,则重定向到之前的那个请求,否则转到默认的成功页面;这边登录之前访问的是/oauth2-login,so,会拦截两次。

onAccessDenied的时候saveRequestAndRedirectToLogin,onLoginSuccess的时候issueSuccessRedirect,有SavedRequest

这是把服务端客户端在一个web环境才会这样,好像。


Server和client分别部署在两个Tomcat下也同样会报错,OAuthProblemException{error='invalid_request', description='Missing parameters: code', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}

我也遇到 OAuthProblemException{error='invalid_request', description='Missing parameters: code', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}这个错误了 请问怎么解决啊 博主!

我调试发现 extractUsername这个方法进了两次 第一次的时候 .setCode(code)里的code不为null 第二次进来的时候 code 就为null了 这是怎么回事
16 楼 zqb666kkk 2015-09-18  
xiaozhi7616 写道
worldfather168 写道
worldfather168 写道
worldfather168 写道
michnus 写道
例子好像有bug,为何server登录成功后跳转到client,client的OAuth2AuthenticationFilter 执行了两次的拦截,第二次请求oauth2-login时,code为空

OAuth2AuthenticationFilter#onLoginSuccess中调用了issueSuccessRedirect(request, response);这个方法不一定使用successUrl:登录成功后如果之前有保存的请求,则重定向到之前的那个请求,否则转到默认的成功页面;这边登录之前访问的是/oauth2-login,so,会拦截两次。

onAccessDenied的时候saveRequestAndRedirectToLogin,onLoginSuccess的时候issueSuccessRedirect,有SavedRequest

这是把服务端客户端在一个web环境才会这样,好像。


Server和client分别部署在两个Tomcat下也同样会报错,OAuthProblemException{error='invalid_request', description='Missing parameters: code', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}

我也遇到 OAuthProblemException{error='invalid_request', description='Missing parameters: code', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}这个错误了 请问怎么解决啊 博主!
15 楼 zqb666kkk 2015-09-18  
java.lang.NullPointerException
at com.sys.OAuth2.service.OAuthServiceImpl.addAuthCode(OAuthServiceImpl.java:28)
at com.sys.OAuth2.service.OAuthServiceImpl$$FastClassBySpringCGLIB$$dfb0da7c.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:717)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)

登录的是报空指针  cache为null  请问 开涛怎么解决
14 楼 xiaozhi7616 2015-09-03  
worldfather168 写道
worldfather168 写道
worldfather168 写道
michnus 写道
例子好像有bug,为何server登录成功后跳转到client,client的OAuth2AuthenticationFilter 执行了两次的拦截,第二次请求oauth2-login时,code为空

OAuth2AuthenticationFilter#onLoginSuccess中调用了issueSuccessRedirect(request, response);这个方法不一定使用successUrl:登录成功后如果之前有保存的请求,则重定向到之前的那个请求,否则转到默认的成功页面;这边登录之前访问的是/oauth2-login,so,会拦截两次。

onAccessDenied的时候saveRequestAndRedirectToLogin,onLoginSuccess的时候issueSuccessRedirect,有SavedRequest

这是把服务端客户端在一个web环境才会这样,好像。


Server和client分别部署在两个Tomcat下也同样会报错,OAuthProblemException{error='invalid_request', description='Missing parameters: code', uri='null', state='null', scope='null', redirectUri='null', responseStatus=0, parameters={}}

相关推荐

Global site tag (gtag.js) - Google Analytics