Spring Security 解析(七) —— Spring Security Oauth2 源码解析

Spring Security 解析(七) —— Spring Security Oauth2 源码解析

  在学习Spring Cloud 时,遇到了受权服务oauth 相关内容时,老是只知其一;不知其二,所以决定先把Spring Security 、Spring Security Oauth2 等权限、认证相关的内容、原理及设计学习并整理一遍。本系列文章就是在学习的过程当中增强印象和理解所撰写的,若有侵权请告知。

项目环境:html

  • JDK1.8
  • Spring boot 2.x
  • Spring Security 5.x

  在解析Spring Security Oauth2 源码前,咱们先看下 Spring Security Oauth2 官方文档 ,其中有这么一段描述:git

The provider role in OAuth 2.0 is actually split between Authorization Service and Resource Service, and while these sometimes reside in the same application, with Spring Security OAuth you have the option to split them across two applications, and also to have multiple Resource Services that share an Authorization Service. The requests for the tokens are handled by Spring MVC controller endpoints, and access to protected resources is handled by standard Spring Security request filters. The following endpoints are required in the Spring Security filter chain in order to implement OAuth 2.0 Authorization Server:github

  • AuthorizationEndpoint is used to service requests for authorization. Default URL: /oauth/authorize.
  • TokenEndpoint is used to service requests for access tokens. Default URL: /oauth/token.

The following filter is required to implement an OAuth 2.0 Resource Server:spring

  • The OAuth2AuthenticationProcessingFilter is used to load the Authentication for the request given an authenticated access token.

  翻译后:express

  实现OAuth 2.0受权服务器,Spring Security过滤器链中须要如下端点:json

  • AuthorizationEndpoint 用于服务于受权请求。预设地址:/oauth/authorize。
  • TokenEndpoint 用于服务访问令牌的请求。预设地址:/oauth/token。

  实现OAuth 2.0资源服务器,须要如下过滤器:segmentfault

  • OAuth2AuthenticationProcessingFilter 用于加载给定的认证访问令牌请求的认证。

  按照官方提示,咱们开始源码解析。(我的建议: 在看源码前最好先去看下官方文档,可以减小没必要要的时间)数组

1、 @EnableAuthorizationServer 解析

  咱们都知道 一个受权认证服务器最最核心的就是 @EnableAuthorizationServer , 那么 @EnableAuthorizationServer 主要作了什么呢? 咱们看下 @EnableAuthorizationServer 源码:服务器

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class})
public @interface EnableAuthorizationServer {

}

   咱们能够看到其源码内部导入了 AuthorizationServerEndpointsConfigurationAuthorizationServerSecurityConfiguration 这2个配置类。 接下来咱们分别看下这2个配置类具体作了什么。session

(一)、 AuthorizationServerEndpointsConfiguration

   从这个配置类的名称咱们不难想象其内部确定存在官方文档中介绍的 AuthorizationEndpointTokenEndpoint ,那么咱们经过源码来印证下吧:

@Configuration
@Import(TokenKeyEndpointRegistrar.class)
public class AuthorizationServerEndpointsConfiguration {

  // 省略 其余相关配置代码
  ....
  
  // 一、 AuthorizationEndpoint 建立
    @Bean
    public AuthorizationEndpoint authorizationEndpoint() throws Exception {
        AuthorizationEndpoint authorizationEndpoint = new AuthorizationEndpoint();
        FrameworkEndpointHandlerMapping mapping = getEndpointsConfigurer().getFrameworkEndpointHandlerMapping();
        authorizationEndpoint.setUserApprovalPage(extractPath(mapping, "/oauth/confirm_access"));
        authorizationEndpoint.setProviderExceptionHandler(exceptionTranslator());
        authorizationEndpoint.setErrorPage(extractPath(mapping, "/oauth/error"));
        authorizationEndpoint.setTokenGranter(tokenGranter());
        authorizationEndpoint.setClientDetailsService(clientDetailsService);
        authorizationEndpoint.setAuthorizationCodeServices(authorizationCodeServices());
        authorizationEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
        authorizationEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
        authorizationEndpoint.setUserApprovalHandler(userApprovalHandler());
        authorizationEndpoint.setRedirectResolver(redirectResolver());
        return authorizationEndpoint;
    }

  // 二、 TokenEndpoint 建立
    @Bean
    public TokenEndpoint tokenEndpoint() throws Exception {
        TokenEndpoint tokenEndpoint = new TokenEndpoint();
        tokenEndpoint.setClientDetailsService(clientDetailsService);
        tokenEndpoint.setProviderExceptionHandler(exceptionTranslator());
        tokenEndpoint.setTokenGranter(tokenGranter());
        tokenEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
        tokenEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
        tokenEndpoint.setAllowedRequestMethods(allowedTokenEndpointRequestMethods());
        return tokenEndpoint;
    }
    
