認證鑑權與API許可權控制在微服務架構中的設計與實現(三)
引言: 本文系《認證鑑權與API許可權控制在微服務架構中的設計與實現》系列的第三篇,本文重點講解token以及API級別的鑑權。本文對涉及到的大部分程式碼進行了分析,歡迎訂閱本系列文章。
1. 前文回顧
在開始講解這一篇文章之前,先對之前兩篇文章進行回憶下。在第一篇 認證鑑權與API許可權控制在微服務架構中的設計與實現(一)介紹了該專案的背景以及技術調研與最後選型。第二篇認證鑑權與API許可權控制在微服務架構中的設計與實現(二)畫出了簡要的登入和校驗的流程圖,並重點講解了使用者身份的認證與token發放的具體實現。
本文重點講解鑑權,包括兩個方面:token合法性以及API級別的操作許可權。首先token合法性很容易理解,第二篇文章講解了獲取授權token的一系列流程,token是否是認證伺服器頒發的,必然是需要驗證的。其次對於API級別的操作許可權,將上下文資訊不具備操作許可權的請求直接拒絕,當然此處是設計token合法性校驗在先,其次再對操作許可權進行驗證,如果前一個驗證直接拒絕,通過則進入操作許可權驗證。
2.資源伺服器配置
ResourceServer
配置在第一篇就列出了,在進入鑑權之前,把這邊的配置搞清,即使有些配置在本專案中沒有用到,大家在自己的專案有可能用到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { //http安全配置 @Override public void configure(HttpSecurity http) throws Exception { //禁掉csrf,設定session策略 http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and()//預設允許訪問 .requestMatchers().antMatchers("/**") .and().authorizeRequests() .antMatchers("/**").permitAll() .anyRequest().authenticated() .and().logout() //logout登出端點配置 .logoutUrl("/logout") .clearAuthentication(true) .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()) .addLogoutHandler(customLogoutHandler()); } //新增自定義的CustomLogoutHandler @Bean public CustomLogoutHandler customLogoutHandler() { return new CustomLogoutHandler(); } //資源安全配置相關 @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { super.configure(resources); } } |
(1). @EnableResourceServer
這個註解很重要,OAuth2資源伺服器的簡便註解。其使得Spring Security filter通過請求中的OAuth2 token來驗證請求。通常與EnableWebSecurity
配合使用,該註解還建立了硬編碼的@Order(3) WebSecurityConfigurerAdapter
,由於當前spring的技術,order的順序不易修改,所以在專案中避免還有其他order=3的配置。
(2). 關聯的HttpSecurity
,與之前的 Spring Security XML中的 “http”元素配置類似,它允許配置基於web安全以針對特定http請求。預設是應用到所有的請求,通過requestMatcher
可以限定具體URL範圍。HttpSecurity類圖如下。
總的來說:HttpSecurity是SecurityBuilder介面的一個實現類,從名字上我們就可以看出這是一個HTTP安全相關的構建器。當然我們在構建的時候可能需要一些配置,當我們呼叫HttpSecurity物件的方法時,實際上就是在進行配置。
authorizeRequests(),formLogin()、httpBasic()這三個方法返回的分別是ExpressionUrlAuthorizationConfigurer
、FormLoginConfigurer
、HttpBasicConfigurer
,他們都是SecurityConfigurer介面的實現類,分別代表的是不同型別的安全配置器。
因此,從總的流程上來說,當我們在進行配置的時候,需要一個安全構建器SecurityBuilder(例如我們這裡的HttpSecurity),SecurityBuilder例項的建立需要有若干安全配置器SecurityConfigurer例項的配合。
(3).關聯的ResourceServerSecurityConfigurer
,為資源伺服器新增特殊的配置,預設的適用於很多應用,但是這邊的修改至少以resourceId為單位。類圖如下。
ResourceServerSecurityConfigurer類圖
ResourceServerSecurityConfigurer
建立了OAuth2核心過濾器OAuth2AuthenticationProcessingFilter
,併為其提供固定了OAuth2AuthenticationManager
。只有被OAuth2AuthenticationProcessingFilter
攔截到的oauth2相關請求才被特殊的身份認證器處理。同時設定了TokenExtractor、異常處理實現。
OAuth2AuthenticationProcessingFilter
是OAuth2保護資源的預先認證過濾器。配合OAuth2AuthenticationManager
使用,根據請求獲取到OAuth2 token,之後就會使用OAuth2Authentication
來填充Spring Security上下文。OAuth2AuthenticationManager
在前面的文章給出的AuthenticationManager
類圖就出現了,與token認證相關。這邊略過貼出原始碼進行講解,讀者可以自行閱讀。
3. 鑑權endpoint
鑑權主要是使用內建的endpoint /oauth/check_token
,筆者將對端點的分析放在前面,因為這是鑑權的唯一入口。下面我們來看下該API介面中的主要程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
@RequestMapping(value = "/oauth/check_token") @ResponseBody public Map<String, ?> checkToken(CheckTokenEntity checkTokenEntity) { //CheckTokenEntity為自定義的dto Assert.notNull(checkTokenEntity, "invalid token entity!"); //識別token OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(checkTokenEntity.getToken()); //判斷token是否為空 if (token == null) { throw new InvalidTokenException("Token was not recognised"); } //未過期 if (token.isExpired()) { throw new InvalidTokenException("Token has expired"); } //載入OAuth2Authentication OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue()); //獲取response,token合法性驗證完畢 Map<String, Object> response = (Map<String, Object>) accessTokenConverter.convertAccessToken(token, authentication); //check for api permission if (response.containsKey("jti")) { //上下文操作許可權校驗 Assert.isTrue(checkPermissions.checkPermission(checkTokenEntity)); } response.put("active", true); // Always true if token exists and not expired return response; } |
看過security-oauth原始碼的同學可能立馬就看出上述程式碼與原始碼不同,熟悉/oauth/check_token
校驗流程的也會看出來,這邊筆者對security-oauth
jar進行了重新編譯,修改了部分原始碼用於該專案需求的場景。主要是加入了前置的API級別的許可權校驗。
4. token 合法性驗證
從上面的CheckTokenEndpoint
中可以看出,對於token合法性驗證首先是識別請求體中的token。用到的主要方法是ResourceServerTokenServices
提供的readAccessToken()
方法。該介面的實現類為DefaultTokenServices
,在之前的配置中有講過這邊配置了jdbc的TokenStore。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
public class JdbcTokenStore implements TokenStore { ... public OAuth2AccessToken readAccessToken(String tokenValue) { OAuth2AccessToken accessToken = null; try { //使用selectAccessTokenSql語句,呼叫了私有的extractTokenKey()方法 accessToken = jdbcTemplate.queryForObject(selectAccessTokenSql, new RowMapper<OAuth2AccessToken>() { public OAuth2AccessToken mapRow(ResultSet rs, int rowNum) throws SQLException { return deserializeAccessToken(rs.getBytes(2)); } }, extractTokenKey(tokenValue)); } //異常情況 catch (EmptyResultDataAccessException e) { if (LOG.isInfoEnabled()) { LOG.info("Failed to find access token for token " + tokenValue); } } catch (IllegalArgumentException e) { LOG.warn("Failed to deserialize access token for " + tokenValue, e); //不合法則移除 removeAccessToken(tokenValue); } return accessToken; } ... //提取TokenKey方法 protected String extractTokenKey(String value) { if (value == null) { return null; } MessageDigest digest; try { //MD5 digest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("MD5 algorithm not available. Fatal (should be in the JDK)."); } try { byte[] bytes = digest.digest(value.getBytes("UTF-8")); return String.format("%032x", new BigInteger(1, bytes)); } catch (UnsupportedEncodingException e) { throw new IllegalStateException("UTF-8 encoding not available. Fatal (should be in the JDK)."); } } } |
readAccessToken()
檢索出該token值的完整資訊。上述程式碼比較簡單,涉及到的邏輯也不復雜,此處簡單講解。下圖為debug token校驗的變數資訊,讀者可以自己動手操作下,截圖僅供參考。
至於後面的步驟,loadAuthentication()
為特定的access token 載入credentials。得到的credentials 與token作為convertAccessToken()
引數,得到校驗token的response。
5. API級別許可權校驗
筆者專案目前都是基於Web的許可權驗證,之前遺留的一個巨大的單體應用系統正在逐漸拆分,然而當前又不能完全拆分完善。為了同時相容新舊服務,儘量減少對業務系統的入侵,實現微服務的統一性和獨立性。筆者根據業務業務場景,嘗試在Auth處做操作許可權校驗。
首先想到的是資源伺服器配置ResourceServer,如:
1 2 |
http.authorizeRequests() .antMatchers("/order/**").access("#oauth2.hasScope('select') and hasRole('ROLE_USER')") |
這樣做需要將每個操作介面的API許可權控制放在各個不同的業務服務,每個服務在接收到請求後,需要先從Auth服務取出該token 對應的role和scope等許可權資訊。這個方法肯定是可行的,但是由於專案鑑權的粒度更細,而且暫時不想大動原有系統,在加上之前閘道器設計,閘道器呼叫Auth服務校驗token合法性,所以最後決定在Auth系統呼叫中,把這些校驗一起解決完。
文章開頭資源伺服器的配置程式碼可以看出,對於所有的資源並沒有做攔截,因為閘道器處是呼叫Auth系統的相關endpoint,並不是所有的請求url都會經過一遍Auth系統,所以對於所有的資源,在Auth系統中,定義需要鑑權介面所需要的API許可權,然後根據上下文進行匹配。這是採用的第二種方式,也是筆者目前採用的方法。當然這種方式的弊端也很明顯,一旦併發量大,閘道器還要耗時在呼叫Auth系統的鑑權上,TPS勢必要下降很多,對於一些不需要鑑權的服務介面也會引起不可用。另外一點是,對於某些特殊許可權的介面,需要的上下文資訊很多,可能並不能完全覆蓋,對於此,筆者的解決是分兩方面:一是儘量將這些特殊情況進行分類,某一類的情況統一解決;二是將嚴苛的校驗降低,對於上下文校驗失敗的直接拒絕,而通過的,對於某些介面,在介面內進行操作之前,對特殊的地方還要再次進行校驗。
上面在講endpoint有提到這邊對原始碼進行了改寫。CheckTokenEntity
是自定義的DTO,這這個類中定義了鑑權需要的上下文,這裡是指能校驗操作許可權的最小集合,如URI、roleId、affairId等等。另外定義了CheckPermissions
介面,其方法checkPermission(CheckTokenEntity checkTokenEntity)
返回了check的結果。而其具體實現類則定義在Auth系統中。筆者專案中呼叫的例項如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Component public class CustomCheckPermission implements CheckPermissions { @Autowired private PermissionService permissionService; @Override public boolean checkPermission(CheckTokenEntity checkTokenEntity) { String url = checkTokenEntity.getUri(); Long affairId = checkTokenEntity.getAffairId(); Long roleId = checkTokenEntity.getRoleId(); //校驗 if (StringUtils.isEmpty(url) || affairId <= 0 || roleId <= 0) { return true; } else { return permissionService.checkPermission(url, affairId, roleId); } } } |
關於jar包spring-cloud-starter-oauth2
中的具體修改內容,大家可以看下文末筆者的GitHub專案。通過自定義CustomCheckPermission
,覆寫checkPermission()
方法,大家也可以對自己業務的操作許可權進行校驗,非常靈活。這邊涉及到具體業務,筆者在專案中只提供介面,具體的實現需要讀者自行完成。
6. 總結
本文相對來說比較簡單,主要講解了token以及API級別的鑑權。token的合法性認證很常規,Auth系統對於API級別的鑑權是結合自身業務需要和現狀進行的設計。這兩塊的校驗都前置到Auth系統中,優缺點在上面的小節也有講述。最後,架構設計根據自己的需求和現狀,筆者的解決思路僅供參考。
本文的原始碼地址:
GitHub:https://github.com/keets2012/Auth-service
碼雲: https://gitee.com/keets/Auth-Service