認證鑒權與API權限控制在微服務架構中的設計與實現(三)


引言: 本文系《認證鑒權與API權限控制在微服務架構中的設計與實現》系列的第三篇,本文重點講解token以及API級別的鑒權。本文對涉及到的大部分代碼進行了分析,歡迎訂閱本系列文章。

1. 前文回顧

在開始講解這一篇文章之前,先對之前兩篇文章進行回憶下。在第一篇 認證鑒權與API權限控制在微服務架構中的設計與實現(一)介紹了該項目的背景以及技術調研與最后選型。第二篇認證鑒權與API權限控制在微服務架構中的設計與實現(二)畫出了簡要的登錄和校驗的流程圖,並重點講解了用戶身份的認證與token發放的具體實現。

本文重點講解鑒權,包括兩個方面:token合法性以及API級別的操作權限。首先token合法性很容易理解,第二篇文章講解了獲取授權token的一系列流程,token是否是認證服務器頒發的,必然是需要驗證的。其次對於API級別的操作權限,將上下文信息不具備操作權限的請求直接拒絕,當然此處是設計token合法性校驗在先,其次再對操作權限進行驗證,如果前一個驗證直接拒絕,通過則進入操作權限驗證。

2.資源服務器配置

ResourceServer配置在第一篇就列出了,在進入鑒權之前,把這邊的配置搞清,即使有些配置在本項目中沒有用到,大家在自己的項目有可能用到。

 1 @Configuration
 2 @EnableResourceServer
 3 public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
 4     //http安全配置
 5     @Override
 6     public void configure(HttpSecurity http) throws Exception {
 7         //禁掉csrf,設置session策略
 8         http.csrf().disable()
 9                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
10                 .and()//默認允許訪問
11                 .requestMatchers().antMatchers("/**")
12                 .and().authorizeRequests()
13                 .antMatchers("/**").permitAll()
14                 .anyRequest().authenticated()
15                 .and().logout() //logout注銷端點配置
16                 .logoutUrl("/logout")
17                 .clearAuthentication(true)
18                 .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
19                 .addLogoutHandler(customLogoutHandler());
20     }
21     //添加自定義的CustomLogoutHandler
22     @Bean
23     public CustomLogoutHandler customLogoutHandler() {
24         return new CustomLogoutHandler();
25     }
26     //資源安全配置相關
27     @Override
28     public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
29         super.configure(resources);
30     }
31 }

