引言: 本文系《認證鑑權與API權限控制在微服務架構中的設計與實現》系列的第二篇,本文重點講解用戶身份的認證與token發放的具體實現。本文篇幅較長,對涉及到的大部分代碼進行了分析,可收藏於閒暇時間閱讀,歡迎訂閱本系列文章。java
在上一篇 認證鑑權與API權限控制在微服務架構中的設計與實現(一)介紹了該項目的背景以及技術調研與最後選型,而且對於最終實現的endpoint執行結果進行展現。對系統架構雖然有提到,可是並未列出詳細流程圖。在筆者的應用場景中,Auth系統與網關進行結合。在網關出配置相應的端點信息,如登陸系統申請token受權,校驗check_token等端點。 git
下圖爲網關與Auth系統結合的流程圖,網關係統的具體實現細節在後面另寫文章介紹。(此處流程圖的繪製中,筆者使用極簡的語言描述,各位同窗輕噴😆!)github
上圖展現了系統登陸的簡單流程,其中的細節有省略,用戶信息的合法性校驗實際是調用用戶系統。大致流程是這樣,客戶端請求到達網關以後,根據網關識別的請求登陸端點,轉發到Auth系統,將用戶的信息進行校驗。web
另外一方面是對於通常請求的校驗。一些不須要權限的公開接口,在網關處配置好,請求到達網關後,匹配了路徑將會直接放行。若是須要對該請求進行校驗,會將該請求的相關驗證信息截取,以及API權限校驗所需的上下文信息(筆者項目對於一些操做進行權限前置驗證,下一篇章會講到),調用Auth系統,校驗成功後進行路由轉發。算法
這篇文章就重點講解咱們在第一篇文章中提到的用戶身份的認證與token發放。這個也主要包含兩個方面:安全
關於AuthorizationServer
和ResourceServer
的配置在上一篇文章已經列出。AuthorizationServer
主要是繼承了AuthorizationServerConfigurerAdapter
,覆寫了其實現接口的三個方法:bash
//對應於配置AuthorizationServer安全認證的相關信息,建立ClientCredentialsTokenEndpointFilter核心過濾器
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
//配置OAuth2的客戶端相關信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
//配置身份認證器,配置認證方式,TokenStore,TokenGranter,OAuth2RequestFactory
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}複製代碼
Authentication
類的類圖主要的驗證方法authenticate(Authentication authentication)
在接口AuthenticationManager
中,其實現類有ProviderManager
,有上圖能夠看出ProviderManager
又依賴於AuthenticationProvider
接口,其定義了一個List<AuthenticationProvider>
全局變量。筆者這邊實現了該接口的實現類CustomAuthenticationProvider
。自定義一個provider
,並在GlobalAuthenticationConfigurerAdapter
中配置好改自定義的校驗provider
,覆寫configure()
方法。服務器
@Configuration
public class AuthenticationManagerConfig extends GlobalAuthenticationConfigurerAdapter {
@Autowired
CustomAuthenticationProvider customAuthenticationProvider;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider);//使用自定義的AuthenticationProvider
}
}複製代碼
AuthenticationManagerBuilder
是用來建立AuthenticationManager
,容許自定義提供多種方式的AuthenticationProvider
,好比LDAP、基於JDBC等等。微信
下面講解認證與受權token主要的類與接口。網絡
TokenEndpoint
Spring-Security-Oauth2的提供的jar包中內置了與token相關的基礎端點。本文認證與受權token與/oauth/token
有關,其處理的接口類爲TokenEndpoint
。下面咱們來看一下對於認證與受權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 {
//首先對client信息進行校驗
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
String clientId = getClientId(principal);
//根據請求中的clientId,加載client的具體信息
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
...
//驗證scope域範圍
if (authenticatedClient != null) {
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
//受權方式不能爲空
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
}
//token endpoint不支持Implicit模式
if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
}
...
//進入CompositeTokenGranter,匹配受權模式,而後進行password模式的身份驗證和token的發放
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
...
}複製代碼
上面給代碼進行了註釋,讀者感興趣能夠看看。接口處理的主要流程就是對authentication信息進行檢查是否合法,不合法直接拋出異常,而後對請求的GrantType進行處理,根據GrantType,進行password模式的身份驗證和token的發放。
這邊涉及到的getTokenGranter()
,代碼也列下:
public class CompositeTokenGranter implements TokenGranter {
//GrantType的集合,有五種,以前有講
private final List<TokenGranter> tokenGranters;
public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
}
//遍歷list,匹配到相應的grantType就進行處理
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
for (TokenGranter granter : tokenGranters) {
OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
if (grant!=null) {
return grant;
}
}
return null;
}
...
}複製代碼
本次請求是使用的password模式,隨後進入其GrantType具體的處理流程,下面是grant()
方法。
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) {
return null;
}
String clientId = tokenRequest.getClientId();
//加載clientId對應的ClientDetails,爲了下一步的驗證
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
//再次驗證clientId是否擁有該grantType模式,安全
validateGrantType(grantType, client);
//獲取token
return getAccessToken(client, tokenRequest);
}
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
//進入建立token以前,進行身份驗證
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
//身份驗證
OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, null);
}複製代碼
上面一段代碼是grant()
方法具體的實現細節。GrantType匹配到其對應的grant()
後,先進行基本的驗證確保安全,而後進入主流程,就是下面小節要講的驗證身份和發放token。
CustomAuthenticationProvider
CustomAuthenticationProvider
中定義了驗證方法的具體實現。其具體實現以下所示。
//主要的自定義驗證方法
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
Map data = (Map) authentication.getDetails();
String clientId = (String) data.get("client");
Assert.hasText(clientId,"clientId must have value" );
String type = (String) data.get("type");
//經過調用user服務,校驗用戶信息
Map map = userClient.checkUsernameAndPassword(getUserServicePostObject(username, password, type));
//校驗返回的信息,不正確則拋出異常,受權失敗
String userId = (String) map.get("userId");
if (StringUtils.isBlank(userId)) {
String errorCode = (String) map.get("code");
throw new BadCredentialsException(errorCode);
}
CustomUserDetails customUserDetails = buildCustomUserDetails(username, password, userId, clientId);
return new CustomAuthenticationToken(customUserDetails);
}
//構造一個CustomUserDetails,簡單,略去
private CustomUserDetails buildCustomUserDetails(String username, String password, String userId, String clientId) {
}
//構造一個請求userService的map,內容略
private Map<String, String> getUserServicePostObject(String username, String password, String type) {
}複製代碼
authenticate()
最後返回構造的自定義CustomAuthenticationToken
,在CustomAuthenticationToken
中,將boolean authenticated
設爲true,user信息驗證成功。這邊傳入的參數CustomUserDetails
與token生成有關,做爲payload中的信息,下面會講到。
//繼承抽象類AbstractAuthenticationToken
public class CustomAuthenticationToken extends AbstractAuthenticationToken {
private CustomUserDetails userDetails;
public CustomAuthenticationToken(CustomUserDetails userDetails) {
super(null);
this.userDetails = userDetails;
super.setAuthenticated(true);
}
...
}複製代碼
而AbstractAuthenticationToken
實現了接口Authentication和CredentialsContainer
,裏面的具體信息讀者能夠本身看下源碼。
用戶信息校驗完成以後,下一步則是要對該用戶進行受權。在講具體的受權以前,先補充下關於JWT Token的相關知識點。
Json web token (JWT), 是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準(RFC 7519)。該token被設計爲緊湊且安全的,特別適用於分佈式站點的單點登陸(SSO)場景。JWT的聲明通常被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也能夠增長一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
從上面的描述可知JWT的定義,這邊讀者能夠對比下token的認證和傳統的session認證的區別。推薦一篇文章什麼是 JWT -- JSON WEB TOKEN,筆者這邊就不詳細擴展講了,只是簡單介紹下其構成。
JWT包含三部分:header頭部、payload信息、signature簽名。下面以上一篇生成好的access_token爲例介紹。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9複製代碼
playload
存放的有效信息,這些有效信息包含三個部分、標準中註冊的聲明、公共的聲明、私有的聲明。這邊筆者額外添加的信息爲X-KEETS-UserId
和X-KEETS-ClientId
。讀者可根據實際項目須要進行定製。最後playload通過base64編碼後的結果爲:
eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsImV4cCI6MTUwODQ0Nzc1NiwidXNlcl9uYW1lIjoia2VldHMiLCJqdGkiOiJiYWQ3MmIxOS1kOWYzLTQ5MDItYWZmYS0wNDMwZTdkYjc5ZWQiLCJjbGllbnRfaWQiOiJmcm9udGVuZCIsInNjb3BlIjpbImFsbCJdfQ複製代碼
signature
jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:header (base64後的)、payload (base64後的)、secret。
關於secret,細心的讀者可能會發現以前的配置裏面有具體設置。前兩部分鏈接組成的字符串,經過header中聲明的加密方式進行加鹽secret組合加密,而後就構成了jwt的第三部分。第三部分結果爲:
5ZNVN8TLavgpWy8KZQKArcbj7ItJLLaY1zBRaAgMjdo複製代碼
至於具體應用方法,能夠參見第一篇文章中構建的/logout
端點。
AuthorizationTokenServices
如今到了爲用戶建立token,這邊主要與自定義的接口AuthorizationServerTokenServices
有關。AuthorizationServerTokenServices
主要有以下三個方法:
//建立token
OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
//刷新token
OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest) throws AuthenticationException;
//獲取token
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);複製代碼
因爲篇幅限制,筆者這邊僅對createAccessToken()
的實現方法進行分析,其餘的方法實現,讀者能夠下關注筆者的GitHub項目。
public class CustomAuthorizationTokenServices implements AuthorizationServerTokenServices, ConsumerTokenServices {
...
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
//經過TokenStore,獲取現存的AccessToken
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken;
//移除已有的AccessToken和refreshToken
if (existingAccessToken != null) {
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);
}
//recreate a refreshToken
refreshToken = createRefreshToken(authentication);
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
if (accessToken != null) {
tokenStore.storeAccessToken(accessToken, authentication);
}
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
...
}複製代碼
這邊具體的實如今上面有註釋,基本沒有改寫多少,讀者此處能夠參閱源碼。createAccessToken()
還調用了兩個私有方法,分別建立accessToken和refreshToken。建立accessToken,須要基於refreshToken。
此處能夠自定義設置token的時效長度,accessToken建立實現以下:
private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.
private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
//對應tokenId,存儲的標識
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);
//scope對應做用範圍
token.setScope(authentication.getOAuth2Request().getScope());
//上一節介紹的自定義TokenEnhancer,這邊使用
return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}複製代碼
既然提到TokenEnhancer
,這邊簡單貼一下代碼。
public class CustomTokenEnhancer extends JwtAccessTokenConverter {
private static final String TOKEN_SEG_USER_ID = "X-KEETS-UserId";
private static final String TOKEN_SEG_CLIENT = "X-KEETS-ClientId";
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
Map<String, Object> info = new HashMap<>();
//從自定義的userDetails中取出UserId
info.put(TOKEN_SEG_USER_ID, userDetails.getUserId());
DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
customAccessToken.setAdditionalInformation(info);
OAuth2AccessToken enhancedToken = super.enhance(customAccessToken, authentication);
//設置ClientId
enhancedToken.getAdditionalInformation().put(TOKEN_SEG_CLIENT, userDetails.getClientId());
return enhancedToken;
}
}複製代碼
自此,用戶身份校驗與發放受權token結束。最終成功返回的結果爲:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsImV4cCI6MTUwODQ0Nzc1NiwidXNlcl9uYW1lIjoia2VldHMiLCJqdGkiOiJiYWQ3MmIxOS1kOWYzLTQ5MDItYWZmYS0wNDMwZTdkYjc5ZWQiLCJjbGllbnRfaWQiOiJmcm9udGVuZCIsInNjb3BlIjpbImFsbCJdfQ.5ZNVN8TLavgpWy8KZQKArcbj7ItJLLaY1zBRaAgMjdo",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsInVzZXJfbmFtZSI6ImtlZXRzIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImJhZDcyYjE5LWQ5ZjMtNDkwMi1hZmZhLTA0MzBlN2RiNzllZCIsImV4cCI6MTUxMDk5NjU1NiwianRpIjoiYWE0MWY1MjctODE3YS00N2UyLWFhOTgtZjNlMDZmNmY0NTZlIiwiY2xpZW50X2lkIjoiZnJvbnRlbmQifQ.mICT1-lxOAqOU9M-Ud7wZBb4tTux6OQWouQJ2nn1DeE",
"expires_in": 43195,
"scope": "all",
"X-KEETS-UserId": "d6448c24-3c4c-4b80-8372-c2d61868f8c6",
"jti": "bad72b19-d9f3-4902-affa-0430e7db79ed",
"X-KEETS-ClientId": "frontend"
}複製代碼
本文開頭給出了Auth系統概述,畫出了簡要的登陸和校驗的流程圖,方便讀者能對系統的實現有個大概的瞭解。而後主要講解了用戶身份的認證與token發放的具體實現。對於其中主要的類和接口進行了分析與講解。下一篇文章主要講解token的鑑定和API級別的上下文權限校驗。
本文的源碼地址:
GitHub:github.com/keets2012/A…
碼雲: gitee.com/keets/Auth-…