微服務(wù)中的鑒權(quán)該怎么做?
最近剛好有小伙伴在微信上問到這個問題,松哥就來和大家聊一聊,本文主要和小伙伴們聊一聊思路,不寫代碼,小伙伴們可以結(jié)合松哥之前的文章,應(yīng)該能夠自己寫出來本文的代碼。當(dāng)然,思路也只是我自己的一點(diǎn)實(shí)踐經(jīng)驗(yàn),不一定是最完美的方案,歡迎小伙伴們在留言中一起探討。
1. 認(rèn)證與授權(quán)
首先小伙伴們知道,無論我們學(xué)習(xí) Shiro 還是 Spring Security,里邊的功能無論有哪些,核心都是兩個:
- 認(rèn)證
- 授權(quán)
所以,我們在微服務(wù)中處理鑒權(quán)問題,也可以從這兩個方面來考慮。
1.1 認(rèn)證
認(rèn)證,說白了就是登錄。傳統(tǒng)的 Web 登錄是 Cookie+Session 的方案,這種方案依賴于服務(wù)器本地內(nèi)存,在微服務(wù)中,由于服務(wù)眾多,這種方案顯然不再合適。
可能會有小伙伴說用 Redis+SpringSession 做 Session 共享,這是個辦法,但是不是最佳方案,因?yàn)檫@種方案的性能以及可擴(kuò)展性都比較差。
所以,微服務(wù)中的認(rèn)證,還是建議使用令牌的方式,可以選擇 JWT 令牌,這也是目前使用較多的一種方案。但是熟悉 JWT 的小伙伴都知道,純粹的無狀態(tài)登錄無法實(shí)現(xiàn)注銷,這就很頭大,所以在實(shí)際應(yīng)用中,單純的使用 JWT 是不行的,一般還是要結(jié)合 Redis 一起,將生成的 JWT 字符串在 Redis 上也保存一份,并設(shè)置過期時間,判斷用戶是否登錄時,需要先去 Redis 上查看 JWT 字符串是否存在,存在的話再對 JWT 字符串做解析操作,如果能成功解析,就沒問題,如果不能成功解析,就說明令牌不合法。
這樣有狀態(tài)登錄+無狀態(tài)登錄混在一起的方式,雖然看起來有點(diǎn)不倫不類,但是就當(dāng)下來說,這個折衷的辦法算是一個可行的方案了。
其實(shí),上面的方案,說白了,跟傳統(tǒng)的 Cookie+Session 沒什么兩樣,思路幾乎都是完全 copy 的:傳統(tǒng)的 Session 用 Redis 代替了;傳統(tǒng)穿梭于服務(wù)端和瀏覽器之間的 jsessionId 被 JWT 字符串代替了;傳統(tǒng)的 jsessionId 通過 Cookie 來傳輸,現(xiàn)在的 JWT 則通過開發(fā)者手動設(shè)置后通過請求頭來傳輸;傳統(tǒng)的 Session 可以自動續(xù)簽,現(xiàn)在用 JWT 就是手動續(xù)簽,每次請求到達(dá)服務(wù)端的時候,就去看下 Redis 上令牌的過期時間,快過期了,就重新設(shè)置一下,其他都一模一樣。
這是認(rèn)證方案的選擇。
1.2 授權(quán)
微服務(wù)中授權(quán),也可以使用 Shiro 或者 Spring Security 框架來做,省事一些??紤]到微服務(wù)技術(shù)棧都是 Spring 家族的產(chǎn)品,所以在權(quán)限框架這塊也是建議大家首選 Spring Security(如果有小伙伴對 Spring Security 還不熟悉的話,可以在微信公眾號后臺回復(fù) ss,有教程)。
當(dāng)然,如果覺得 Spring Security 比較復(fù)雜想自己搞的話,也是可以的。自己搞的話,也是可以借助于 Spring Security 的思路的,松哥最近的一個項(xiàng)目就是這樣:
請求到達(dá)微服務(wù)之后,先找到當(dāng)前用戶的各種信息,包括當(dāng)前用戶所擁有的角色和權(quán)限等信息,然后存入到和當(dāng)前線程綁定的 ThreadLocal 對象中。另一方面自定義權(quán)限注解和角色注解,在切面中對這些注解進(jìn)行解析,檢查當(dāng)前用戶是否具備所需要的角色/權(quán)限等。
當(dāng)然,如果你使用了 Spring Security 的話,上面這個就不需要自定義注解了,直接使用 Spring Security 中自帶的即可,還可以體驗(yàn) Spring Security 中更多的豐富的安全功能。
2. 認(rèn)證服務(wù)
那么認(rèn)證和授權(quán)在哪里做?
先來說認(rèn)證,認(rèn)證我們可以簡單分為兩個步驟:
- 登錄
- 校驗(yàn)
2.1 登錄
一般來說,登錄我們可以單獨(dú)做一個認(rèn)證服務(wù)。當(dāng)?shù)卿浾埱蟮竭_(dá)網(wǎng)關(guān)之后,我們將之轉(zhuǎn)發(fā)到認(rèn)證服務(wù)上,完成認(rèn)證操作。
在認(rèn)證服務(wù)上,我們就去檢查用戶名/密碼是否 OK,用戶狀態(tài)是否都 OK,都沒問題的話,生成 JWT 字符串,同時再把數(shù)據(jù)存入到 Redis 上,然后把 JWT 字符串返回。
如果系統(tǒng)有注冊功能的話,注冊功能也是放在這個微服務(wù)上來完成。
2.2 校驗(yàn)
校驗(yàn)是指每一個請求到達(dá)的時候,校驗(yàn)用戶是否已經(jīng)登錄。
這個當(dāng)然可以和 2.1 放到一起去做,但是松哥不建議。問題在于,假如是一個創(chuàng)建訂單的請求,這個請求原本是要經(jīng)過網(wǎng)關(guān)轉(zhuǎn)發(fā)到訂單服務(wù)上的,但是,此時就得先在網(wǎng)關(guān)上調(diào)用 2.1 小節(jié)的服務(wù)進(jìn)行登錄校驗(yàn),沒問題再轉(zhuǎn)發(fā)到訂單服務(wù)上,這樣做很明顯很費(fèi)事,也不合理。
一個比較好的辦法是直接在網(wǎng)關(guān)上去校驗(yàn)請求的令牌是否合法,這個校驗(yàn)本身也比較容易,校驗(yàn)令牌是否合法,我們只需要看 Redis 上是否存在這個令牌,并且這個 JWT 令牌能夠被順利解析就行,這個操作完全可以在網(wǎng)關(guān)上做。
以 Gateway 網(wǎng)關(guān)為例,我們可以自定義全局過濾器,在全局過濾器中校驗(yàn)每一個請求的令牌,校驗(yàn)通過了,再進(jìn)行請求的轉(zhuǎn)發(fā),否則就不轉(zhuǎn)發(fā)。
校驗(yàn)通過之后,在轉(zhuǎn)發(fā)到具體的微服務(wù)之后,我們可以將解析出來的用戶 id 以及用戶名等信息放到請求頭中,然后再轉(zhuǎn)發(fā),這樣到達(dá)各個具體的微服務(wù)之后,就知道這個請求是誰發(fā)來的,這人都有哪些角色/權(quán)限,方便做下一步的權(quán)限校驗(yàn)。
松哥的做法是定義了一個公共模塊,所有的微服務(wù)都依賴這個公共模塊,這個公共模塊中定義了一個攔截器,會攔截下來每一個請求,從請求頭中取出用戶 ID,然后從 Redis 中拿到具體的用戶信息,存入到 ThreadLocal 中,這樣在后續(xù)的方法調(diào)用中,如果需要判斷用戶是否具備某一個權(quán)限,就可以通過 ThreadLocal 去獲取了。
大致上就是這樣一個流程。
3. 授權(quán)服務(wù)
授權(quán)沒法放到網(wǎng)關(guān)上做,還是得在各個微服務(wù)上去完成。
微服務(wù)上的授權(quán)我們又可以將之大致上分為兩類:
前端發(fā)送來的請求(外部請求)。
別的微服務(wù)發(fā)送來的請求(內(nèi)部請求)。
3.1 外部請求
對于外部請求來說,就按正常的權(quán)限校驗(yàn)對待就行了,自定義注解亦或者使用 Spring Security 等框架都是可以的,如果是自定義注解的話,就結(jié)合 AOP 一起,定義切面自己去處理權(quán)限注解,當(dāng)然,這些功能基本上每一個微服務(wù)都是需要的,所以可以將之抽取成為一個公共的模塊,在不同的微服務(wù)中依賴即可。
3.2 內(nèi)部請求
對于內(nèi)部的請求來說,正常是不需要鑒權(quán)的,內(nèi)部請求可以直接處理。問題是如果使用了 OpenFeign,數(shù)據(jù)都是通過接口暴露出去的,不鑒權(quán)的話,又會擔(dān)心從外部來的請求調(diào)用這個接口,對于這個問題,我們也可以自定義注解+AOP,然后在內(nèi)部請求調(diào)用的時候,額外加一個頭字段加以區(qū)分。
當(dāng)然,內(nèi)部請求到達(dá)微服務(wù)的時候,也是需要進(jìn)行認(rèn)證的,就行請求從網(wǎng)關(guān)轉(zhuǎn)發(fā)到每一個具體的微服務(wù)上時需要認(rèn)證一樣,不過很明顯,我們沒必要每次使用 OpenFeign 調(diào)用別的服務(wù)的時候,都去傳一堆認(rèn)證信息,我們可以通過實(shí)現(xiàn) feign.RequestInterceptor 接口來定義一個 OpenFeign 的請求攔截器,在攔截器中,統(tǒng)一為 OpenFeign 請求設(shè)置請求頭信息。
好啦,關(guān)于微服務(wù)中的鑒權(quán),我們目前是這么做的,歡迎小伙伴們留言一起探討。


