    // 省略 其余相关配置代码
    ....

   经过源码咱们能够很明确的知道:

  • AuthorizationEndpoint 用于服务于受权请求。预设地址:/oauth/authorize。
  • TokenEndpoint 用于服务访问令牌的请求。预设地址:/oauth/token。

   这里就不先解析 AuthorizationEndpoint 和 TokenEndpoint 源码了,在下面我会专门解析的。

(二)、 AuthorizationServerSecurityConfiguration

   AuthorizationServerSecurityConfiguration 因为配置相对复杂,这里就再也不贴源码了介绍了。但其中最主要的配置 ClientDetailsServiceClientDetailsUserDetailsService 以及 ClientCredentialsTokenEndpointFilter 仍是得讲一讲。
   这里介绍下 ClientDetailsUserDetailsService 、UserDetailsService、ClientDetailsService 3者之间的关系:

  • ClientDetailsService : 内部仅有 loadClientByClientId 方法。从方法名咱们就可知其是经过 clientId 来获取 Client 信息, 官方提供 JdbcClientDetailsService、InMemoryClientDetailsService 2个实现类,咱们也能够像UserDetailsService 同样编写本身的实现类。
  • UserDetailsService : 内部仅有 loadUserByUsername 方法。这个类不用我再介绍了吧。不清楚得同窗能够看下我以前得文章。
  • ClientDetailsUserDetailsService : UserDetailsService子类,内部维护了 ClientDetailsService 。其 loadUserByUsername 方法重写后调用ClientDetailsService.loadClientByClientId()。

  ClientCredentialsTokenEndpointFilter 做用与 UserNamePasswordAuthenticationFilter 相似,经过拦截 /oauth/token 地址,获取到 clientId 和 clientSecret 信息并建立 UsernamePasswordAuthenticationToken 做为 AuthenticationManager.authenticate() 参数 调用认证过程。整个认证过程惟一最大得区别在于 DaoAuthenticationProvider.retrieveUser() 获取认证用户信息时调用的是 ClientDetailsUserDetailsService,根据前面讲述的其内部实际上是调用ClientDetailsService 获取到客户端信息

2、 @EnableResourceServer 解析

  像受权认证服务器同样,资源服务器也有一个最核心的配置 @EnableResourceServer , 那么 @EnableResourceServer 主要作了什么呢? 咱们 同样先看下 @EnableResourceServer 源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ResourceServerConfiguration.class)
public @interface EnableResourceServer {

}

   从源码中咱们能够看到其导入了 ResourceServerConfiguration 配置类,这个配置类最核心的配置是 应用了 ResourceServerSecurityConfigurer ,我这边贴出 ResourceServerSecurityConfigurer 源码 最核心的配置代码以下:

@Override
    public void configure(HttpSecurity http) throws Exception {
     // 一、 建立 OAuth2AuthenticationManager  对象
        AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http);
        // 二、 建立 OAuth2AuthenticationProcessingFilter 过滤器
        resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
        resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
        resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
        if (eventPublisher != null) {
            resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher);
        }
        if (tokenExtractor != null) {
            resourcesServerFilter.setTokenExtractor(tokenExtractor);
        }
        if (authenticationDetailsSource != null) {
            resourcesServerFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
        }
        resourcesServerFilter = postProcess(resourcesServerFilter);
        resourcesServerFilter.setStateless(stateless);

        // @formatter:off
        http
            .authorizeRequests().expressionHandler(expressionHandler)
        .and()
            .addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class) // 三、 将 OAuth2AuthenticationProcessingFilter 过滤器加载到过滤器链上
            .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint);
        // @formatter:on
    }
    
 private AuthenticationManager oauthAuthenticationManager(HttpSecurity http) {
     OAuth2AuthenticationManager oauthAuthenticationManager = new OAuth2AuthenticationManager();
     if (authenticationManager != null) {
         if (authenticationManager instanceof OAuth2AuthenticationManager) {
             oauthAuthenticationManager = (OAuth2AuthenticationManager) authenticationManager;
         }
         else {
             return authenticationManager;
         }
     }
     oauthAuthenticationManager.setResourceId(resourceId);
     oauthAuthenticationManager.setTokenServices(resourceTokenServices(http));
     oauthAuthenticationManager.setClientDetailsService(clientDetails());
     return oauthAuthenticationManager;
 }

   源码中最核心的 就是 官方文档中介绍的 OAuth2AuthenticationProcessingFilter 过滤器, 其配置分3步:

  • 一、 建立 OAuth2AuthenticationProcessingFilter 过滤器 对象
  • 二、 建立 OAuth2AuthenticationManager 对象 对将其做为参数设置到 OAuth2AuthenticationProcessingFilter 中
  • 三、 将 OAuth2AuthenticationProcessingFilter 过滤器添加到过滤器链上