(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()這三個方法返回的分別是ExpressionUrlAuthorizationConfigurerFormLoginConfigurerHttpBasicConfigurer,他們都是SecurityConfigurer接口的實現類,分別代表的是不同類型的安全配置器。
因此,從總的流程上來說,當我們在進行配置的時候,需要一個安全構建器SecurityBuilder(例如我們這里的HttpSecurity),SecurityBuilder實例的創建需要有若干安全配置器SecurityConfigurer實例的配合。

(3).關聯的ResourceServerSecurityConfigurer,為資源服務器添加特殊的配置,默認的適用於很多應用,但是這邊的修改至少以resourceId為單位。類圖如下。

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 @RequestMapping(value = "/oauth/check_token")
 2    @ResponseBody
 3    public Map<String, ?> checkToken(CheckTokenEntity checkTokenEntity) {
 4     //CheckTokenEntity為自定義的dto
 5        Assert.notNull(checkTokenEntity, "invalid token entity!");
 6        //識別token
 7        OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(checkTokenEntity.getToken());
 8     //判斷token是否為空
 9        if (token == null) {
10            throw new InvalidTokenException("Token was not recognised");
11        }
12     //未過期
13        if (token.isExpired()) {
14            throw new InvalidTokenException("Token has expired");
15        }
16        //加載OAuth2Authentication
17        OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
18     //獲取response,token合法性驗證完畢
19        Map<String, Object> response = (Map<String, Object>) accessTokenConverter.convertAccessToken(token, authentication);
20        //check for api permission
21        if (response.containsKey("jti")) {
22            //上下文操作權限校驗
23            Assert.isTrue(checkPermissions.checkPermission(checkTokenEntity));
24        }
25        
26        response.put("active", true);    // Always true if token exists and not expired
27        return response;
28    }

看過security-oauth源碼的同學可能立馬就看出上述代碼與源碼不同,熟悉/oauth/check_token校驗流程的也會看出來,這邊筆者對security-oauth jar進行了重新編譯,修改了部分源碼用於該項目需求的場景。主要是加入了前置的API級別的權限校驗。

4. token 合法性驗證

從上面的CheckTokenEndpoint中可以看出,對於token合法性驗證首先是識別請求體中的token。用到的主要方法是ResourceServerTokenServices提供的readAccessToken()方法。該接口的實現類為DefaultTokenServices,在之前的配置中有講過這邊配置了jdbc的TokenStore。

 1 public class JdbcTokenStore implements TokenStore {
 2     ...
 3     public OAuth2AccessToken readAccessToken(String tokenValue) {
 4         OAuth2AccessToken accessToken = null;
 5         try {
 6             //使用selectAccessTokenSql語句,調用了私有的extractTokenKey()方法
 7             accessToken = jdbcTemplate.queryForObject(selectAccessTokenSql, new RowMapper<OAuth2AccessToken>() {
 8                 public OAuth2AccessToken mapRow(ResultSet rs, int rowNum) throws SQLException {
 9                     return deserializeAccessToken(rs.getBytes(2));
10                 }
11             }, extractTokenKey(tokenValue));
12         }
13         //異常情況
14         catch (EmptyResultDataAccessException e) {
15             if (LOG.isInfoEnabled()) {
16                 LOG.info("Failed to find access token for token " + tokenValue);
17             }
18         }
19         catch (IllegalArgumentException e) {
20             LOG.warn("Failed to deserialize access token for " + tokenValue, e);
21             //不合法則移除
22             removeAccessToken(tokenValue);
23         }
24         return accessToken;
25     }
26     ...
27     //提取TokenKey方法
28     protected String extractTokenKey(String value) {
29         if (value == null) {
30             return null;
31         }
32         MessageDigest digest;
33         try {
34             //MD5
35             digest = MessageDigest.getInstance("MD5");
36         }
37         catch (NoSuchAlgorithmException e) {
38             throw new IllegalStateException("MD5 algorithm not available.  Fatal (should be in the JDK).");
39         }
40         try {
41             byte[] bytes = digest.digest(value.getBytes("UTF-8"));
42             return String.format("%032x", new BigInteger(1, bytes));
43         }
44         catch (UnsupportedEncodingException e) {
45             throw new IllegalStateException("UTF-8 encoding not available.  Fatal (should be in the JDK).");
46         }
47     }
48 }

 readAccessToken()檢索出該token值的完整信息。上述代碼比較簡單,涉及到的邏輯也不復雜,此處簡單講解。下圖為debug token校驗的變量信息,讀者可以自己動手操作下,截圖僅供參考。

至於后面的步驟,loadAuthentication()為特定的access token 加載credentials。得到的credentials 與token作為convertAccessToken()參數,得到校驗token的response。

5. API級別權限校驗

筆者項目目前都是基於Web的權限驗證,之前遺留的一個巨大的單體應用系統正在逐漸拆分,然而當前又不能完全拆分完善。為了同時兼容新舊服務,盡量減少對業務系統的入侵,實現微服務的統一性和獨立性。筆者根據業務業務場景,嘗試在Auth處做操作權限校驗。
首先想到的是資源服務器配置ResourceServer,如:

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 @Component
 2 public class CustomCheckPermission implements CheckPermissions {
 3     @Autowired
 4     private PermissionService permissionService;
 5     @Override
 6     public boolean checkPermission(CheckTokenEntity checkTokenEntity) {
 7         String url = checkTokenEntity.getUri();
 8         Long affairId = checkTokenEntity.getAffairId();
 9         Long roleId = checkTokenEntity.getRoleId();
10         //校驗
11         if (StringUtils.isEmpty(url) || affairId <= 0 || roleId <= 0) {
12             return true;
13         } else {
14             return permissionService.checkPermission(url, affairId, roleId);
15         }
16     }
17 }

關於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


參考

    1. 微服務API級權限的技術架構
    2. spring-security-oauth
    3. Spring-Security Docs

 

來源: http://blueskykong.com/2017/10/24/security3/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM