微服務中的鑑權該怎麼做?

語言: CN / TW / HK

最近剛好有小夥伴在微信上問到這個問題,松哥就來和大家聊一聊,本文主要和小夥伴們聊一聊思路,不寫程式碼,小夥伴們可以結合松哥之前的文章,應該能夠自己寫出來本文的程式碼。當然,思路也只是我自己的一點實踐經驗,不一定是最完美的方案,歡迎小夥伴們在留言中一起探討。

1. 認證與授權

首先小夥伴們知道,無論我們學習 Shiro 還是 Spring Security,裡邊的功能無論有哪些,核心都是兩個:

  1. 認證
  2. 授權

所以,我們在微服務中處理鑑權問題,也可以從這兩個方面來考慮。

1.1 認證

認證,說白了就是登入。傳統的 Web 登入是 Cookie+Session 的方案,這種方案依賴於伺服器本地記憶體,在微服務中,由於服務眾多,這種方案顯然不再合適。

可能會有小夥伴說用 Redis+SpringSession 做 Session 共享,這是個辦法,但是不是最佳方案,因為這種方案的效能以及可擴充套件性都比較差。

所以,微服務中的認證,還是建議使用令牌的方式,可以選擇 JWT 令牌,這也是目前使用較多的一種方案。但是熟悉 JWT 的小夥伴都知道,純粹的無狀態登入無法實現登出,這就很頭大,所以在實際應用中,單純的使用 JWT 是不行的,一般還是要結合 Redis 一起,將生成的 JWT 字串在 Redis 上也儲存一份,並設定過期時間,判斷使用者是否登入時,需要先去 Redis 上檢視 JWT 字串是否存在,存在的話再對 JWT 字串做解析操作,如果能成功解析,就沒問題,如果不能成功解析,就說明令牌不合法。

這樣有狀態登入+無狀態登入混在一起的方式,雖然看起來有點不倫不類,但是就當下來說,這個折衷的辦法算是一個可行的方案了。

其實,上面的方案,說白了,跟傳統的 Cookie+Session 沒什麼兩樣,思路幾乎都是完全 copy 的:傳統的 Session 用 Redis 代替了;傳統穿梭於服務端和瀏覽器之間的 jsessionId 被 JWT 字串代替了;傳統的 jsessionId 通過 Cookie 來傳輸,現在的 JWT 則通過開發者手動設定後通過請求頭來傳輸;傳統的 Session 可以自動續簽,現在用 JWT 就是手動續簽,每次請求到達服務端的時候,就去看下 Redis 上令牌的過期時間,快過期了,就重新設定一下,其他都一模一樣。

這是認證方案的選擇。

1.2 授權

微服務中授權,也可以使用 Shiro 或者 Spring Security 框架來做,省事一些。考慮到微服務技術棧都是 Spring 家族的產品,所以在許可權框架這塊也是建議大家首選 Spring Security(如果有小夥伴對 Spring Security 還不熟悉的話,可以在微信公眾號後臺回覆 ss,有教程)。

當然,如果覺得 Spring Security 比較複雜想自己搞的話,也是可以的。自己搞的話,也是可以藉助於 Spring Security 的思路的,松哥最近的一個專案就是這樣:

請求到達微服務之後,先找到當前使用者的各種資訊,包括當前使用者所擁有的角色和許可權等資訊,然後存入到和當前執行緒繫結的 ThreadLocal 物件中。另一方面自定義許可權註解和角色註解,在切面中對這些註解進行解析,檢查當前使用者是否具備所需要的角色/許可權等。

當然,如果你使用了 Spring Security 的話,上面這個就不需要自定義註解了,直接使用 Spring Security 中自帶的即可,還可以體驗 Spring Security 中更多的豐富的安全功能。

2. 認證服務

那麼認證和授權在哪裡做?

先來說認證,認證我們可以簡單分為兩個步驟:

  1. 登入
  2. 校驗

2.1 登入

一般來說,登入我們可以單獨做一個認證服務。當登入請求到達閘道器之後,我們將之轉發到認證服務上,完成認證操作。

在認證服務上,我們就去檢查使用者名稱/密碼是否 OK,使用者狀態是否都 OK,都沒問題的話,生成 JWT 字串,同時再把資料存入到 Redis 上,然後把 JWT 字串返回。

如果系統有註冊功能的話,註冊功能也是放在這個微服務上來完成。

2.2 校驗

校驗是指每一個請求到達的時候,校驗使用者是否已經登入。

這個當然可以和 2.1 放到一起去做,但是松哥不建議。問題在於,假如是一個建立訂單的請求,這個請求原本是要經過閘道器轉發到訂單服務上的,但是,此時就得先在閘道器上呼叫 2.1 小節的服務進行登入校驗,沒問題再轉發到訂單服務上,這樣做很明顯很費事,也不合理。

一個比較好的辦法是直接在閘道器上去校驗請求的令牌是否合法,這個校驗本身也比較容易,校驗令牌是否合法,我們只需要看 Redis 上是否存在這個令牌,並且這個 JWT 令牌能夠被順利解析就行,這個操作完全可以在閘道器上做。

以 Gateway 閘道器為例,我們可以自定義全域性過濾器,在全域性過濾器中校驗每一個請求的令牌,校驗通過了,再進行請求的轉發,否則就不轉發。

校驗通過之後,在轉發到具體的微服務之後,我們可以將解析出來的使用者 id 以及使用者名稱等資訊放到請求頭中,然後再轉發,這樣到達各個具體的微服務之後,就知道這個請求是誰發來的,這人都有哪些角色/許可權,方便做下一步的許可權校驗。

松哥的做法是定義了一個公共模組,所有的微服務都依賴這個公共模組,這個公共模組中定義了一個攔截器,會攔截下來每一個請求,從請求頭中取出使用者 ID,然後從 Redis 中拿到具體的使用者資訊,存入到 ThreadLocal 中,這樣在後續的方法呼叫中,如果需要判斷使用者是否具備某一個許可權,就可以通過 ThreadLocal 去獲取了。

大致上就是這樣一個流程。

3. 授權服務

授權沒法放到閘道器上做,還是得在各個微服務上去完成。

微服務上的授權我們又可以將之大致上分為兩類:

  1. 前端傳送來的請求(外部請求)。
  2. 別的微服務傳送來的請求(內部請求)。

3.1 外部請求

對於外部請求來說,就按正常的許可權校驗對待就行了,自定義註解亦或者使用 Spring Security 等框架都是可以的,如果是自定義註解的話,就結合 AOP 一起,定義切面自己去處理許可權註解,當然,這些功能基本上每一個微服務都是需要的,所以可以將之抽取成為一個公共的模組,在不同的微服務中依賴即可。

3.2 內部請求

對於內部的請求來說,正常是不需要鑑權的,內部請求可以直接處理。問題是如果使用了 OpenFeign,資料都是通過介面暴露出去的,不鑑權的話,又會擔心從外部來的請求呼叫這個介面,對於這個問題,我們也可以自定義註解+AOP,然後在內部請求呼叫的時候,額外加一個頭欄位加以區分。

當然,內部請求到達微服務的時候,也是需要進行認證的,就行請求從閘道器轉發到每一個具體的微服務上時需要認證一樣,不過很明顯,我們沒必要每次使用 OpenFeign 呼叫別的服務的時候,都去傳一堆認證資訊,我們可以通過實現 feign.RequestInterceptor 介面來定義一個 OpenFeign 的請求攔截器,在攔截器中,統一為 OpenFeign 請求設定請求頭資訊。

好啦,關於微服務中的鑑權,我們目前是這麼做的,歡迎小夥伴們留言一起探討。