## 獲取token
上一篇博客中我們嘗試使用了password模式和client模式,有一個比較關鍵的endpoint:/oauth/token。從這個入口開始分析,spring security oauth2內部是如何生成token的。
首先開啟debug信息:
~~~
logging:
level:
org.springframework: DEBUG
~~~
可以完整的看到內部的運轉流程。
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
~~~
## ClientCredentialsTokenEndpointFilter和DaoAuthenticationProvider
截取關鍵的代碼,可以分析出大概的流程
在請求到達/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);
}
~~~
用來從請求中獲取client_id,client_secret,組裝成一個UsernamePasswordAuthenticationToken作為身份標識,使用容器中的頂級身份管理器AuthenticationManager去進行身份認證(AuthenticationManager的實現類一般是ProviderManager。而ProviderManager內部維護了一個List,真正的身份認證是由一系列AuthenticationProvider去完成。而AuthenticationProvider的常用實現類則是DaoAuthenticationProvider,DaoAuthenticationProvider內部又聚合了一個UserDetailsService接口,UserDetailsService才是獲取用戶詳細信息的最終接口,而我們上一篇文章中在內存中配置用戶,就是使用了UserDetailsService的一個實現類InMemoryUserDetailsManager)。UML類圖可以大概理解下這些類的關系,省略了授權部分。

可能機智的讀者會發現一個問題,我前面一片文章已經提到了client模式是不存在“用戶”的概念的,那么這里的身份認證是在認證什么呢?debug可以發現UserDetailsService的實現被適配成了ClientDetailsUserDetailsService,這個設計是將client客戶端的信息(client_id,client_secret)適配成用戶的信息(username,password),這樣我們的認證流程就不需要修改了。
經過ClientCredentialsTokenEndpointFilter之后,身份信息已經得到了AuthenticationManager的驗證。接著便到達了
TokenEndpoint。
## TokenEndpoint
前面的兩個ClientCredentialsTokenEndpointFilter和DaoAuthenticationProvider可以理解為一些前置校驗,和身份封裝,而這個類一看名字就知道和我們的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);
...
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
...
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
...
return getResponse(token);
}
private TokenGranter tokenGranter;
}
~~~
省略了一些校驗代碼之后,真正的/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接口的設計有一個宏觀的認識

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
~~~
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內部的運作流程,其他模式有一定的不同,但抽象功能是固定的,只是具體的實現類會被響應地替換。閱讀spring的源碼,會發現它的設計中出現了非常多的抽象接口,這對我們理清楚內部工作流程產生了不小的困擾,我的方式是可以借助UML類圖,先從宏觀理清楚作者的設計思路,這會讓我們的分析事半功倍。
下一篇文章重點分析用戶攜帶token訪問受限資源時,spring security oauth2內部的工作流程。
- java
- 設計模式
- 設計模式總覽
- 設計原則
- 工廠方法模式
- 抽象工廠模式
- 單例模式
- 建造者模式
- 原型模式
- 適配器模式
- 裝飾者模式
- 代理模式
- 外觀模式
- 橋接模式
- 組合模式
- 享元模式
- 策略模式
- 模板方法模式
- 觀察者模式
- 迭代子模式
- 責任鏈模式
- 命令模式
- 備忘錄模式
- 狀態模式
- 訪問者模式
- 中介者模式
- 解釋器模式
- 附錄
- JVM相關
- JVM內存結構
- Java虛擬機的內存組成以及堆內存介紹
- Java堆和棧
- 附錄-數據結構的堆棧和內存分配的堆區棧區的區別
- Java內存之Java 堆
- Java內存之虛擬機和內存區域概述
- Java 內存之方法區和運行時常量池
- Java 內存之直接內存(堆外內存)
- JAVA內存模型
- Java內存模型介紹
- 內存模型如何解決緩存一致性問題
- 深入理解Java內存模型——基礎
- 深入理解Java內存模型——重排序
- 深入理解Java內存模型——順序一致性
- 深入理解Java內存模型——volatile
- 深入理解Java內存模型——鎖
- 深入理解Java內存模型——final
- 深入理解Java內存模型——總結
- 內存可見性
- JAVA對象模型
- JVM內存結構 VS Java內存模型 VS Java對象模型
- Java的對象模型
- Java的對象頭
- HotSpot虛擬機
- HotSpot虛擬機對象探秘
- 深入分析Java的編譯原理
- Java虛擬機的鎖優化技術
- 對象和數組并不是都在堆上分配內存的
- 垃圾回收
- JVM內存管理及垃圾回收
- JVM 垃圾回收器工作原理及使用實例介紹
- JVM內存回收理論與實現(對象存活的判定)
- JVM參數及調優
- CMS GC日志分析
- JVM實用參數(一)JVM類型以及編譯器模式
- JVM實用參數(二)參數分類和即時(JIT)編譯器診斷
- JVM實用參數(三)打印所有XX參數及值
- JVM實用參數(四)內存調優
- JVM實用參數(五)新生代垃圾回收
- JVM實用參數(六) 吞吐量收集器
- JVM實用參數(七)CMS收集器
- JVM實用參數(八)GC日志
- Java性能調優原則
- JVM 優化經驗總結
- 面試題整理
- 面試題1
- java日志規約
- Spring安全
- OAtuth2.0簡介
- Spring Session 簡介(一)
- Spring Session 簡介(二)
- Spring Session 簡介(三)
- Spring Security 簡介(一)
- Spring Security 簡介(二)
- Spring Security 簡介(三)
- Spring Security 簡介(四)
- Spring Security 簡介(五)
- Spring Security Oauth2 (一)
- Spring Security Oauth2 (二)
- Spring Security Oauth2 (三)
- SpringBoot
- Shiro
- Shiro和Spring Security對比
- Shiro簡介
- Session、Cookie和Cache
- Web Socket
- Spring WebFlux