3、 AuthorizationEndpoint 解析

   正如前面介绍同样,AuthorizationEndpoint 自己 最大的功能点就是实现了 /oauth/authorize , 那么咱们此次就来看看它是如何实现的:

@RequestMapping(value = "/oauth/authorize")
 public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
         SessionStatus sessionStatus, Principal principal) {

     //  一、 经过 OAuth2RequestFactory 从 参数中获取信息建立 AuthorizationRequest 受权请求对象
     AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);

     Set<String> responseTypes = authorizationRequest.getResponseTypes();

     if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
         throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
     }

     if (authorizationRequest.getClientId() == null) {
         throw new InvalidClientException("A client id must be provided");
     }

     try {
         // 二、 判断  principal 是否 已受权 : /oauth/authorize 设置为无权限访问 ,因此要判断,若是 判断失败则抛出 InsufficientAuthenticationException (AuthenticationException 子类),其异常会被 ExceptionTranslationFilter 处理 ,最终跳转到 登陆页面,这也是为何咱们第一次去请求获取 受权码时会跳转到登录界面的缘由
         if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
             throw new InsufficientAuthenticationException(
                     "User must be authenticated with Spring Security before authorization can be completed.");
         }

         // 三、 经过 ClientDetailsService.loadClientByClientId() 获取到 ClientDetails 客户端信息
         ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());

         // 四、 获取参数中的回调地址而且与系统配置的回调地址对比
         String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
         String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
         if (!StringUtils.hasText(resolvedRedirect)) {
             throw new RedirectMismatchException(
                     "A redirectUri must be either supplied or preconfigured in the ClientDetails");
         }
         authorizationRequest.setRedirectUri(resolvedRedirect);

         //  五、 验证 scope 
         oauth2RequestValidator.validateScope(authorizationRequest, client);

         //  六、 检测该客户端是否设置自动 受权(即 咱们配置客户端时配置的 autoApprove(true)  )
         authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
                 (Authentication) principal);
         boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
         authorizationRequest.setApproved(approved);

         if (authorizationRequest.isApproved()) {
             if (responseTypes.contains("token")) {
                 return getImplicitGrantResponse(authorizationRequest);
             }
             if (responseTypes.contains("code")) {
                 // 7 调用 getAuthorizationCodeResponse() 方法生成code码并回调到设置的回调地址
                 return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
                         (Authentication) principal));
             }
         }
         model.put(AUTHORIZATION_REQUEST_ATTR_NAME, authorizationRequest);
         model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest));

         return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);

     }
     catch (RuntimeException e) {
         sessionStatus.setComplete();
         throw e;
     }

 }

   咱们来大体解析下这段逻辑:

  • 一、 经过 OAuth2RequestFactory 从 参数中获取信息建立 AuthorizationRequest 受权请求对象
  • 二、 判断 principal 是否 已受权 : /oauth/authorize 设置为无权限访问 ,因此要判断,若是 判断失败则抛出 InsufficientAuthenticationException (AuthenticationException 子类),其异常会被 ExceptionTranslationFilter 处理 ,最终跳转到 登陆页面,这也是为何咱们第一次去请求获取 受权码时会跳转到登录界面的缘由
  • 三、 经过 ClientDetailsService.loadClientByClientId() 获取到 ClientDetails 客户端信息
  • 四、 获取参数中的回调地址而且与系统配置的回调地址(步骤3获取到的client信息)对比
  • 五、 与步骤4同样 验证 scope
  • 六、 检测该客户端是否设置自动 受权(即 咱们配置客户端时配置的 autoApprove(true))
  • 七、 因为咱们设置 autoApprove(true) 则 调用 getAuthorizationCodeResponse() 方法生成code码并回调到设置的回调地址
  • 八、 真实生成Code 的方法时 generateCode(AuthorizationRequest authorizationRequest, Authentication authentication) 方法: 其内部是调用 authorizationCodeServices.createAuthorizationCode()方法生成code的

  生成受权码的整个逻辑实际上是相对简单的,真正复杂的是token的生成逻辑,那么接下来咱们就看看token的生成。

