作者: [Aoho's Blog]
引言: 本文系《認(rèn)證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計(jì)與實(shí)現(xiàn)》系列的第二篇,本文重點(diǎn)講解用戶身份的認(rèn)證與token發(fā)放的具體實(shí)現(xiàn)乾闰。本文篇幅較長色罚,對涉及到的大部分代碼進(jìn)行了分析,可收藏于閑暇時(shí)間閱讀,歡迎訂閱本系列文章益咬。
1. 系統(tǒng)概覽
在上一篇 認(rèn)證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計(jì)與實(shí)現(xiàn)(一)介紹了該項(xiàng)目的背景以及技術(shù)調(diào)研與最后選型,并且對于最終實(shí)現(xiàn)的endpoint執(zhí)行結(jié)果進(jìn)行展示帜平。對系統(tǒng)架構(gòu)雖然有提到幽告,但是并未列出詳細(xì)流程圖。在筆者的應(yīng)用場景中裆甩,Auth系統(tǒng)與網(wǎng)關(guān)進(jìn)行結(jié)合冗锁。在網(wǎng)關(guān)出配置相應(yīng)的端點(diǎn)信息,如登錄系統(tǒng)申請token授權(quán)嗤栓,校驗(yàn)check_token等端點(diǎn)冻河。
下圖為網(wǎng)關(guān)與Auth系統(tǒng)結(jié)合的流程圖,網(wǎng)關(guān)系統(tǒng)的具體實(shí)現(xiàn)細(xì)節(jié)在后面另寫文章介紹茉帅。(此處流程圖的繪制中叨叙,筆者使用極簡的語言描述,各位同學(xué)輕噴???芭臁)
[上圖展示了系統(tǒng)登錄的簡單流程擂错,其中的細(xì)節(jié)有省略,用戶信息的合法性校驗(yàn)實(shí)際是調(diào)用用戶系統(tǒng)樱蛤。大體流程是這樣钮呀,客戶端請求到達(dá)網(wǎng)關(guān)之后,根據(jù)網(wǎng)關(guān)識(shí)別的請求登錄端點(diǎn)昨凡,轉(zhuǎn)發(fā)到Auth系統(tǒng)爽醋,將用戶的信息進(jìn)行校驗(yàn)。
另一方面是對于一般請求的校驗(yàn)土匀。一些不需要權(quán)限的公開接口子房,在網(wǎng)關(guān)處配置好,請求到達(dá)網(wǎng)關(guān)后就轧,匹配了路徑將會(huì)直接放行证杭。如果需要對該請求進(jìn)行校驗(yàn),會(huì)將該請求的相關(guān)驗(yàn)證信息截取妒御,以及API權(quán)限校驗(yàn)所需的上下文信息(筆者項(xiàng)目對于一些操作進(jìn)行權(quán)限前置驗(yàn)證解愤,下一篇章會(huì)講到),調(diào)用Auth系統(tǒng)乎莉,校驗(yàn)成功后進(jìn)行路由轉(zhuǎn)發(fā)送讲。
[這篇文章就重點(diǎn)講解我們在第一篇文章中提到的用戶身份的認(rèn)證與token發(fā)放奸笤。這個(gè)也主要包含兩個(gè)方面:
- 用戶合法性的認(rèn)證
- 獲取到授權(quán)的token
2. 配置與類圖
2.1 AuthorizationServer主要配置
關(guān)于AuthorizationServer
和ResourceServer
的配置在上一篇文章已經(jīng)列出。AuthorizationServer
主要是繼承了AuthorizationServerConfigurerAdapter
哼鬓,覆寫了其實(shí)現(xiàn)接口的三個(gè)方法:
//對應(yīng)于配置AuthorizationServer安全認(rèn)證的相關(guān)信息监右,創(chuàng)建ClientCredentialsTokenEndpointFilter核心過濾器
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
//配置OAuth2的客戶端相關(guān)信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
//配置身份認(rèn)證器,配置認(rèn)證方式异希,TokenStore健盒,TokenGranter,OAuth2RequestFactory
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}
2.2 主要Authentication
類的類圖
[主要的驗(yàn)證方法authenticate(Authentication authentication)
在接口AuthenticationManager
中称簿,其實(shí)現(xiàn)類有ProviderManager
扣癣,有上圖可以看出ProviderManager
又依賴于AuthenticationProvider
接口,其定義了一個(gè)List
全局變量憨降。筆者這邊實(shí)現(xiàn)了該接口的實(shí)現(xiàn)類CustomAuthenticationProvider
父虑。自定義一個(gè)provider
,并在GlobalAuthenticationConfigurerAdapter
中配置好改自定義的校驗(yàn)provider
授药,覆寫configure()
方法士嚎。
@Configuration
public class AuthenticationManagerConfig extends GlobalAuthenticationConfigurerAdapter {
@Autowired
CustomAuthenticationProvider customAuthenticationProvider;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider);//使用自定義的AuthenticationProvider
}
}
AuthenticationManagerBuilder
是用來創(chuàng)建AuthenticationManager
,允許自定義提供多種方式的AuthenticationProvider
烁焙,比如LDAP航邢、基于JDBC等等。
3. 認(rèn)證與授權(quán)token
下面講解認(rèn)證與授權(quán)token主要的類與接口骄蝇。
3.1 內(nèi)置端點(diǎn)TokenEndpoint
Spring-Security-Oauth2的提供的jar包中內(nèi)置了與token相關(guān)的基礎(chǔ)端點(diǎn)膳殷。本文認(rèn)證與授權(quán)token與/oauth/token
有關(guān),其處理的接口類為TokenEndpoint
九火。下面我們來看一下對于認(rèn)證與授權(quán)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信息進(jìn)行校驗(yàn)
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
String clientId = getClientId(principal);
//根據(jù)請求中的clientId,加載client的具體信息
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
...
//驗(yàn)證scope域范圍
if (authenticatedClient != null) {
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
//授權(quán)方式不能為空
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");
}
...
//進(jìn)入CompositeTokenGranter岔激,匹配授權(quán)模式勒极,然后進(jìn)行password模式的身份驗(yàn)證和token的發(fā)放
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
...
}
[上面給代碼進(jìn)行了注釋,讀者感興趣可以看看虑鼎。接口處理的主要流程就是對authentication信息進(jìn)行檢查是否合法辱匿,不合法直接拋出異常,然后對請求的GrantType進(jìn)行處理炫彩,根據(jù)GrantType匾七,進(jìn)行password模式的身份驗(yàn)證和token的發(fā)放。下面我們來看下TokenGranter
的類圖江兢。
可以看出TokenGranter
的實(shí)現(xiàn)類CompositeTokenGranter中有一個(gè)List
昨忆,對應(yīng)五種GrantType的實(shí)際授權(quán)實(shí)現(xiàn)。這邊涉及到的getTokenGranter()
杉允,代碼也列下:
public class CompositeTokenGranter implements TokenGranter {
//GrantType的集合邑贴,有五種席里,之前有講
private final List<TokenGranter> tokenGranters;
public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
}
//遍歷list,匹配到相應(yīng)的grantType就進(jìn)行處理
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模式拢驾,隨后進(jìn)入其GrantType具體的處理流程奖磁,下面是grant()
方法。
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) {
return null;
}
String clientId = tokenRequest.getClientId();
//加載clientId對應(yīng)的ClientDetails独旷,為了下一步的驗(yàn)證
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
//再次驗(yàn)證clientId是否擁有該grantType模式署穗,安全
validateGrantType(grantType, client);
//獲取token
return getAccessToken(client, tokenRequest);
}
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
//進(jìn)入創(chuàng)建token之前寥裂,進(jìn)行身份驗(yàn)證
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
//身份驗(yàn)證
OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, null);
}
上面一段代碼是grant()
方法具體的實(shí)現(xiàn)細(xì)節(jié)嵌洼。GrantType匹配到其對應(yīng)的grant()
后,先進(jìn)行基本的驗(yàn)證確保安全封恰,然后進(jìn)入主流程麻养,就是下面小節(jié)要講的驗(yàn)證身份和發(fā)放token。
3.2 自定義的驗(yàn)證類CustomAuthenticationProvider
CustomAuthenticationProvider
中定義了驗(yàn)證方法的具體實(shí)現(xiàn)诺舔。其具體實(shí)現(xiàn)如下所示鳖昌。
//主要的自定義驗(yàn)證方法
@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");
//通過調(diào)用user服務(wù),校驗(yàn)用戶信息
Map map = userClient.checkUsernameAndPassword(getUserServicePostObject(username, password, type));
//校驗(yàn)返回的信息低飒,不正確則拋出異常许昨,授權(quán)失敗
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);
}
//構(gòu)造一個(gè)CustomUserDetails,簡單褥赊,略去
private CustomUserDetails buildCustomUserDetails(String username, String password, String userId, String clientId) {
}
//構(gòu)造一個(gè)請求userService的map糕档,內(nèi)容略
private Map<String, String> getUserServicePostObject(String username, String password, String type) {
}
authenticate()
最后返回構(gòu)造的自定義CustomAuthenticationToken
,在CustomAuthenticationToken
中拌喉,將boolean authenticated
設(shè)為true速那,user信息驗(yàn)證成功。這邊傳入的參數(shù)CustomUserDetails
與token生成有關(guān)尿背,作為payload中的信息端仰,下面會(huì)講到。
//繼承抽象類AbstractAuthenticationToken
public class CustomAuthenticationToken extends AbstractAuthenticationToken {
private CustomUserDetails userDetails;
public CustomAuthenticationToken(CustomUserDetails userDetails) {
super(null);
this.userDetails = userDetails;
super.setAuthenticated(true);
}
...
}
而AbstractAuthenticationToken
實(shí)現(xiàn)了接口Authentication和CredentialsContainer
田藐,里面的具體信息讀者可以自己看下源碼荔烧。
3.3 關(guān)于JWT
用戶信息校驗(yàn)完成之后,下一步則是要對該用戶進(jìn)行授權(quán)汽久。在講具體的授權(quán)之前鹤竭,先補(bǔ)充下關(guān)于JWT Token的相關(guān)知識(shí)點(diǎn)。
Json web token (JWT), 是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標(biāo)準(zhǔn)(RFC 7519)回窘。該token被設(shè)計(jì)為緊湊且安全的诺擅,特別適用于分布式站點(diǎn)的單點(diǎn)登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務(wù)提供者間傳遞被認(rèn)證的用戶身份信息啡直,以便于從資源服務(wù)器獲取資源烁涌,也可以增加一些額外的其它業(yè)務(wù)邏輯所必須的聲明信息苍碟,該token也可直接被用于認(rèn)證,也可被加密撮执。
從上面的描述可知JWT的定義微峰,這邊讀者可以對比下token的認(rèn)證和傳統(tǒng)的session認(rèn)證的區(qū)別。推薦一篇文章什么是 JWT – JSON WEB TOKEN抒钱,筆者這邊就不詳細(xì)擴(kuò)展講了蜓肆,只是簡單介紹下其構(gòu)成。
JWT包含三部分:header頭部谋币、payload信息仗扬、signature簽名。下面以上一篇生成好的access_token為例介紹蕾额。
-
header
header
jwt的頭部承載兩部分信息早芭,一是聲明類型,這里是jwt诅蝶;二是聲明加密的算法 通常直接使用 HMAC SHA256退个。第一部分一般固定為:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
-
playload
playload
存放的有效信息,這些有效信息包含三個(gè)部分调炬、標(biāo)準(zhǔn)中注冊的聲明语盈、公共的聲明、私有的聲明缰泡。這邊筆者額外添加的信息為X-KEETS-UserId
和X-KEETS-ClientId
刀荒。讀者可根據(jù)實(shí)際項(xiàng)目需要進(jìn)行定制。最后playload經(jīng)過base64編碼后的結(jié)果為:eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsImV4cCI6MTUwODQ0Nzc1NiwidXNlcl9uYW1lIjoia2VldHMiLCJqdGkiOiJiYWQ3MmIxOS1kOWYzLTQ5MDItYWZmYS0wNDMwZTdkYjc5ZWQiLCJjbGllbnRfaWQiOiJmcm9udGVuZCIsInNjb3BlIjpbImFsbCJdfQ
-
signature
signature
jwt的第三部分是一個(gè)簽證信息匀谣,這個(gè)簽證信息由三部分組成:header (base64后的)照棋、payload (base64后的)、secret武翎。signature
jwt的第三部分是一個(gè)簽證信息烈炭,這個(gè)簽證信息由三部分組成:header (base64后的)、payload (base64后的)宝恶、secret符隙。
關(guān)于secret,細(xì)心的讀者可能會(huì)發(fā)現(xiàn)之前的配置里面有具體設(shè)置垫毙。前兩部分連接組成的字符串霹疫,通過header中聲明的加密方式進(jìn)行加鹽secret組合加密,然后就構(gòu)成了jwt的第三部分综芥。第三部分結(jié)果為:5ZNVN8TLavgpWy8KZQKArcbj7ItJLLaY1zBRaAgMjdo
至于具體應(yīng)用方法丽蝎,可以參見第一篇文章中構(gòu)建的/logout
端點(diǎn)。
3.3 自定義的AuthorizationTokenServices
現(xiàn)在到了為用戶創(chuàng)建token,這邊主要與自定義的接口AuthorizationServerTokenServices
有關(guān)屠阻。AuthorizationServerTokenServices
主要有如下三個(gè)方法:
//創(chuàng)建token
OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
//刷新token
OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
throws AuthenticationException;
//獲取token
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
由于篇幅限制红省,筆者這邊僅對createAccessToken()
的實(shí)現(xiàn)方法進(jìn)行分析,其他的方法實(shí)現(xiàn)国觉,讀者可以下關(guān)注筆者的GitHub項(xiàng)目吧恃。
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;
}
...
}
這邊具體的實(shí)現(xiàn)在上面有注釋麻诀,基本沒有改寫多少痕寓,讀者此處可以參閱源碼。createAccessToken()
還調(diào)用了兩個(gè)私有方法蝇闭,分別創(chuàng)建accessToken和refreshToken呻率。創(chuàng)建accessToken,需要基于refreshToken丁眼。
這邊具體的實(shí)現(xiàn)在上面有注釋筷凤,基本沒有改寫多少,讀者此處可以參閱源碼苞七。createAccessToken()
還調(diào)用了兩個(gè)私有方法,分別創(chuàng)建accessToken和refreshToken挪丢。創(chuàng)建accessToken蹂风,需要基于refreshToken。
此處可以自定義設(shè)置token的時(shí)效長度乾蓬,accessToken創(chuàng)建實(shí)現(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) {
//對應(yīng)tokenId惠啄,存儲(chǔ)的標(biāo)識(shí)
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對應(yīng)作用范圍
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";
@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);
//設(shè)置ClientId
enhancedToken.getAdditionalInformation().put(TOKEN_SEG_CLIENT, userDetails.getClientId());
return enhancedToken;
}
}
自此,用戶身份校驗(yàn)與發(fā)放授權(quán)token結(jié)束死嗦。最終成功返回的結(jié)果為:
{
"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"
}
4. 總結(jié)
本文開頭給出了Auth系統(tǒng)概述趋距,畫出了簡要的登錄和校驗(yàn)的流程圖,方便讀者能對系統(tǒng)的實(shí)現(xiàn)有個(gè)大概的了解越除。然后主要講解了用戶身份的認(rèn)證與token發(fā)放的具體實(shí)現(xiàn)节腐。對于其中主要的類和接口進(jìn)行了分析與講解。下一篇文章主要講解token的鑒定和API級別的上下文權(quán)限校驗(yàn)摘盆。
參考
相關(guān)閱讀
認(rèn)證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計(jì)與實(shí)現(xiàn)(一)
我的官網(wǎng)
我的官網(wǎng)http://guan2ye.com
我的CSDN地址http://blog.csdn.net/chenjianandiyi
我的簡書地址http://www.reibang.com/u/9b5d1921ce34
我的githubhttps://github.com/javanan
我的碼云地址https://gitee.com/jamen/
阿里云優(yōu)惠券https://promotion.aliyun.com/ntms/act/ambassador/sharetouser.html?userCode=vf2b5zld&utm_source=vf2b5zld