如何構建安全的微服務應用?
目錄
前言
微服務架構的引入為軟體應用帶來了諸多好處:包括小開發團隊,縮短開發週期,語言選擇靈活性,增強服務伸縮能力等。與此同時,也引入了分散式系統的諸多複雜問題。其中一個挑戰就是如何在微服務架構中實現一個靈活,安全,高效的認證和鑑權方案。本文將嘗試就此問題進行一次比較完整的探討。
單體應用的實現方式
在單體架構下,整個應用是一個程序,在應用中,一般會用一個安全模組來實現使用者認證和鑑權。 使用者登入時,應用的安全模組對使用者身份進行驗證,驗證使用者身份合法後,為該使用者生成一個會話(Session),併為該Session關聯一個唯一的編號(Session Id)。Session是應用中的一小塊記憶體結構,其中儲存了登入使用者的資訊,如User name, Role, Permission等。伺服器把該Session的Session Id返回給客戶端,客戶端將Session Id以cookie或者URL重寫的方式記錄下來,並在後續請求中傳送給應用,這樣應用在接收到客戶端訪問請求時可以使用Session Id驗證使用者身份,不用每次請求時都輸入使用者名稱和密碼進行身份驗證。
備註:為了避免Session Id被第三者擷取和盜用,客戶端和應用之前應使用TLS加密通訊,session也會設定有過期時間。
客戶端訪問應用時,Session Id隨著HTTP請求傳送到應用,客戶端請求一般會通過一個攔截器處理所有收到的客戶端請求。攔截器首先判斷Session Id是否存在,如果該Session Id存在,就知道該使用者已經登入。然後再通過查詢使用者許可權判斷使用者能否執行該此請求,以實現操作鑑權。
微服務認證和鑑權面臨的問題
在微服務架構下,一個應用被拆分為多個微服務程序,每個微服務實現原來單體應用中一個模組的業務功能。應用拆分後,對每個微服務的訪問請求都需要進行認證和鑑權。如果參考單體應用的實現方式會遇到下述問題:
- 認證和鑑權邏輯需要在每個微服務中進行處理,需要在各個微服務中重複實現這部分公共邏輯。雖然我們可以使用程式碼庫複用部分程式碼,但這又會導致所有微服務對特定程式碼庫及其版本存在依賴,影響微服務語言/框架選擇的靈活性。
- 微服務應遵循單一職責原理,一個微服務只處理單一的業務邏輯。認證和鑑權的公共邏輯不應該放到微服務實現中。
- 為了充分利用微服務架構的好處,實現微服務的水平擴充套件(Scalability)和彈性(Resiliency),微服務最好是無狀態的。因此不建議使用session這種有狀態的方案。
- 微服務架構下的認證和鑑權涉及到場景更為複雜,涉及到使用者訪問微服務應用,第三方應用訪問微服務應用,應用內多個微服務之間相互訪問等多種場景,每種場景下的認證和鑑權方案都需要考慮到,以保證應用程式的安全性。
微服務認證和鑑權的技術方案
使用者身份認證
一個完整的微服務應用是由多個相互獨立的微服務程序組成的,對每個微服務的訪問都需要進行使用者認證。如果將使用者認證的工作放到每個微服務中,應用的認證邏輯將會非常複雜。因此需要考慮一個SSO(單點登入)的方案,即使用者只需要登入一次,就可以訪問所有微服務提供的服務。 由於在微服務架構中以API Gateway作為對外提供服務的入口,因此可以考慮在API Gateway處提供統一的使用者認證。
使用者狀態保持
HTTP是一個無狀態的協議,對伺服器來說,使用者的每次HTTP請求是相互獨立的。網際網路是一個巨大的分散式系統,HTTP協議作為網際網路上的一個重要協議,要考慮到大量應用訪問的效率問題。無狀態意味著服務端可以把客戶端的請求根據需要傳送到叢集中的任何一個節點,HTTP的無狀態設計對負載均衡有明顯的好處,由於沒有狀態,使用者請求可以被分發到任意一個伺服器,應用也可以在靠近使用者的網路邊緣部署快取伺服器。對於不需要身份認證的服務,例如瀏覽新聞網頁等,這是沒有任何問題的。但很多服務如網路購物,企業管理系統等都需要對使用者的身份進行認證,因此需要在HTTP協議基礎上採用一種方式儲存使用者的登入狀態,避免使用者每發起一次請求都需要進行驗證。
傳統方式是在伺服器端採用Cookie來儲存使用者狀態,由於在伺服器是有狀態的,對伺服器的水平擴充套件有影響。在微服務架構下建議採用Token來記錄使用者登入狀態。
Token和Seesion主要的不同點是儲存的地方不同。Session是集中儲存在伺服器中的;而Token是使用者自己持有的,一般以cookie的形式儲存在瀏覽器中。Token中儲存了使用者的身份資訊,每次請求都會發送給伺服器,伺服器因此可以判斷訪問者的身份,並判斷其對請求的資源有沒有訪問許可權。
Token用於表明使用者身份,因此需要對其內容進行加密,避免被請求方或者第三者篡改。JWT(Json Web Token)是一個定義Token格式的開放標準(RFC 7519),定義了Token的內容,加密方式,並提供了各種語言的lib。
JWT Token的結構非常簡單,包括三部分:
- Header
頭部包含型別,為固定值JWT。然後是JWT使用的Hash演算法。{ "alg": "HS256", "typ": "JWT" }
- Payload
包含釋出者,過期時間,使用者名稱等標準資訊,也可以新增使用者角色,使用者自定義的資訊。{ "sub": "1234567890", "name": "John Doe", "admin": true }
- Signature
Token頒發方的簽名,用於客戶端驗證Token頒發方的身份,也用於伺服器防止Token被篡改。 簽名演算法HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
這三部分使用Base64編碼後組合在一起,成為最終返回給客戶端的Token串,每部分之間採用”.”分隔。下圖是上面例子最終形成的Token採用Token進行使用者認證,伺服器端不再儲存使用者狀態,客戶端每次請求時都需要將Token傳送到伺服器端進行身份驗證。Token傳送的方式rfc6750進行了規定,採用一個 Authorization: Bearer HHTP Header進行傳送。
Authorization: Bearer mF_9.B5f-4.1JqM
採用Token方式進行使用者認證的基本流程如下圖所示:
- 使用者輸入使用者名稱,密碼等驗證資訊,向伺服器發起登入請求
- 伺服器端驗證使用者登入資訊,生成JWT token
- 伺服器端將Token返回給客戶端,客戶端儲存在本地(一般以Cookie的方式儲存)
- 客戶端向伺服器端傳送訪問請求,請求中攜帶之前頒發的Token
- 伺服器端驗證Token,確認使用者的身份和對資源的訪問許可權,並進行相應的處理(拒絕或者允許訪問)
實現單點登入
單點登入的理念很簡單,即使用者只需要登入應用一次,就可以訪問應用中所有的微服務。API Gateway提供了客戶端訪問微服務應用的入口,Token實現了無狀態的使用者認證。結合這兩種技術,可以為微服務應用實現一個單點登入方案。
使用者的認證流程和採用Token方式認證的基本流程類似,不同之處是加入了API Gateway作為外部請求的入口。
使用者登入
- 客戶端傳送登入請求到API Gateway
- API Gateway將登入請求轉發到Security Service
- Security Service驗證使用者身份,並頒發Token
使用者請求
- 客戶端請求傳送到API Gateway
- API Gateway呼叫的Security Service對請求中的Token進行驗證,檢查使用者的身份
- 如果請求中沒有Token,Token過期或者Token驗證非法,則拒絕使用者請求。
- Security Service檢查使用者是否具有該操作權
- 如果使用者具有該操作許可權,則把請求傳送到後端的Business Service,否則拒絕使用者請求
使用者許可權控制
使用者許可權控制有兩種做法,在API Gateway處統一處理,或者在各個微服務中單獨處理。
API Gateway處進行統一的許可權控制
客戶端傳送的HTTP請求中包含有請求的Resource及HTTP Method。如果系統遵循REST規範,以URI資源方式對訪問物件進行建模,則API Gateway可以從請求中直接擷取到訪問的資源及需要進行的操作,然後呼叫Security Service進行許可權判斷,根據判斷結果決定使用者是否有許可權對該資源進行操作,並轉發到後端的Business Service。這種實現方式API Gateway處統一處理鑑權邏輯,各個微服務不需要考慮使用者鑑權,只需要處理業務邏輯,簡化了各微服務的實現。
由各個微服務單獨進行許可權控制
如果微服務未嚴格遵循REST規範對訪問物件進行建模,或者應用需要進行定製化的許可權控制,則需要在微服務中單獨對使用者許可權進行判斷和處理。這種情況下微服務的許可權控制更為靈活,但各個微服務需要單獨維護使用者的授權資料,實現更復雜一些。
第三方應用接入
對於第三方應用接入的訪問控制,有兩種實現方式:
API Token
第三方使用一個應用頒發的API Token對應用的資料進行訪問。該Token由使用者在應用中生成,並提供給第三方應用使用。在這種情況下,一般只允許第三方應用訪問該Token所屬使用者自身的資料,而不能訪問其他使用者的敏感私有資料。
例如Github就提供了Personal API Token功能,使用者可以在Github的開發者設定介面中建立Token,然後使用該Token來訪問Github的API。在建立Token時,可以設定該Token可以訪問使用者的哪些資料,如檢視Repo資訊,刪除Repo,檢視使用者資訊,更新使用者資訊等。
使用API Token來訪問Github API
curl -u zhaohuabing:fbdf8e8862252ed0f3ba9dba4e328c01ac93aeec https://api.github.com/user
使用API Token而不是直接使用使用者名稱/密碼來訪問API的好處是降低了使用者密碼暴露的風險,並且可以隨時收回Token的許可權而不用修改密碼。
由於API Token只能訪問指定使用者的資料,因此適合於使用者自己開發一些指令碼或小程式對應用中自己的資料進行操作。
OAuth
某些第三方應用需要訪問不同使用者的資料,或者對多個使用者的資料進行整合處理,則可以考慮採用OAuth。採用OAuth,當第三方應用訪問服務時,應用會提示使用者授權第三方應用相應的訪問許可權,根據使用者的授權操作結果生成用於訪問的Token,以對第三方應用的操作請求進行訪問控制。
同樣以Github為例,一些第三方應用如Travis CI,GitBook等就是通過OAuth和Github進行整合的。 OAuth針對不同場景有不同的認證流程,一個典型的認證流程如下圖所示:
- 使用者向OAuth客戶端程式發起一個請求,OAuth客戶端程式在處理該請求時發現需要訪問使用者在資源伺服器中的資料。
- 客戶端程式將使用者請求重定向到認證伺服器,該請求中包含一個callback的URL。
- 認證伺服器返回授權頁面,要求使用者對OAuth客戶端的資源請求進行授權。
- 使用者對該操作進行授權後,認證伺服器將請求重定向到客戶端程式的callback url,將授權碼返回給客戶端程式。
- 客戶端程式將授權碼傳送給認證伺服器,請求token。
- 認證伺服器驗證授權碼後將token頒發給客戶端程式。
- 客戶端程式採用頒發的token訪問資源,完成使用者請求。
備註:
OAuth中按照功能區分了資源伺服器和認證伺服器這兩個角色,在實現時這兩個角色常常是同一個應用。將該流程圖中的各個角色對應到Github的例子中,資源伺服器和認證伺服器都是Github,客戶端程式是Travis CI或者GitBook,使用者則是使用Travis CI或者GitBook的直接使用者。
有人可能會疑惑在該流程中為何要使用一個授權碼(Authorization Code)來申請Token,而不是由認證伺服器直接返回Token給客戶端。OAuth這樣設計的原因是在重定向到客戶端Callback URL的過程中會經過使用者代理(瀏覽器),如果直接傳遞Token存在被竊取的風險。採用授權碼的方式,申請Token時客戶端直接和認證伺服器進行互動,並且認證服務期在處理客戶端的Token申請請求時還會對客戶端進行身份認證,避免其他人偽造客戶端身份來使用認證碼申請Token。 下面是一個客戶端程式採用Authorization Code來申請Token的示例,client_id和client_secret被用來驗證客戶端的身份。
POST /oauth/token HTTP/1.1 Host: authorization-server.com grant_type=authorization_code &code=xxxxxxxxxxx &redirect_uri=https://example-app.com/redirect &client_id=xxxxxxxxxx &client_secret=xxxxxxxxxx
另外在談及OAuth時,我們需要注意微服務應用作為OAuth客戶端和OAuth伺服器的兩種不同場景:
在實現微服務自身的使用者認證時,也可以採用OAuth將微服務的使用者認證委託給一個第三方的認證服務提供商,例如很多應用都將使用者登入和微信或者QQ的OAuth服務進行了整合。
第三方應用接入和微服務自身使用者認證採用OAuth的目的是不同的,前者是為了將微服務中使用者的私有資料訪問許可權授權給第三方應用,微服務在OAuth架構中是認證和資源伺服器的角色;而後者的目的是整合並利用知名認證提供服務商提供的OAuth認證服務,簡化繁瑣的註冊操作,微服務在OAuth架構中是客戶端的角色。
因此在我們需要區分這兩種不同的場景,以免造成誤解。
微服務之間的認證
除了來自使用者和第三方的北向流量外,微服務之間還有大量的東西向流量,這些流量可能在同一個區域網中,也可能跨越不同的資料中心,這些服務間的流量存在被第三方的嗅探和攻擊的危險,因此也需要進行安全控制。
通過雙向SSL可以實現服務之間的相互身份認證,並通過TLS加密服務間的資料傳輸。需要為每個服務生成一個證書,服務之間通過彼此的證書進行身份驗證。在微服務執行環境中,可能存在大量的微服務例項,並且微服務例項經常會動態變化,例如隨著水平擴充套件增加服務例項。在這種情況下,為每個服務建立並分發證書變得非常困難。我們可以通過建立一個私有的證書中心(Internal PKI/CA)來為各個微服務提供證書管理如頒發、撤銷、更新等。