spring boot / cloud (十四) 微服務間遠程服務調用的認證和鑒權的思考和設計,以及restFul風格的url匹配攔截方法
前言
本篇接着<spring boot / cloud (八) 使用RestTemplate來構建遠程調用服務>這篇博客來繼續討論微服務間接口調用的認證和鑒權的思考和設計
在上一篇文章中主要是偏實現方面,具體的實現思想沒有過多討論,本篇文章則是主要討論一下設計的思路.
我們都知道,在微服務的架構設計中,一個大的系統會被按照不同的領域拆分成一個個小的微服務,而這些微服務之間不可避免的會有業務數據的交互,
那么我們會用一些遠程服務調用的方式來連接各個微服務( 比如:RPC,RestFul,等 ).
不過,就像前面說的一樣,期初,應用不大,隨便弄弄無所謂,但是等應用規模起來以后,你會發現,有成百上千的服務在運行,這些服務相互依賴,仿佛一團亂麻.
更可怕的事情是如果沒有有效的權限控制,我們很有可能都不清楚是誰調用了你的服務......
所以說,我認為,在微服務架構設計中,內部服務調用的權限控制是非常必要的(至少我參與的項目都有這種需求),它應該滿足如下幾個主要的功能:
-
防止越權行為
-
管理服務的依賴關系
-
規范服務調用行為
-
能夠在運行時修改權限配置
下面我們來看看具體的分析:
場景分析
防止越權行為
在系統中添加權限相關的控制,主要是為了增加系統的安全性,總結下來主要是為了防止如下的兩種越權行為:
-
橫向越權 (指的是攻擊者嘗試訪問與他擁有相同權限的用戶的資源)
-
縱向越權 (指的是一個低級別攻擊者嘗試訪問高級別用戶的資源)
所以說通常,在系統中的權限校驗也按照以上划分會分為兩個步驟:
-
校驗訪問者身份
-
校驗是否擁有被訪問資源的權限
校驗訪問者身份
先說校驗訪問者身份,這個主要目的就是確定A的確是A,不是其他的阿貓阿狗.
要做到這一點並不難,並且有很多安全框架都能支持,比如apache shiro,spring security,jwt,等等.
這些框架主要思路還是使用token簽名的方式,也就是說,
要么調用方和服務方約定一個私鑰,然后調用方自行通過算法生成token,
或者服務方提供一個獲取token的接口(OAuth2),調用方主動調用接口獲取token,
最后調用方在調用服務的時候,都把這個token給帶上,便於服務方認證身份.
那么那種方式更好呢? 個人經驗我會按場景做如下架構原則定義:
-
如果服務是給系統用的,則采用私鑰的方式
-
如果服務是給用戶(人)使用的,則采用獲取token的方式(通過用戶名和密碼來獲取token)
那么,還有一種情況,如果這個接口既是給人使用的,也是給第三方系統使用的,怎么辦呢?
這個其實也不復雜,不過不會在今天這篇文章中討論,這里只提一點,通過入口區分,也就是網關,大家可先自行腦洞.
那么,回過頭來看,我們今天討論的場景顯然是屬於是給系統使用的服務,所以在我設計的RMS組件中,是采用的私鑰的方式.
采用這種身份認證方式需要注意如下幾點:
-
私鑰的安全性
-
token的過期策略
-
token的計算算法
在RMS組件中,我並沒有引入第三方的依賴,因為我希望,這個身份認證是輕量級的,靈活的,這些第三方認證框架大而全,很優秀,但我們只會用到其中的一小塊,會造成一些沒必要的依賴.
從實現方面,首先,所有的私鑰,都會配置到遠端的配置中心里面,本地不做任何存儲,由專門的人員管理和維護,系統只有在運行的時候,才能獲取到私鑰.
同時依賴於spring cloud config server的特性,可以在運行時更換私鑰,更加靈活,也保證了的私鑰的安全,
如下的sign(token)的算法,通過應用名稱和私鑰,要有當前時間(精確到小時),拼接起來然后進行md5,得到最終的sign,因為加入了時間的這個因子,所以計算出來的sign是每小時過期的
算法方面大家可以隨意設計,但是切記,不要過度設計,滿足需求即可
public static String sign(String rmsApplicationName, String secret) {
final String split = "_";
StringBuilder sb = new StringBuilder();
sb.append(rmsApplicationName).append(split).append(secret).append(split)
.append(new SimpleDateFormat(DATA_FORMAT).format(new Date()));
return DigestUtils.md5Hex(sb.toString());
}
校驗是否擁有被訪問資源的權限
然后我們再聊校驗是否擁有被訪問資源的權限,這個點說簡單也簡單,說復雜也非常復雜.
在前面一步的校驗中,已經確定了身份,現在是要確定,A是否有訪問B的/user服務的權限.
其他的不說,我這邊只提兩點:
-
uri匹配
-
性能
在沒有RestFul風格的url的時候,一切其實都還蠻美好的,因為,url就是唯一值,是整個系統的最小顆粒度的權限點.
大家以前可能是這樣做的,有張表,記錄這系統的url,以及其他的角色,崗位等的關聯,然后,如何校驗呢,非常簡單,
直接的sql語句select count(1) form xxx where url='/aaa/getUserByName'就行了,能查到值就代表有權限.
在稍微進階一點的,會考慮性能問題,會將某個用戶的一些權限緩存起來,然后在內存中進行判斷.
但是,當RestFul風格的url到來的時候,這一切變得不那么美好了,先看如下幾個url的例子:
GET /users -- 查詢用戶列表
GET /user/{id} -- 查詢用戶詳情
POST /user -- 新增用戶
PUT /user --更新用戶
DELETE /user/{id} --刪除用戶
GET /user/{id}/scores -- 查詢某個用戶的所有成績
GET /user/{id}/score/{sid} -- 查詢某個用戶的某門課程的成績
我們可以看到,按照原有的方式已經不那么使用了,url的定義從原來的平面化的,變成了立體化的,
按照原來的方式,那么就變成了,如果擁有查詢用戶詳情接口權限的系統,同時也就擁有了更新用戶和刪除用戶的權限,這是非常嚴重的越權行為.這顯然不是我們期望看到的.
那么如何優化呢? 首先,我們的權限判斷中應該加入httpmethod的判斷,這樣,就能很簡單的避免以上的情況.
但是更嚴重的問題來了,url不再是固定不變的了,而是動態的,怎么辦呢?先拍腦子想想,處理方案可能有如下幾種:
-
正則匹配
-
將所有url解析成樹形結構,將動態部分用星號表示,然后進行最短匹配
以上兩種方案我都試過,不過方案都過於復雜,甚至存在性能問題,因為以上兩種方式都不可不免會進行循環匹配.
我們當然不想因為一個url校驗,而引入一個性能問題的風險,那么如何解決這個問題呢?
其實我們回過頭來想想,spring mvc為什么就能准確的定位到每個url對應的handler呢?
其實還是那句話,最復雜的部分,spring已經幫我們完成了,在spring 上下文初始化的時候,容器就會記錄所有的mapping,如下:
Mapped "{[/health || /health.json],methods=[GET],produces=[application/json || application/json]}" onto HealthMvcEndpoint.invoke(HttpServletRequest,Principal)
Mapped "{[/env/{name:.*}],methods=[GET],produces=[application/json || application/json]}" onto EnvironmentMvcEndpoint.value(String)
Mapped "{[/env || /env.json],methods=[GET],produces=[application/json || application/json]}" onto EndpointMvcAdapter.invoke()
Mapped "{[/features || /features.json],methods=[GET],produces=[application/json || application/json]}" onto EndpointMvcAdapter.invoke()
以上日志大家隨便啟動一個spring boot應用都能看到,其實這類輸出就是我們controller定義的requestMapping
@RequestMapping(value = "/user/{id}/score/{sid}", method = RequestMethod.GET)
而我們平時獲取url的方式是這樣的:
String url = request.getRequestURI();
這樣獲取到的url 大概是,也就是我們實際請求的url:
/user/1/score/5
那么如何改變呢?以上這個url我們還是不知道如何匹配? 換個思路想想,能進入攔擊器,則表示spring將這個地址已經匹配到了對應的handler,我們只需找到這個url對應的那個handler就行了.
在request的作用域中(Attribute),存放這很多spring的信息,debug一下,打個斷點,看看都有啥?,最終我定位到了bestMatchingPattern這個屬性,大致含義就是最佳匹配模式,也里面的值是requesMapping里的value
這不正是我們想要的結果嗎?spring已經幫我們做了最佳的替換,如下代碼:
String url = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE).toString();
//url的值為 : /user/{id}/score/{sid}
那么后續我們就可以調整我們的設計了,如果你的權限配置是配置在數據庫中的那么,最簡單的鑒權sql語句就是:
select count(1) form xxx where url='/user/{id}/score/{sid}' method='GET'
這樣做的好處就是可以做到最精確的匹配,顆粒度達到最細,並且毫無性能損耗,你無需做任何的循環匹配.詳細代碼可以在項目的RmsAuthHandlerInterceptor類中找到
其他
在上面還提到了 管理服務的依賴關系 , 規范服務調用行為 , 能夠在運行時修改權限配置 這幾點 ,
在上一篇講解RMS代碼的時候也都提及到了,會通過遠程配置文件的方式,來進行管理,如下:
# application
org.itkk.rms.properties.application.udf-service-a-demo.serviceId=UDF-SERVICE-A-DEMO
org.itkk.rms.properties.application.udf-service-a-demo.secret=ASD5S2SDF6ASD2S2SD32S
org.itkk.rms.properties.application.udf-service-a-demo.purview=ID_2,SCHEDULER_JOB_4,SCH_CLIENT_CALLBACK_1
org.itkk.rms.properties.application.udf-service-a-demo.all=false
org.itkk.rms.properties.application.udf-service-a-demo.disabled=false
org.itkk.rms.properties.application.udf-service-a-demo.description=測試服務A
# service
org.itkk.rms.properties.service.ID_1.owner=udf-general-server-demo
org.itkk.rms.properties.service.ID_1.uri=/service/id
org.itkk.rms.properties.service.ID_1.method=GET
org.itkk.rms.properties.service.ID_1.isHttps=false
org.itkk.rms.properties.service.ID_1.description=獲得分布式ID
會分為application和service兩類,application主要描述身份認證和權限,service主要描述服務詳情 . 通過這種結構來管理服務間的依賴關系
然后在項目中,大家可以看Rms這個類,里面抽象出了一個公共的方法,用於規范調用行為,最終調用的方式如下:
ResponseEntity<RestResponse<FileInfo>> fileInfo = rms.call("FILE_4", fileParam, null,
new ParameterizedTypeReference<RestResponse<FileInfo>>() {
}, null);
在系統中,開發人員都無需關心任何接口的定義,只需通過接口編號就可以進行調用.
最后,所有的RMS相關的配置都會放在配置中心,同一管理.
結束
今天代碼層面的東西講的比較少,主要是跟大家介紹一下設計的思路,還是一個原則,使代碼更健壯,更靈活,更合理,同時,也切記不要重復造輪子,也不要過度玩技術.
在下一篇文章中,我會介紹一下分布式任務調度的思考和設計,敬請期待.
代碼倉庫 (博客配套代碼)
想獲得最快更新,請關注公眾號