引言: 本文系《認證鑒權與API權限控制在微服務架構中的設計與實現(xiàn)》系列的第二篇,本文重點講解用戶身份的認證與token發(fā)放的具體實現(xiàn)。
在上一篇《認證鑒權與API權限控制在微服務架構中的設計與實現(xiàn)(一)》介紹了該項目的背景以及技術調(diào)研與最后選型,并且對于最終實現(xiàn)的Endpoint執(zhí)行結果進行展示。對系統(tǒng)架構雖然有提到,但是并未列出詳細流程圖。在筆者的應用場景中,Auth系統(tǒng)與網(wǎng)關進行結合。在網(wǎng)關出配置相應的端點信息,如登錄系統(tǒng)申請token授權,校驗check_token等端點。
下圖為網(wǎng)關與Auth系統(tǒng)結合的流程圖,網(wǎng)關系統(tǒng)的具體實現(xiàn)細節(jié)在后面另寫文章介紹。(此處流程圖的繪制中,筆者使用極簡的語言描述,各位同學輕噴?。?/p>
授權流程圖
上圖展示了系統(tǒng)登錄的簡單流程,其中的細節(jié)有省略,用戶信息的合法性校驗實際是調(diào)用用戶系統(tǒng)。大體流程是這樣,客戶端請求到達網(wǎng)關之后,根據(jù)網(wǎng)關識別的請求登錄端點,轉發(fā)到Auth系統(tǒng),將用戶的信息進行校驗。
另一方面是對于一般請求的校驗。一些不需要權限的公開接口,在網(wǎng)關處配置好,請求到達網(wǎng)關后,匹配了路徑將會直接放行。如果需要對該請求進行校驗,會將該請求的相關驗證信息截取,以及API權限校驗所需的上下文信息(筆者項目對于一些操作進行權限前置驗證,下一盤文章會講到),調(diào)用Auth系統(tǒng),校驗成功后進行路由轉發(fā)。
身份及API權限校驗的流程圖
用戶合法性的認證
獲取到授權的token
2.1 AuthorizationServer主要配置
關于AuthorizationServer和ResourceServer的配置在上一篇文章已經(jīng)列出。AuthorizationServer主要是繼承了AuthorizationServerConfigurerAdapter,覆寫了其實現(xiàn)接口的三個方法:
//對應于配置AuthorizationServer安全認證的相關信息,創(chuàng)建ClientCredentialsTokenEndpointFilter核心過濾器@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception { }//配置OAuth2的客戶端相關信息@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {}//配置身份認證器,配置認證方式,TokenStore,TokenGranter,OAuth2RequestFactory@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {}
2.2 主要Authentication類的類圖
AuthorizationServer UML類圖
主要的驗證方法authenticate(Authentication authentication)在接口AuthenticationManager中,其實現(xiàn)類有ProviderManager,有上圖可以看出ProviderManager又依賴于AuthenticationProvider接口,其定義了一個List<AuthenticationProvider>全局變量。筆者這邊實現(xiàn)了該接口的實現(xiàn)類CustomAuthenticationProvider。自定義一個provider,并在GlobalAuthenticationConfigurerAdapter中配置好改自定義的校驗provider,覆寫configure()方法。
@Configurationpublic class AuthenticationManagerConfig extends GlobalAuthenticationConfigurerAdapter {@AutowiredCustomAuthenticationProvider customAuthenticationProvider;@Overridepublic void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(customAuthenticationProvider);//使用自定義的AuthenticationProvider}}
AuthenticationManagerBuilder是用來創(chuàng)建AuthenticationManager,允許自定義提供多種方式的AuthenticationProvider,比如LDAP、基于JDBC等等。
下面講解認證與授權token主要的類與接口。
3.1 自定義的驗證類CustomAuthenticationProvider
CustomAuthenticationProvider中定義了驗證方法的具體實現(xiàn)。其具體實現(xiàn)如下所示。
//主要的自定義驗證方法@Overridepublic 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"); //通過調(diào)用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,內(nèi)容略private Map<String, String> getUserServicePostObject(String username, String password, String type) {}
authenticate()最后返回構造的自定義CustomAuthenticationToken,在CustomAuthenticationToken中,將boolean authenticated設為true,user信息驗證成功。這邊傳入的參數(shù)CustomUserDetails與token生成有關,作為payload中的信息,下面會講到。
//繼承抽象類AbstractAuthenticationTokenpublic class CustomAuthenticationToken extends AbstractAuthenticationToken {private CustomUserDetails userDetails;public CustomAuthenticationToken(CustomUserDetails userDetails) { super(null); this.userDetails = userDetails; super.setAuthenticated(true);}...}
3.2 關于JWT
用戶信息校驗完成之后,下一步則是要對該用戶進行授權。在講具體的授權之前,先補充下關于JWT Token的相關知識點。
Json web token(JWT),是為了在網(wǎng)絡應用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標準(RFC 7519)。該token被設計為緊湊且安全的,特別適用于分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便于從資源服務器獲取資源,也可以增加一些額外的其它業(yè)務邏輯所必須的聲明信息,該token也可直接被用于認證,也可被加密。
從上面的描述可知JWT的定義,這邊讀者可以對比下token的認證和傳統(tǒng)的session認證的區(qū)別。推薦一篇文章《什么是 JWT – JSON WEB TOKEN》,筆者這邊就不詳細擴展講了,只是簡單介紹下其構成。
header
JWT的頭部承載兩部分信息,一是聲明類型,這里是JWT;二是聲明加密的算法 通常直接使用HMAC SHA256。第一部分一般固定為:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
playload
存放的有效信息,這些有效信息包含三個部分、標準中注冊的聲明、公共的聲明、私有的聲明。這邊筆者額外添加的信息為X-KEETS-UserId和X-KEETS-ClientId。讀者可根據(jù)實際項目需要進行定制。最后playload經(jīng)過base64編碼后的結果為:
eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsImV4cCI6MTUwODQ0Nzc1NiwidXNlcl9uYW1lIjoia2VldHMiLCJqdGkiOiJiYWQ3MmIxOS1kOWYzLTQ5MDItYWZmYS0wNDMwZTdkYjc5ZWQiLCJjbGllbnRfaWQiOiJmcm9udGVuZCIsInNjb3BlIjpbImFsbCJdfQ
signature
JWT的第三部分是一個簽證信息,這個簽證信息由三部分組成:header(base64后的)、payload(base64后的)、secret。
關于secret,細心的讀者可能會發(fā)現(xiàn)之前的配置里面有具體設置。前兩部分連接組成的字符串,通過header中聲明的加密方式進行加鹽secret組合加密,然后就構成了JWT的第三部分。第三部分結果為:
5ZNVN8TLavgpWy8KZQKArcbj7ItJLLaY1zBRaAgMjdo
至于具體應用方法,可以參見第一篇文章中構建的/logout端點。
3.3 自定義的AuthorizationTokenServices
現(xiàn)在到了為用戶創(chuàng)建token,這邊主要與自定義的接口AuthorizationServerTokenServices有關。AuthorizationServerTokenServices主要有如下三個方法:
//創(chuàng)建tokenOAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;//刷新tokenOAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest) throws AuthenticationException;//獲取tokenOAuth2AccessToken getAccessToken(OAuth2Authentication authentication)
由于篇幅限制,筆者這邊僅對createAccessToken()的實現(xiàn)方法進行分析,其他的方法實現(xiàn),讀者可以下關注筆者的GitHub項目。
public class CustomAuthorizationTokenServices implements AuthorizationServerTokenServices, ConsumerTokenServices {...public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { //通過TokenStore,獲取現(xiàn)存的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;}...}
這邊具體的實現(xiàn)在上面有注釋,基本沒有改寫多少,讀者此處可以參閱源碼。createAccessToken()還調(diào)用了兩個私有方法,分別創(chuàng)建accessToken和refreshToken。創(chuàng)建accessToken,需要基于refreshToken。
此處可以自定義設置token的時效長度,accessToken創(chuàng)建實現(xiàn)如下:
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());//上一節(jié)介紹的自定義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";@Overridepublic 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;}}
自此,用戶身份校驗與發(fā)放授權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系統(tǒng)概述,畫出了簡要的登錄和校驗的流程圖,方便讀者能對系統(tǒng)的實現(xiàn)有個大概的了解。然后主要講解了用戶身份的認證與token發(fā)放的具體實現(xiàn)。對于其中主要的類和接口進行了分析與講解。下一篇文章主要講解token的鑒定和API級別的上下文權限校驗。
GitHub:https://github.com/keets2012/Auth-service
碼云:
https://gitee.com/keets/Auth-Service