Re:從零開始的Spring Security Oauth2(二)

本文開始從源碼的層面,講解一些Spring Security Oauth2的認證流程。本文較長,適合在空餘時間段觀看。且涉及了較多的源碼,非關鍵性代碼以…代替。html

準備工做

首先開啓debug信息:redis

logging:
  level:
    org.springframework: DEBUG

能夠完整的看到內部的運轉流程。spring

client模式稍微簡單一些,使用client模式獲取token 
http://localhost:8080/oauth/token?client_id=client_1&client_secret=123456&scope=select&grant_type=client_credentials數據庫

因爲debug信息太多了,我簡單按照順序列了一下關鍵的幾個類:安全

ClientCredentialsTokenEndpointFilter
DaoAuthenticationProvider
TokenEndpoint
TokenGranter

@EnableAuthorizationServer

上一篇博客中咱們嘗試使用了password模式和client模式,有一個比較關鍵的endpoint:/oauth/token。從這個入口開始分析,spring security oauth2內部是如何生成token的。獲取token,與第一篇文章中的兩個重要概念之一有關,也就是AuthorizationServer與ResourceServer中的AuthorizationServer。app

在以前的配置中框架

@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {}

出現了AuthorizationServerConfigurerAdapter 關鍵類,他關聯了三個重要的配置類,分別是ide

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security <1>) throws Exception {
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients <2>) throws Exception {
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints <3>) throws Exception {
    }

}

<1> 配置AuthorizationServer安全認證的相關信息,建立ClientCredentialsTokenEndpointFilter核心過濾器post

<2> 配置OAuth2的客戶端相關信息this

<3> 配置AuthorizationServerEndpointsConfigurer衆多相關類,包括配置身份認證器,配置認證方式,TokenStore,TokenGranter,OAuth2RequestFactory

咱們逐步分析其中關鍵的類

客戶端身份認證核心過濾器ClientCredentialsTokenEndpointFilter(掌握)

截取關鍵的代碼,能夠分析出大概的流程 
在請求到達/oauth/token以前通過了ClientCredentialsTokenEndpointFilter這個過濾器,關鍵方法以下

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
        throws AuthenticationException, IOException, ServletException {
    ...
    String clientId = request.getParameter("client_id");
    String clientSecret = request.getParameter("client_secret");

    ...
    clientId = clientId.trim();
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
            clientSecret);

    return this.getAuthenticationManager().authenticate(authRequest);

}

頂級身份管理者AuthenticationManager(掌握)

用來從請求中獲取client_id,client_secret,組裝成一個UsernamePasswordAuthenticationToken做爲身份標識,使用容器中的頂級身份管理器AuthenticationManager去進行身份認證(AuthenticationManager的實現類通常是ProviderManager。而ProviderManager內部維護了一個List,真正的身份認證是由一系列AuthenticationProvider去完成。而AuthenticationProvider的經常使用實現類則是DaoAuthenticationProvider,DaoAuthenticationProvider內部又聚合了一個UserDetailsService接口,UserDetailsService纔是獲取用戶詳細信息的最終接口,而咱們上一篇文章中在內存中配置用戶,就是使用了UserDetailsService的一個實現類InMemoryUserDetailsManager)。UML類圖能夠大概理解下這些類的關係,省略了受權部分。 
認證相關 

圖1 認證相關UML類圖


可能機智的讀者會發現一個問題,我前面一篇文章已經提到了client模式是不存在「用戶」的概念的,那麼這裏的身份認證是在認證什麼呢?debug能夠發現UserDetailsService的實現被適配成了ClientDetailsUserDetailsService,這個設計是將client客戶端的信息(client_id,client_secret)適配成用戶的信息(username,password),這樣咱們的認證流程就不須要修改了。

 

通過ClientCredentialsTokenEndpointFilter以後,身份信息已經獲得了AuthenticationManager的驗證。接着便到達了 
TokenEndpoint。

Token處理端點TokenEndpoint(掌握)

前面的兩個ClientCredentialsTokenEndpointFilter和AuthenticationManager能夠理解爲一些前置校驗,和身份封裝,而這個類一看名字就知道和咱們的token是密切相關的。

