前言:之前的文章有講過微服務的權限系列和網關實現,都是孤立存在,本文將整合后端服務與網關、權限系統。安全權限部分的實現還講解了基於前置驗證的方式實現,但是由於與業務聯系比較緊密,沒有具體的示例。業務權限與業務聯系非常密切,本次的整合項目將會把這部分的操作權限校驗實現基於具體的業務服務。
1. 前文回顧與整合設計
在認證鑒權與API權限控制在微服務架構中的設計與實現系列文章中,講解了在微服務架構中Auth系統的授權認證和鑒權。在微服務網關中,講解了基於netflix-zuul組件實現的微服務網關。下面我們看一下這次整合的架構圖。
整個流程分為兩類:
- 用戶尚未登錄。客戶端(web和移動端)發起登錄請求,網關對於登錄請求直接轉發到auth服務,auth服務對用戶身份信息進行校驗(整合項目省略用戶系統,讀者可自行實現,直接硬編碼返回用戶信息),最終將身份合法的token返回給客戶端。
- 用戶已登錄,請求其他服務。這種情況,客戶端的請求到達網關,網關會調用auth系統進行請求身份合法性的驗證,驗證不通則直接拒絕,並返回401;如果通過驗證,則轉發到具體服務,服務經過過濾器,根據請求頭部中的userId,獲取該user的安全權限信息。利用切面,對該接口需要的權限進行校驗,通過則proceed,否則返回403。
第一類其實比較簡單,在講解認證鑒權與API權限控制在微服務架構中的設計與實現就基本實現,現在要做的是與網關進行結合;第二類中,我們新建了一個后端服務,與網關、auth系統整合。
下面對整合項目涉及到的三個服務分別介紹。網關和auth服務的實現已經講過,本文主要講下這兩個服務進行整合需要的改動,還有就是對於后端服務的主要實現進行講解。
2. gateway實現
微服務網關已經基本介紹完了網關的實現,包括服務路由、幾種過濾方式等。這一節將重點介紹實際應用時的整合。對於需要修改增強的地方如下:
- 區分暴露接口(即對外直接訪問)和需要合法身份登錄之后才能訪問的接口
- 暴露接口直接放行,轉發到具體服務,如登錄、刷新token等
- 需要合法身份登錄之后才能訪問的接口,根據傳入的Access token進行構造頭部,頭部主要包括userId等信息,可根據自己的實際業務在auth服務中進行設置。
- 最后,比較重要的一點,引入Spring Security的資源服務器配置,對於暴露接口設置permitAll(),其余接口進入身份合法性校驗的流程,調用auth服務,如果通過則正常繼續轉發,否則拋出異常,返回401。
繪制的流程圖如下:
2.1 permitAll實現
對外暴露的接口可以直接訪問,這可以依賴配置文件,而配置文件又可以通過配置中心進行動態更新,所以不用擔心有hard-code的問題。
在配置文件中定義需要permitall的路徑。
1 |
auth: |
服務啟動時,讀入相應的Configuration,下面的配置屬性讀取以auth開頭的配置。
1 |
|
當然還需要有PermitAllUrlProperties對應的實體類,比較簡單,不列出來了。
2.2 加強頭部
Filter過濾器,它是Servlet技術中最實用的技術,Web開發人員通過Filter技術,對web服務器管理的所有web資源進行攔截。這邊使用Filter進行頭部增強,解析請求中的token,構造統一的頭部信息,到了具體服務,可以利用頭部中的userId進行操作權限獲取與判斷。
1 |
public class HeaderEnhanceFilter implements Filter { |
上面代碼列出了頭部增強的基本處理流程,將isPermitAllUrl的請求進行直接傳遞,否則判斷是不是符合規范的頭部,然后解析authorization中的token,構造USER_ID_IN_HEADER。最后為了適配,設置匿名頭部。
需要注意的是,HeaderEnhanceFilter也要進行注冊。Spring 提供了FilterRegistrationBean類,此類提供setOrder方法,可以為filter設置排序值,讓spring在注冊web filter之前排序后再依次注冊。
2.3 資源服務器配置
利用資源服務器的配置,控制哪些是暴露端點不需要進行身份合法性的校驗,直接路由轉發,哪些是需要進行身份loadAuthentication,調用auth服務。
1 |
|
資源服務器的配置大家看了筆者之前的文章應該很熟悉,此處不過多重復講了。關於ResourceServerSecurityConfigurer
配置類,之前的安全系列文章已經講過,ResourceServerTokenServices
接口,當時我們也用到了,只不過用的是默認的DefaultTokenServices
。這邊通過自定義的CustomRemoteTokenServices
,植入身份合法性的相關驗證。
當然這個配置還要引入Spring Cloud Security oauth2的相應依賴。
1 |
<dependency> |
2.4 自定義RemoteTokenServices實現
ResourceServerTokenServices
接口其中的一個實現是RemoteTokenServices
。
Queries the /check_token endpoint to obtain the contents of an access token.
If the endpoint returns a 400 response, this indicates that the token is invalid.
RemoteTokenServices
主要是查詢auth服務的/check_token
端點以獲取一個token的校驗結果。如果有錯誤,則說明token是不合法的。筆者這邊的的CustomRemoteTokenServices
實現就是沿用該思路。需要注意的是,筆者的項目基於Spring cloud,auth服務是多實例的,所以這邊使用了Netflix Ribbon獲取auth服務進行負載均衡。Spring Cloud Security添加如下默認配置,對應auth服務中的相應端點。
1 |
security: |
至於具體的CustomRemoteTokenServices
實現,可以參考上面講的思路以及RemoteTokenServices
,很簡單,此處略去。
至此,網關服務的增強完成,下面看一下我們對auth服務和后端backend服務的實現。
強調一下,為什么頭部傳遞的userId等信息需要在網關構造?讀者可以自己思考一下,結合安全等方面,😆筆者暫時不給出答案。
3. auth整合
auth服務的整合修改,其實沒那么多,之前對於user、role以及permission之間的定義和關系沒有給出實現,這部分的sql語句已經在auth.sql中。所以為了能給出一個完整的實例,筆者把這部分實現給補充了,主要就是user-role,role、role-permission的相應接口定義與實現,實現增刪改查。
讀者要是想參考整合項目進行實際應用,這部分完全可以根據自己的業務進行增強,包括token的創建,其自定義的信息還可以在網關中進行統一處理,構造好之后傳遞給后端服務。
這邊的接口只是列出了需要的幾個,其他接口沒寫(因為懶。。)
這兩個接口也是給backend項目用來獲取相應的userId權限。
1 |
//根據userId獲取用戶對應的權限 |
好了,這邊的實現已經講完了,具體見項目中的實現。
4. backend項目實現
本節是進行實現一個backend的實例,后端項目主要實現哪些功能呢?我們考慮一下,之前網關服務和auth服務所做的准備:
- 網關構造的頭部userId(可能還有其他信息,這邊只是示例),可以在backend獲得
- 轉發到backend服務的請求,都是經過身份合法性校驗,或者是直接對外暴露的接口
- auth服務,提供根據userId進行獲取相應的權限的接口
根據這些,筆者繪制了一個backend的通用流程圖:
上面的流程圖其實已經非常清晰了,首先經過filter過濾器,填充SecurityContextHolder
的上下文。其次,通過切面來實現注解,是否需要進入切面表達式處理。不需要的話,直接執行接口內的方法;否則解析注解中需要的權限,判斷是否有權限執行,有的話繼續執行,否則返回403 forbidden。
4.1 filter過濾器
Filter過濾器,和上面網關使用一樣,攔截客戶的HttpServletRequest。
1 |
public class AuthorizationFilter implements Filter { |
上述代碼主要實現了,根據請求頭中的userId,利用feign client獲取auth服務中的該user所具有的權限集合。之后構造了一個UserContext,UserContext是自定義的,實現了Spring Security的UserDetails, SecurityContext
接口。
4.2 通過切面來實現@PreAuth注解
基於Spring的項目,使用Spring的AOP切面實現注解是比較方便的一件事,這邊我們使用了自定義的注解@PreAuth
1 |
|
Target用於描述注解的使用范圍,超出范圍時編譯失敗,可以用在方法或者類上面。在運行時生效。不了解注解相關知識的,可以自行Google。
1 |
|
因為Aspect作用在bean上,所以先用Component把這個類添加到容器中。@Pointcut
定義要攔截的注解。@Around
定制一個環繞通知,當想獲得注解里面的屬性,可以直接注入該注解。切面表達式內主要實現了,利用Spring EL對value進行解析,將SecurityContextHolder.getContext()
轉換成標准的操作上下文,然后解析注解中的表達式,最后獲取對表達式判斷的結果。
1 |
public class CustomerSecurityExpressionRoot extends SecurityExpressionRoot { |
CustomerSecurityExpressionRoot
繼承的是抽象類SecurityExpressionRoot
,而我們用到的實際表達式是定義在SecurityExpressionOperations
接口,SecurityExpressionRoot
又實現了SecurityExpressionOperations
接口。不過這里面的具體判斷實現,Spring Security 調用的也是Spring EL。
4.3 controller接口
下面我們看看最終接口是怎么用上面實現的注解。
1 |
|
@PreAuth
中,可以定義的表達式很多,可以看SecurityExpressionOperations
接口中的方法。目前筆者只是實現了hasAuthority()
表達式,如果你想支持其他所有表達式,只需要構造相應的SecurityContextHolder
即可。
4.4 為什么這樣設計?
有些讀者看了上面的設計,既然好多用到了Spring Security的工具類,肯定會問,為什么要引入這么復雜的工具類?
其實很簡單,首先因為SecurityExpressionOperations
接口中定義的表達式足夠多,且較為合理,能夠覆蓋我們在平時用到的大部分場景;其次,筆者之前的設計是直接在注解中指定所需權限,沒有擴展性,且可讀性差;最后,Spring Security 4 確實引入了@PreAuthorize,@PostAuthorize
等注解,本來想用來着,自己嘗試了一下,發現對於微服務架構這樣的接口級別的操作權限校驗不是很適合,十多個過濾器太過復雜,而且還涉及到的Principal、Credentials等信息,這些已經在auth系統實現了身份合法性校驗。筆者認為這邊的功能實現並不是很復雜,需要很輕量的實現,讀者有興趣可以試着這部分的實現封裝成jar包或者Spring Boot的starter。
4.5 后期優化
優化的地方主要有兩點:
- 現在的設計是,每次請求過來都會去調用auth服務獲取該user相應的權限信息。而后端微服務數量有很多,沒必要每個服務,或者說一個服務的多個服務實例,每次都去調用auth服務,筆者認為完全可以引入redis集群的緩存機制,在請求到達一個服務的某個實例時,首先去查詢對應的user的緩存中的權限,如果沒有再調用auth服務,最后寫入redis緩存。當然,如果權限更新了,在auth服務肯定要delete相應的user權限緩存。
- 關於被拒絕的請求,在切面表達式中,直接返回了對象,筆者認為可以和response status 403進行綁定,定制返回對象的內容,返回的response更加友好。
5. 總結
如上,首先講了整合的設計思路,主要包含三個服務:gateway、auth和backend demo。整合的項目,總體比較復雜,其中gateway服務擴充了好多內容,對於暴露的接口進行路由轉發,這邊引入了Spring Security 的starter,配置資源服務器對暴露的路徑進行放行;對於其他接口需要調用auth服務進行身份合法性校驗,保證到達backend的請求都是合法的或者公開的接口;auth服務在之前的基礎上,補充了role、permission、user相應的接口,供外部調用;backend demo是新起的服務,實現了接口級別的操作權限的校驗,主要用到了自定義注解和Spring AOP切面。
由於實現的細節實在有點多,本文限於篇幅,只對部分重要的實現進行列出與講解。如果讀者有興趣實際的應用,可以根據實際的業務進行擴增一些信息,如auth授權的token、網關攔截請求構造的頭部信息、注解支持的表達式等等。
可以優化的地方當然還有很多,整合項目中設計不合理的地方,各位同學可以多多提意見。