4、 TokenEndpoint 解析

   对于使用oauth2 的用户来讲,最最不可避免的就是token 的获取,话很少说,源码解析贴上:

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
  public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
  Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
 
     // 一、 验证 用户信息 (正常状况下会通过 ClientCredentialsTokenEndpointFilter 过滤器认证后获取到用户信息 )
      if (!(principal instanceof Authentication)) {
          throw new InsufficientAuthenticationException(
                  "There is no client authentication. Try adding an appropriate authentication filter.");
      }
 
     // 二、 经过 ClientDetailsService().loadClientByClientId() 获取系统配置客户端信息
      String clientId = getClientId(principal);
      ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
 
     // 三、 经过客户端信息生成 TokenRequest 对象
      TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
 
     ......
     
     // 四、 调用 TokenGranter.grant()方法生成 OAuth2AccessToken 对象(即token)
      OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
      if (token == null) {
          throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
      }
     // 五、 返回token
      return getResponse(token);
 
  }

   简单归纳下来,整个生成token 的逻辑以下:

  • 一、 验证 用户信息 (正常状况下会通过 ClientCredentialsTokenEndpointFilter 过滤器认证后获取到用户信息 )
  • 二、 经过 ClientDetailsService().loadClientByClientId() 获取系统配置的客户端信息
  • 三、 经过客户端信息生成 TokenRequest 对象
  • 四、 将步骤3获取到的 TokenRequest 做为TokenGranter.grant() 方法参照 生成 OAuth2AccessToken 对象(即token)
  • 五、 返回 token

   其中 步骤 4 是整个token生成的核心,咱们来看下 TokenGranter.grant() 方法源码:

public class CompositeTokenGranter implements TokenGranter {
 
     private final List<TokenGranter> tokenGranters;
 
     public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
         for (TokenGranter granter : tokenGranters) {
             OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
             if (grant!=null) {
                 return grant;
             }
         }
         return null;
     }
     
     .....
 }

   官方默认调用 CompositeTokenGranter 的 grant()方法,从源码中咱们能够看到其聚合了 TokenGranter ,采用遍历的方式一个一个的去尝试,因为Oauth2 有4种模式外加token刷新,因此 官方目前有5个子类。
   Debug 看下 tokenGranters :
/img/remote/1460000020491373?w=800&h=379
  从截图中能够看出分别是: AuthorizationCodeTokenGranter、ClientCredentialsTokenGranter、ImplicitTokenGranter、RefreshTokenGranter、ResourceOwnerPasswordTokenGranter ,固然还有一个他们共同的 父类 AbstractTokenGranter。
其中除了 ClientCredentialsTokenGranter 重写了 AbstractTokenGranter.grant() 方法之外,其余4中都是直接调用 AbstractTokenGranter.grant() 进行处理。 咱们来看下 AbstractTokenGranter.grant() 其方法内部实现:

public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

     // 一、 判断 grantType 是否匹配
     if (!this.grantType.equals(grantType)) {
         return null;
     }
     
     // 二、 获取  ClientDetails 信息 并验证 grantType 
     String clientId = tokenRequest.getClientId();
     ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
     validateGrantType(grantType, client);

     if (logger.isDebugEnabled()) {
         logger.debug("Getting access token for: " + clientId);
     }

     // 三、 调用 getAccessToken() 方法生成token并返回
     return getAccessToken(client, tokenRequest);

 }

   AbstractTokenGranter.grant() 方法内部逻辑分3步:

  • 一、 判断 grantType 是否匹配
  • 二、 获取 ClientDetails 信息 并验证 grantType
  • 三、 调用 getAccessToken() 方法生成token并返回

   到目前 咱们尚未看到token具体生成的逻辑,那么接下来咱们就来揭开这层面纱:

protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
     return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
 }

   这里分2个步骤:

  • 一、 经过 getOAuth2Authentication() 方法(子类重写)获取到 OAuth2Authentication 对象
  • 二、 将步骤1 获取到的 OAuth2Authentication 做为 tokenServices.createAccessToken() 方法入参生成token

   因为受权码模式最为复杂,那么咱们就觉得例,查看 其 getOAuth2Authentication() 源码:

@Override
 protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
     
     // 一、 从TokenRequest 中 获取 code 码 、 回调url
     Map<String, String> parameters = tokenRequest.getRequestParameters();
     String authorizationCode = parameters.get("code");
     String redirectUri = parameters.get(OAuth2Utils.REDIRECT_URI);

     if (authorizationCode == null) {
         throw new InvalidRequestException("An authorization code must be supplied.");
     }
     // 二、 调用 authorizationCodeServices.consumeAuthorizationCode(authorizationCode) 方法经过 Code码 获取 OAuth2Authentication 对象
     OAuth2Authentication storedAuth = authorizationCodeServices.consumeAuthorizationCode(authorizationCode);
     if (storedAuth == null) {
         throw new InvalidGrantException("Invalid authorization code: " + authorizationCode);
     }
     // 三、 从 OAuth2Authentication 对象中获取 OAuth2Request 对象并验证回调url、clientId
     OAuth2Request pendingOAuth2Request = storedAuth.getOAuth2Request();
     String redirectUriApprovalParameter = pendingOAuth2Request.getRequestParameters().get(
             OAuth2Utils.REDIRECT_URI);

     if ((redirectUri != null || redirectUriApprovalParameter != null)
             && !pendingOAuth2Request.getRedirectUri().equals(redirectUri)) {
         throw new RedirectMismatchException("Redirect URI mismatch.");
     }

     String pendingClientId = pendingOAuth2Request.getClientId();
     String clientId = tokenRequest.getClientId();
     if (clientId != null && !clientId.equals(pendingClientId)) {
         throw new InvalidClientException("Client ID mismatch");
     }
     // 四、 建立一个全新的 OAuth2Request,并从OAuth2Authentication 中获取到 Authentication 对象
     Map<String, String> combinedParameters = new HashMap<String, String>(pendingOAuth2Request
             .getRequestParameters());
     combinedParameters.putAll(parameters);
     OAuth2Request finalStoredOAuth2Request = pendingOAuth2Request.createOAuth2Request(combinedParameters);
     
     Authentication userAuth = storedAuth.getUserAuthentication();
     
     // 五、 建立一个全新的 OAuth2Authentication 对象
     return new OAuth2Authentication(finalStoredOAuth2Request, userAuth);

 }

   咱们从源码中能够看到,整个 getOAuth2Authentication 分5个步骤:

  • 一、 从TokenRequest 中 获取 code 码 、 回调url
  • 二、 调用 authorizationCodeServices.consumeAuthorizationCode(authorizationCode) 方法经过 Code码 获取 OAuth2Authentication 对象
  • 三、 从 OAuth2Authentication 对象中获取 OAuth2Request 对象并验证回调url、clientId
  • 四、 建立一个全新的 OAuth2Request,并从OAuth2Authentication 中获取到 Authentication 对象
  • 五、 经过步骤4 的OAuth2Request 和 Authentication 建立一个全新的 OAuth2Authentication 对象

   这里可能有人会问怎么不直接使用本来经过code 获取的 OAuth2Authentication 对象,这里我也不清楚,若是有同窗清楚麻烦告知如下,谢谢!!

OAuth2Authentication 对象生成后会调用 tokenServices.createAccessToken(),咱们来看下 官方默认提供 的 DefaultTokenServices(AuthorizationServerTokenServices 实现类) 的 createAccessToken 方法内部实现源码:

@Transactional
 public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
     // 一、 经过 tokenStore 获取到以前存在的token 并判断是否为空、过时,不为空且未过时则直接返回原有存在的token (因为咱们经常使用Jwt 因此这里是 JwtTokenStore ,且 existingAccessToken 永远为空,即每次请求获取token的值均不一样,这与RedisTokenStore 是有区别的)
     OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
     OAuth2RefreshToken refreshToken = null;
     if (existingAccessToken != null) {
         if (existingAccessToken.isExpired()) {
             if (existingAccessToken.getRefreshToken() != null) {
                 refreshToken = existingAccessToken.getRefreshToken();
                 tokenStore.removeRefreshToken(refreshToken);
             }
             tokenStore.removeAccessToken(existingAccessToken);
         }
         else {
             tokenStore.storeAccessToken(existingAccessToken, authentication);
             return existingAccessToken;
         }
     }
     // 二、 调用 createRefreshToken 方法生成 refreshToken
     if (refreshToken == null) {
         refreshToken = createRefreshToken(authentication);
     }else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
         ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
         if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
             refreshToken = createRefreshToken(authentication);
         }
     }
     
     // 三、 调用  createAccessToken(authentication, refreshToken) 方法获取 token
     OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
     tokenStore.storeAccessToken(accessToken, authentication);
     // 四、 从新覆盖原有的刷新token(原有的 refreshToken 为UUID 数据,覆盖为 jwtToken)
     refreshToken = accessToken.getRefreshToken();
     if (refreshToken != null) {
         tokenStore.storeRefreshToken(refreshToken, authentication);
     }
     return accessToken;

 }

   咱们从源码中能够看到,整个 createAccessToken 分4个步骤:

  • 一、 经过 tokenStore 获取到以前存在的token 并判断是否为空、过时,不为空且未过时则直接返回原有存在的token (因为咱们经常使用Jwt 因此这里是 JwtTokenStore ,且 existingAccessToken 永远为空,即每次请求获取token的值均不一样,这与RedisTokenStore 是有区别的)
  • 二、 调用 createRefreshToken 方法生成 refreshToken
  • 三、 调用 createAccessToken(authentication, refreshToken) 方法获取 token
  • 四、 从新覆盖原有的刷新token(原有的 refreshToken 为UUID 数据,覆盖为 jwtToken)并返回token

   在如今为止咱们尚未看到token的生成代码,不要灰心,立马就能看到了 ,咱们在看下步骤3 其 重载方法 createAccessToken(authentication, refreshToken) 源码:

private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
     // 一、 经过 UUID 建立  DefaultOAuth2AccessToken  并设置上有效时长等信息
     DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
     int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
     if (validitySeconds > 0) {
         token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
     }
     token.setRefreshToken(refreshToken);
     token.setScope(authentication.getOAuth2Request().getScope());
     // 二、 判断 是否存在 token加强器 accessTokenEnhancer ,存在则调用加强器加强方法
     return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
 }

   从源码来看,其实token就是经过UUID生成的,且生成过程很简单,但 若是咱们配置了token加强器 (TokenEnhancer)(对于jwtToken来讲,其毋庸置疑的使用了加强器实现),因此咱们还得看下加强器是如何实现的,不过在讲解加强器的实现时,咱们还得回顾下以前咱们在TokenStoreConfig 配置过如下代码:

/**
      * 自定义token扩展链
      *
      * @return tokenEnhancerChain
      */
     @Bean
     public TokenEnhancerChain tokenEnhancerChain() {
         TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
         tokenEnhancerChain.setTokenEnhancers(Arrays.asList(new JwtTokenEnhance(), jwtAccessTokenConverter()));
         return tokenEnhancerChain;
     }

   这段代码 配置了 tokenEnhancerChain (TokenEnhancer实现类),而且在 tokenEnhancerChain对象中添加了2个 TokenEnhance ,分别是 JwtAccessTokenConverter 以及一个咱们自定义的 加强器 JwtTokenEnhance ,因此看到这里应该可以明白 最终会调用 tokenEnhancerChain ,不用想,tokenEnhancerChain确定会遍历 其内部维护的 TokenEnhanceList进行token加强,查看 tokenEnhancerChain 源码以下:

public class TokenEnhancerChain implements TokenEnhancer {

 private List<TokenEnhancer> delegates = Collections.emptyList();

 /**
  * @param delegates the delegates to set
  */
 public void setTokenEnhancers(List<TokenEnhancer> delegates) {
     this.delegates = delegates;
 }

 /**
  * Loop over the {@link #setTokenEnhancers(List) delegates} passing the result into the next member of the chain.
  * 
  * @see org.springframework.security.oauth2.provider.token.TokenEnhancer#enhance(org.springframework.security.oauth2.common.OAuth2AccessToken,
  * org.springframework.security.oauth2.provider.OAuth2Authentication)
  */
 public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
     OAuth2AccessToken result = accessToken;
     for (TokenEnhancer enhancer : delegates) {
         result = enhancer.enhance(result, authentication);
     }
     return result;
 }

}

   至于其加强器实现代码这里就再也不贴出了。至此,我的以为整个获取token的源码解析基本上完成。若是非得要总结的话 请看下图:

https://image-static.segmentfault.com/393/284/3932847928-5d8b131bd170a_articlex

5、 OAuth2AuthenticationProcessingFilter (资源服务器认证)解析

  经过前面的解析咱们最终获取到了token,但获取token 不是咱们最终目的,咱们最终的目的时拿到资源信息,因此咱们还得经过获取到的token去调用资源服务器接口获取资源数据。那么接下来咱们就来解析资源服务器是如何经过传入token去辨别用户并容许返回资源信息的。咱们知道资源服务器在过滤器链新增了 OAuth2AuthenticationProcessingFilter 来拦截请求并认证,那就这个过滤器的实现:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
         ServletException {

     final boolean debug = logger.isDebugEnabled();
     final HttpServletRequest request = (HttpServletRequest) req;
     final HttpServletResponse response = (HttpServletResponse) res;

     try {
         // 一、 调用 tokenExtractor.extract() 方法从请求中解析出token信息并存放到 authentication 的  principal 字段 中
         Authentication authentication = tokenExtractor.extract(request);
         
         if (authentication == null) {
             if (stateless && isAuthenticated()) {
                 if (debug) {
                     logger.debug("Clearing security context.");
                 }
                 SecurityContextHolder.clearContext();
             }
             if (debug) {
                 logger.debug("No token in request, will continue chain.");
             }
         }
         else {
             request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
             if (authentication instanceof AbstractAuthenticationToken) {
                 AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
                 needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
             }
             // 二、 调用  authenticationManager.authenticate() 认证过程: 注意此时的  authenticationManager 是 OAuth2AuthenticationManager 
             Authentication authResult = authenticationManager.authenticate(authentication);

             if (debug) {
                 logger.debug("Authentication success: " + authResult);
             }

             eventPublisher.publishAuthenticationSuccess(authResult);
             SecurityContextHolder.getContext().setAuthentication(authResult);

         }
     }
     catch (OAuth2Exception failed) {
         SecurityContextHolder.clearContext();
         eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
                 new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
                 
         authenticationEntryPoint.commence(request, response,
                 new InsufficientAuthenticationException(failed.getMessage(), failed));

         return;
     }
     
     chain.doFilter(request, response);
 }

   整个filter步骤最核心的是下面2个:

  • 一、 调用 tokenExtractor.extract() 方法从请求中解析出token信息并存放到 authentication 的 principal 字段 中
  • 二、 调用 authenticationManager.authenticate() 认证过程: 注意此时的 authenticationManager 是 OAuth2AuthenticationManager

   在解析@EnableResourceServer 时咱们讲过 OAuth2AuthenticationManager 与 OAuth2AuthenticationProcessingFilter 的关系,这里再也不重述,咱们直接看下 OAuth2AuthenticationManager 的 authenticate() 方法实现:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

     if (authentication == null) {
         throw new InvalidTokenException("Invalid token (token not found)");
     }
     // 一、 从 authentication 中获取 token
     String token = (String) authentication.getPrincipal();
     // 二、 调用 tokenServices.loadAuthentication() 方法  经过 token 参数获取到 OAuth2Authentication 对象 ,这里的tokenServices 就是咱们资源服务器配置的。
     OAuth2Authentication auth = tokenServices.loadAuthentication(token);
     if (auth == null) {
         throw new InvalidTokenException("Invalid token: " + token);
     }

     Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
     if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
         throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
     }
     // 三、 检测客户端信息,因为咱们采用受权服务器和资源服务器分离的设计,因此这个检测方法实际没有检测
     checkClientDetails(auth);

     if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
         OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
         // Guard against a cached copy of the same details
         if (!details.equals(auth.getDetails())) {
             // Preserve the authentication details from the one loaded by token services
             details.setDecodedDetails(auth.getDetails());
         }
     }
     // 四、 设置认证成功标识并返回
     auth.setDetails(authentication.getDetails());
     auth.setAuthenticated(true);
     return auth;

 }

   整个 认证逻辑分4步:

  • 一、 从 authentication 中获取 token
  • 二、 调用 tokenServices.loadAuthentication() 方法 经过 token 参数获取到 OAuth2Authentication 对象 ,这里的tokenServices 就是咱们资源服务器配置的。
  • 三、 检测客户端信息,因为咱们采用受权服务器和资源服务器分离的设计,因此这个检测方法实际没有检测
  • 四、 设置认证成功标识并返回 ,注意返回的是 OAuth2Authentication (Authentication 子类)。

   后面的受权过程就是原汁原味的Security受权,因此至此整个资源服务器 经过获取到的token去调用接口获取资源数据 的解析完成。

6、 重写登录,实现登陆接口直接返回jwtToken

   前面,咱们花了大量时间讲解,那么确定得实践实践一把。 相信你们平时的登陆接口都是直接返回token的,可是因为Security 最本来的设计缘由,登录后都是跳转回到以前求情的接口,这种方式仅仅适用于PC端,那若是是APP呢?因此咱们想要在原有的登录接口上实现当非PC请求时返回token的功能。还记得以前提到过的 AuthenticationSuccessHandler 认证成功处理器,咱们的功能实现就在这里面。

   咱们从新回顾下 /oauth/authorize 实现 token,模仿实现后的代码以下:

@Component("customAuthenticationSuccessHandler")
@Slf4j
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

 @Resource
 private SecurityProperties securityProperties;

 @Resource
 private ObjectMapper objectMapper;

 @Resource
 private PasswordEncoder passwordEncoder;

 private ClientDetailsService clientDetailsService = null;

 private AuthorizationServerTokenServices authorizationServerTokenServices = null;

 private RequestCache requestCache = new HttpSessionRequestCache();

 @Override
 public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                     Authentication authentication) throws IOException, ServletException {
     logger.info("登陆成功");
     // 重构后使得成功处理器可以根据不一样的请求来区别是返回token仍是调用原来的逻辑(好比受权模式就须要跳转)
     // 获取请求头中的Authorization

     String header = request.getHeader("Authorization");
     // 是否以Basic开头
     if (header == null || !header.startsWith("Basic ")) {
         // 为了受权码模式 登录正常跳转,这里就再也不跳转到自定义的登录成功页面了
//            // 若是设置了loginSuccessUrl,老是跳到设置的地址上
//            // 若是没设置,则尝试跳转到登陆以前访问的地址上,若是登陆前访问地址为空,则跳到网站根路径上
//            if (!StringUtils.isEmpty(securityProperties.getLogin().getLoginSuccessUrl())) {
//                requestCache.removeRequest(request, response);
//                setAlwaysUseDefaultTargetUrl(true);
//                setDefaultTargetUrl(securityProperties.getLogin().getLoginSuccessUrl());
//            }
         super.onAuthenticationSuccess(request, response, authentication);
     } else {

         // 这里为何要经过 SpringContextUtil 获取bean,
         // 主要缘由是若是直接在 依赖注入 会致使 AuthorizationServerConfiguration 和 SpringSecurityConfig 配置加载顺序混乱
         // 最直接的表如今 AuthorizationServerConfiguration 中 authenticationManager 获取到 为null,由于这个时候 SpringSecurityConfig 还没加载建立
         // 这里采用这种方式会有必定的性能问题,但也是无赖之举  有兴趣的同窗能够看下: https://blog.csdn.net/qq_36732557/article/details/80338570 和 https://blog.csdn.net/forezp/article/details/84313907
         if (clientDetailsService == null && authorizationServerTokenServices == null) {
             clientDetailsService = SpringContextUtil.getBean(ClientDetailsService.class);
             authorizationServerTokenServices = SpringContextUtil.getBean(AuthorizationServerTokenServices.class);
         }

         String[] tokens = extractAndDecodeHeader(header, request);
         assert tokens.length == 2;

         String clientId = tokens[0];

         String clientSecret = tokens[1];

         ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);

         if (clientDetails == null) {
             throw new UnapprovedClientAuthenticationException("clientId对应的配置信息不存在:" + clientId);
         } else if (!passwordEncoder.matches(clientSecret, clientDetails.getClientSecret())) {
             throw new UnapprovedClientAuthenticationException("clientSecret不匹配:" + clientId);
         }

         TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP,
                 clientId,
                 clientDetails.getScope(),
                 "custom");

         OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);

         OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request,
                 authentication);

         OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);

         response.setContentType("application/json;charset=UTF-8");
         response.getWriter().write(objectMapper.writeValueAsString(token));
     }

 }

 /**
  * 解析请求头拿到clientid  client secret的数组
  *
  * @param header
  * @param request
  * @return
  * @throws IOException
  */
 private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException {

     byte[] base64Token = header.substring(6).getBytes("UTF-8");
     byte[] decoded;
     try {
         decoded = Base64.decode(base64Token);
     } catch (IllegalArgumentException e) {
         throw new BadCredentialsException("Failed to decode basic authentication token");
     }

     String token = new String(decoded, "UTF-8");

     int delim = token.indexOf(":");

     if (delim == -1) {
         throw new BadCredentialsException("Invalid basic authentication token");
     }
     return new String[]{token.substring(0, delim), token.substring(delim + 1)};
 }

}

   回顾下建立token 须要的 几个必要类: clientDetailsService 、 authorizationServerTokenServices、 ClientDetails 、 TokenRequest 、OAuth2Request、 authentication、OAuth2Authentication 。 了解这几个类之间的关系颇有必要。对于clientDetailsService 、 authorizationServerTokenServices 咱们能够直接从Spring 容器中获取,ClientDetails 咱们能够从请求参数中获取,有了 ClientDetails 就有了 TokenRequest,有了 TokenRequest 和 authentication(认证后确定有的) 就有了 OAuth2Authentication ,有了OAuth2Authentication 就可以生成 OAuth2AccessToken。