@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {

    @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
    Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
         ...
        String clientId = getClientId(principal);
        ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);//<1>
        ...
        TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);//<2>
        ...
        OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);//<3>
        ...
        return getResponse(token);

    }

    private TokenGranter tokenGranter;
}

<1> 加載客戶端信息

<2> 結合請求信息,建立TokenRequest

<3> 將TokenRequest傳遞給TokenGranter頒發token

省略了一些校驗代碼以後,真正的/oauth/token端點暴露在了咱們眼前,其中方法參數中的Principal通過以前的過濾器,已經被填充了相關的信息,而方法的內部則是依賴了一個TokenGranter 來頒發token。其中OAuth2AccessToken的實現類DefaultOAuth2AccessToken就是最終在控制檯獲得的token序列化以前的原始類:

public class DefaultOAuth2AccessToken implements Serializable, OAuth2AccessToken {

    private static final long serialVersionUID = 914967629530462926L;

    private String value;

    private Date expiration;

    private String tokenType = BEARER_TYPE.toLowerCase();

    private OAuth2RefreshToken refreshToken;

    private Set<String> scope;

    private Map<String, Object> additionalInformation = Collections.emptyMap();

    //getter,setter
}


@org.codehaus.jackson.map.annotate.JsonSerialize(using = OAuth2AccessTokenJackson1Serializer.class)
@org.codehaus.jackson.map.annotate.JsonDeserialize(using = OAuth2AccessTokenJackson1Deserializer.class)
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = OAuth2AccessTokenJackson2Serializer.class)
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = OAuth2AccessTokenJackson2Deserializer.class)

public interface OAuth2AccessToken {

    public static String BEARER_TYPE = "Bearer";

    public static String OAUTH2_TYPE = "OAuth2";

    /**
     * The access token issued by the authorization server. This value is REQUIRED.
     */
    public static String ACCESS_TOKEN = "access_token";

    /**
     * The type of the token issued as described in <a
     * href="http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-7.1">Section 7.1</a>. Value is case insensitive.
     * This value is REQUIRED.
     */
    public static String TOKEN_TYPE = "token_type";

    /**
     * The lifetime in seconds of the access token. For example, the value "3600" denotes that the access token will
     * expire in one hour from the time the response was generated. This value is OPTIONAL.
     */
    public static String EXPIRES_IN = "expires_in";

    /**
     * The refresh token which can be used to obtain new access tokens using the same authorization grant as described
     * in <a href="http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-6">Section 6</a>. This value is OPTIONAL.
     */
    public static String REFRESH_TOKEN = "refresh_token";

    /**
     * The scope of the access token as described by <a
     * href="http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-3.3">Section 3.3</a>
     */
    public static String SCOPE = "scope";

    ...
}

一個典型的樣例token響應,以下所示,就是上述類序列化後的結果:

{ 
    "access_token":"950a7cc9-5a8a-42c9-a693-40e817b1a4b0", 
    "token_type":"bearer", 
    "refresh_token":"773a0fcd-6023-45f8-8848-e141296cb3cb", 
    "expires_in":27036, 
    "scope":"select" 
}

TokenGranter(掌握)

先從UML類圖對TokenGranter接口的設計有一個宏觀的認識

這裏寫圖片描述 

圖2 TokenGranter相關UML類圖


TokenGranter的設計思路是使用CompositeTokenGranter管理一個List列表,每一種grantType對應一個具體的真正受權者,在debug過程當中能夠發現CompositeTokenGranter 內部就是在循環調用五種TokenGranter實現類的grant方法,而granter內部則是經過grantType來區分是不是各自的受權類型。

 

public class CompositeTokenGranter implements TokenGranter {

    private final List<TokenGranter> tokenGranters;

    public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
        this.tokenGranters = new ArrayList<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;
    }
}

五種類型分別是:

  • ResourceOwnerPasswordTokenGranter ==> password密碼模式
  • AuthorizationCodeTokenGranter ==> authorization_code受權碼模式
  • ClientCredentialsTokenGranter ==> client_credentials客戶端模式
  • ImplicitTokenGranter ==> implicit簡化模式
  • RefreshTokenGranter ==>refresh_token 刷新token專用

