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、收藏、轉發給予支持!

相關文章
相關標籤/搜索