至此,咱们经过直接请求登录接口(注意在请求头中添加ClientDetails信息)就能够实现获取到token了,那么有同窗会问,若是我是手机登录方式呢?其实无论你什么登录方式,只要你设置的登录成功处理器是上面那个就可支持,下图是我测试的手机登录获取token截图:

https://image-static.segmentfault.com/607/804/607804913-5d8b131ed894d_articlex

curl:

curl -X POST \
   'http://localhost/loginByMobile?mobile=15680659123&smsCode=215672' \
   -H 'Accept: */*' \
   -H 'Accept-Encoding: gzip, deflate' \
   -H 'Authorization: Basic Y2xpZW50MToxMjM0NTY=' \
   -H 'Cache-Control: no-cache' \
   -H 'Connection: keep-alive' \
   -H 'Content-Length: 0' \
   -H 'Content-Type: application/json' \
   -H 'Host: localhost' \
   -H 'Postman-Token: 412722f9-b303-4d5d-b4a4-72b1dcb47f44,572f537f-c2f7-4c9c-a0e9-5e0eb07a3ec5' \
   -H 'User-Agent: PostmanRuntime/7.17.1' \
   -H 'cache-control: no-cache'

   注意: 请求头中添加ClientDetails信息

7、 我的总结

   我的以为官方的这段描述是最好的总结:

实现OAuth 2.0受权服务器,Spring Security过滤器链中须要如下端点:

  • AuthorizationEndpoint 用于服务于受权请求。预设地址:/oauth/authorize。
  • TokenEndpoint 用于服务访问令牌的请求。预设地址:/oauth/token。

      实现OAuth 2.0资源服务器,须要如下过滤器:

  • OAuth2AuthenticationProcessingFilter 用于加载给定的认证访问令牌请求的认证。

   源码解析的话,只要理解了下图中全部涉及到的类的做用即出发场景就基本上算是明白了:

https://image-static.segmentfault.com/393/284/3932847928-5d8b131bd170a_articlex

   本文介绍 Spring Security Oauth2 源码解析 能够访问代码仓库中的 security 模块 ,项目的github 地址 : https://github.com/BUG9/sprin...

         若是您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!

相关文章
相关标签/搜索