以客戶端模式爲例,思考如何產生token的,則須要繼續研究5種受權者的抽象類:AbstractTokenGranter

public abstract class AbstractTokenGranter implements TokenGranter {

    protected final Log logger = LogFactory.getLog(getClass());

    //與token相關的service,重點
    private final AuthorizationServerTokenServices tokenServices;
    //與clientDetails相關的service,重點
    private final ClientDetailsService clientDetailsService;
    //建立oauth2Request的工廠,重點
    private final OAuth2RequestFactory requestFactory;

    private final String grantType;
    ...

    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

        ...
        String clientId = tokenRequest.getClientId();
        ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
        validateGrantType(grantType, client);

        logger.debug("Getting access token for: " + clientId);

        return getAccessToken(client, tokenRequest);

    }

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

    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, null);
    }

    ...
}

回過頭去看TokenEndpoint中,正是調用了這裏的三個重要的類變量的相關方法。因爲篇幅限制,不能延展太多,否則沒完沒了,因此重點分析下AuthorizationServerTokenServices是何方神聖。

AuthorizationServerTokenServices(瞭解)

AuthorizationServer端的token操做service,接口設計以下:

public interface AuthorizationServerTokenServices {
    //建立token
    OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
    //刷新token
    OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
            throws AuthenticationException;
    //獲取token
    OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

}

在默認的實現類DefaultTokenServices中,能夠看到token是如何產生的,而且瞭解了框架對token進行哪些信息的關聯。

@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {

    OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
    OAuth2RefreshToken refreshToken = null;
    if (existingAccessToken != null) {
        if (existingAccessToken.isExpired()) {
            if (existingAccessToken.getRefreshToken() != null) {
                refreshToken = existingAccessToken.getRefreshToken();
                // The token store could remove the refresh token when the
                // access token is removed, but we want to
                // be sure...
                tokenStore.removeRefreshToken(refreshToken);
            }
            tokenStore.removeAccessToken(existingAccessToken);
        }
        else {
            // Re-store the access token in case the authentication has changed
            tokenStore.storeAccessToken(existingAccessToken, authentication);
            return existingAccessToken;
        }
    }

    // Only create a new refresh token if there wasn't an existing one
    // associated with an expired access token.
    // Clients might be holding existing refresh tokens, so we re-use it in
    // the case that the old access token
    // expired.
    if (refreshToken == null) {
        refreshToken = createRefreshToken(authentication);
    }
    // But the refresh token itself might need to be re-issued if it has
    // expired.
    else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
        ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
        if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
            refreshToken = createRefreshToken(authentication);
        }
    }

    OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
    tokenStore.storeAccessToken(accessToken, authentication);
    // In case it was modified
    refreshToken = accessToken.getRefreshToken();
    if (refreshToken != null) {
        tokenStore.storeRefreshToken(refreshToken, authentication);
    }
    return accessToken;

}

簡單總結一下AuthorizationServerTokenServices的做用,他提供了建立token,刷新token,獲取token的實現。在建立token時,他會調用tokenStore對產生的token和相關信息存儲到對應的實現類中,能夠是redis,數據庫,內存,jwt。

總結

本篇總結了使用客戶端模式獲取Token時,spring security oauth2內部的運做流程,重點是在分析AuthenticationServer相關的類。其餘模式有必定的不一樣,但抽象功能是固定的,只是具體的實現類會被相應地替換。閱讀spring的源碼,會發現它的設計中出現了很是多的抽象接口,這對咱們理清楚內部工做流程產生了不小的困擾,個人方式是能夠藉助UML類圖,先從宏觀理清楚做者的設計思路,這會讓咱們的分析事半功倍。

下一篇文章重點分析用戶攜帶token訪問受限資源時,spring security oauth2內部的工做流程。即ResourceServer相關的類。

原文:https://blog.csdn.net/u013815546/article/details/76977239

相關文章
相關標籤/搜索