引言: 本文系《認證鑑權與API權限控制在微服務架構中的設計與實現》系列的第三篇,本文重點講解token以及API級別的鑑權。本文對涉及到的大部分代碼進行了分析,歡迎訂閱本系列文章。html
在開始講解這一篇文章以前,先對以前兩篇文章進行回憶下。在第一篇 認證鑑權與API權限控制在微服務架構中的設計與實現(一)介紹了該項目的背景以及技術調研與最後選型。第二篇認證鑑權與API權限控制在微服務架構中的設計與實現(二)畫出了簡要的登陸和校驗的流程圖,並重點講解了用戶身份的認證與token發放的具體實現。git
本文重點講解鑑權,包括兩個方面:token合法性以及API級別的操做權限。首先token合法性很容易理解,第二篇文章講解了獲取受權token的一系列流程,token是不是認證服務器頒發的,必然是須要驗證的。其次對於API級別的操做權限,將上下文信息不具有操做權限的請求直接拒絕,固然此處是設計token合法性校驗在先,其次再對操做權限進行驗證,若是前一個驗證直接拒絕,經過則進入操做權限驗證。github
ResourceServer
配置在第一篇就列出了,在進入鑑權以前,把這邊的配置搞清,即便有些配置在本項目中沒有用到,你們在本身的項目有可能用到。web
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的配置。spring
(2). 關聯的HttpSecurity
,與以前的 Spring Security XML中的 「http」元素配置相似,它容許配置基於web安全以針對特定http請求。默認是應用到全部的請求,經過requestMatcher
能夠限定具體URL範圍。HttpSecurity類圖以下。api
總的來講:HttpSecurity是SecurityBuilder接口的一個實現類,從名字上咱們就能夠看出這是一個HTTP安全相關的構建器。固然咱們在構建的時候可能須要一些配置,當咱們調用HttpSecurity對象的方法時,實際上就是在進行配置。安全
authorizeRequests(),formLogin()、httpBasic()這三個方法返回的分別是ExpressionUrlAuthorizationConfigurer
、FormLoginConfigurer
、HttpBasicConfigurer
,他們都是SecurityConfigurer接口的實現類,分別表明的是不一樣類型的安全配置器。
所以,從總的流程上來講,當咱們在進行配置的時候,須要一個安全構建器SecurityBuilder(例如咱們這裏的HttpSecurity),SecurityBuilder實例的建立須要有若干安全配置器SecurityConfigurer實例的配合。服務器
(3).關聯的ResourceServerSecurityConfigurer
,爲資源服務器添加特殊的配置,默認的適用於不少應用,可是這邊的修改至少以resourceId爲單位。類圖以下。session
ResourceServerSecurityConfigurer
建立了OAuth2核心過濾器OAuth2AuthenticationProcessingFilter
,併爲其提供固定了OAuth2AuthenticationManager
。只有被OAuth2AuthenticationProcessingFilter
攔截到的oauth2相關請求才被特殊的身份認證器處理。同時設置了TokenExtractor、異常處理實現。架構
OAuth2AuthenticationProcessingFilter
是OAuth2保護資源的預先認證過濾器。配合OAuth2AuthenticationManager
使用,根據請求獲取到OAuth2 token,以後就會使用OAuth2Authentication
來填充Spring Security上下文。OAuth2AuthenticationManager
在前面的文章給出的AuthenticationManager
類圖就出現了,與token認證相關。這邊略過貼出源碼進行講解,讀者能夠自行閱讀。
鑑權主要是使用內置的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級別的權限校驗。
從上面的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。
筆者項目目前都是基於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()
方法,你們也能夠對本身業務的操做權限進行校驗,很是靈活。這邊涉及到具體業務,筆者在項目中只提供接口,具體的實現須要讀者自行完成。
本文相對來講比較簡單,主要講解了token以及API級別的鑑權。token的合法性認證很常規,Auth系統對於API級別的鑑權是結合自身業務須要和現狀進行的設計。這兩塊的校驗都前置到Auth系統中,優缺點在上面的小節也有講述。最後,架構設計根據本身的需求和現狀,筆者的解決思路僅供參考。
本文的源碼地址:
GitHub:https://github.com/keets2012/Auth-service
碼雲: https://gitee.com/keets/Auth-Service