基於STS和JWT的微服務身份認證
自 Martin Fowler 提出微服務架構的概念後,這個名詞就一直比較流行,總是成為眾多技術論壇和公眾號的討論熱點。很多互聯網和軟件公司都在將原有的整體架構進行拆分,朝著微服務架構的方向進行叠代,而新的項目也幾乎無一例外的成為了實踐微服務架構的場所。
對於大多數有經驗的工程師來說,將傳統的異步函數調用直接改成 REST API 或者某種 RPC 並不是一件很困難的事,要面臨的問題包括序列化,調用延時和版本等。
但服務接口之間的安全和身份認證(Authentication)問題往往比較棘手,而且也是比較敏感的部分。這裏所指的身份認證,既包括用戶的身份,更強調程序和服務的身份,也就是微服務調用之間的基本接口信任關系。
一些項目在進行微服務架構設計的時候,采用私有網絡或者 IP 網段的隔離來保護被拆分的服務,或者配置類似 Kerberos 的認證系統。這樣的做法比較簡便,在進行原有整體架構的遷移時可以盡快上線。
然而從宏觀角度來看,這樣的安全模式有一定的局限性,尤其對底層網絡架構的依賴會帶來不便,容易造成錯誤配置,在某些應用場景下甚至不符合安全規範。如果服務需要暴露給外部網路和互聯網,則尤其要求相對完善成熟的解決方案。
通常意義上認為微服務是 SOA 架構的一個子集,而關於微服務和 SOA 的區別也有很多的討論。隨著雲計算成為主流,容器技術的普及和無服務架構的日趨流行,服務的粒度和和邊界定義變得比較靈活,因而要求上層服務與底層計算及網絡架構有相對低的耦合度。另外企業混合雲的興起也要求各種分布在多個雲裏的服務能協同工作,對於上面的安全模式也是一大挑戰。
本文在這裏暫時模糊一下微服務的邊界和粒度問題,和大家討論一種具有普遍性的解決方案。這種認證模型可以被引入到新搭建的微服務架構中,也能應用於正在重組的整體架構,或者作為服務集群的聯邦化方案,並且不容易受未來總體架構變遷的影響。
令牌和 STS
最直接簡單的身份認證方式是基於用戶名和密碼,包括 App ID 和 App Secret。理論上講,微服務之間可以在調用時候傳遞密碼,讓接收方來驗證。
但眾所周知保存和傳遞 secret 都是有很大安全負擔的,而且在絕大多數的通信中也完全沒有必要去傳遞密碼,因此比較成熟的解決方案是基於數字簽名的的令牌(token)。簽名和 RSA 加密是常用認證技術的基礎,是各位讀者日常工作都要面對的問題,在這裏不去過多敘述其原理。
Security Token Service (STS),是一種發行和驗證 token 的網絡服務,扮演著客戶端和被訪問資源之間的中介者的角色。STS 被雙方共同信任,和數字證書的 CA 類似,其已知網址被認為是可信任的信息發布端。STS 可能實現 REST 協議,也可能支持瀏覽器跳轉協議。下面的網址是一些現實的例子:
- https://accounts.google.com
- https://login.salesforce.com
- https://sts.aliyuncs.com
下面的序列圖是一個基於 STS 認證的理論模型:
其大致步驟為:
- 客戶端 Client App 向 STS 發送 token 請求,描述所申請的 token 信息。
- STS 通過 Client App 的發送的密碼或者數字簽名進行驗證,並在通過授權(Authorization)的情況下返回 token。
- Client App 調用訪問的資源 Resource Server,並同時發送所獲得的 token。
- Resource Server 直接或者間接通過 STS 來驗證所收到的 token。
- STS 驗證 token 並返回驗證結果。
- Resource Server 執行所請求的操作並返回結果給 Client App。
在具體實現的時候,根據現實情況和要求,該流程可能會有各種變化或者調整,例如:
- 當 STS 頒發 token 的時候,授權過程可能會有用戶和管理員的參與,比如常見的 OAuth 2.0 協議。
- 出於性能考慮,token 可能會被緩存,因此並非每一步都需要和 STS 交互。
- 驗證 token 的過程往往不需要和 STS 直接交互。
- 在微服務系統中,STS 可能並不是由第三方提供,而是自己的內部實現。
- 同樣在微服務中,STS 可以退化為在本機上運行的實例,可以用 RPC 協議提供,甚至可以是進程內部調用。
- 被訪問的服務可以選擇信任多個 STS,並且可以由 Client App 根據當前業務邏輯來選擇。
- 給 token 授權的過程可以發生在 STS,也可以發生在被訪問的資源端,或者兩者皆有。
STS 很大程度上降低了客戶端和被訪問資源之間的耦合性,在現實生活中可以把 STS 比作是金融交易中的第三方擔保人,在軟件設計中可以把 STS 當作像消息隊列一樣的組建來類比。
STS 是獨立於系統業務邏輯之外的中間件,從架構設計的角度講,和其他組件相比具有較強的穩定性和長期性。
前面提到,當微服務之間的調用網格(mesh)比較復雜的時候,讓各個服務去做點對點的安全認證是工作量很大的任務,從開發和運維的角度都不經濟且不安全,因此 STS 角色的引入可以簡化請求者和被調用服務的設計和實現。
在有用戶參與的微服務調用中,單點登錄(SSO)是用戶體驗的最基本要求。作為 token 的中心發布者和仲裁者,STS 可以簡化 SSO 的實現。各個服務只要對 STS 負責,從而實現 token 在服務間的傳遞和交換。
STS 對客戶端的認證
當客戶端 app 調用 STS 服務獲取 token 時候,首先要解決的第一個問題是客戶端自身的認證問題。通常有以下幾種解決方案:
- Client App 發送由 STS 分配和生成的 App ID 和 App Secret。
- Client App 發送使用自己私鑰(private key)生成的數字簽名,然後 STS 使用該 app 註冊時候上傳的公鑰(public key)來驗證簽名。
- 使用數字證書 (certificate)。
- 只提供 App ID,然後由當前的用戶或者管理員來做代理授權。這種情況下客戶端程序也被可以被稱為用戶代理 User Agent,相當於 STS 實際上是在認證用戶而不是 app。
以上的不同方案都要求一個 Client App 的註冊過程,並且可能需要生成或者交換某種形式的對稱或者非對稱密碼。在技術團隊的開發運維中,這個註冊的操作需要有特定權限的工程或者管理人員完成,對於有較高合規性要求的產品或者系統,則更要經過嚴格的流程管控。
有意思的是,認證和授權問題總是一個雞生蛋蛋生雞的鏈條,用人去授權程序,然後再用程序來驗證人。
在一些實現方案中,在註冊 app 的時候同時也要指定該 app 的權限,資源範圍,以及其他的相關參數。總的來講就是描述當前 app 的使用場景,從安全的角度來考慮的話,這種描述越細致越好。
例如在有多租戶(multi-tenancy)的環境中,app 可能被局限於當前所屬的公司組織,也有可能作為全局通用的客戶端。
公鑰發布
當 STS 決定給 Client App 頒發 token 時候,將使用自己的私鑰生成 token 消息主體的數字簽名,並最終組裝成完整的 token 返回給 app。接下來 app 把 token 隨請求發送給被訪問的資源服務,由該服務來驗證當前 token 確實由其信任的某個 STS 頒發。
前面提到,校驗 token 最直接的辦法是把 token 發到已知的 STS 服務端的驗證 endpoint,但這樣做的代價很大,影響整個流程的性能。按照加密原理,校驗簽名的真實性只需要從可信任的來源獲得之前 STS 使用的私鑰所對應的公鑰。
獲取公鑰的一種做法是由運維人員事先設置好,可以是某個本地文件,或者從分布緩存中讀取。然而無論這個過程是手動的還是自動的,如果程序本身不能在需要的時候去初始源獲取公鑰,那麽總是有一定的運維負擔,並且在變更簽名密鑰的時候可能會因為公鑰不匹配而導致無法完成驗證。
由於公鑰數據本身不是 secret,所以業界常用的做法是在某一個已知(well-known)的網絡地址發布公鑰數據來供需要校驗 token 的程序來下載,並支持匿名訪問。下面是幾個 STS 的公鑰 endpoint 的例子:
- https://www.googleapis.com/oauth2/v3/certs
- https://login.microsoftonline.com/common/discovery/v2.0/keys
- https://login.salesforce.com/id/keys
從網址可以看出這些 URL 是由 Google、Microsoft 和 Salesforce 所提供的,在 HTTPS 的前提下是可以被信任的。這裏再次強調 well-known 的重要性,也就是說 token 的校驗程序本身獨立而靜態地知道所信任的 STS 的公鑰地址,而不是從 token 動態獲得,否則就失去了信任的邏輯基礎。
當然程序可以信任多個 STS,而根據當前 token 的信息來匹配使用哪一個 STS 對應的公鑰。在實際實現中,可以借助相對應的 SDK 來簡化工作。
另外以上的這些 URL 實際上是各個公共 STS 實現的 OpenID Connect 協議的一部分(基於 OAuth 2.0),也可以在對應的 Discovery Document 中獲得,比如 Google 的是 https://accounts.google.com/.well-known/openid-configuration。
上面的公鑰地址例子是面向公共網絡和第三方服務設計的,所以任何可以連接到互聯網的程序都可以通過這些 URL 獲取公鑰。在微服務架構的場景下,可以靈活實現這樣的公鑰發布端。
在保證 well-known 的前提下,這種 endpoint 可以是內部地址,某個雲托管虛擬機或者 NLB 的 DNS 名稱,甚至可以是私有 VPC 網絡的 IP 或者 VIP 地址。
密鑰更換和緩存
眾所周知,處於安全考慮,需要對密鑰數據進行定期更換。如果你在瀏覽器裏面打開上面的幾個公鑰地址,會發現每個地址都包含多個 key。需要發布多個 key 的其中一個原因就是更換的過程是需要時間的,尤其在考慮分布式系統和緩存的情況下。
對於 STS 來說,跟換密鑰的典型流程如下:
概括來看,這種模型在某個時刻可能總是存在一個當前密鑰(current key)和之前密鑰(previous key)的配對。實際情況下一般只有在開始引入新的密鑰的時候才會將之前的公鑰徹底移除,也就是說當 current 成為 previous 的時候,next 擠掉了 previous 的 previous。
對於需要下載和使用公鑰的 Resource Server 來說,處於性能考慮基本上需要緩存公鑰。推薦的做法是默認使用當前緩存的公鑰來認證,直到當前 token 和所緩存的公鑰都不匹配,這個時候要考慮重新下載公鑰。
這裏說的不匹配並不一定要執行真正的簽名驗證算法,也可以根據快速比對公鑰的 key ID 與 token 中的 key ID 來實現。
另外值得註意的是,如果當前的服務被暴露於公網中,有被攻擊的風險的話,在下載公鑰前應該檢查一下自己緩存數據是否已經足夠新,而不應該盲目開始下載以免被用來做 DDoS。
JSON Web Token
JSON Web Token,簡稱 JWT,是目前應用得最廣泛的 token 格式,其具體的規格定義在 RFC 7519 文檔中。 JWT 由一個 JSON 頭部,一個 JSON 消息主體和一個數字簽名連接在一起組成,在實際傳輸時候分別用 base64 對各部分編碼。
推薦的 JWT 工具是 https://jwt.io,上面有在線解碼和簽名驗證工具,並且收集了各種編程語言的 JWT 庫。
JWT 的頭部包含該 token 簽名使用的密鑰類型和 key ID,方便於校驗代碼來選擇公鑰。JWT 的主體部分則包含多條斷言(claim),用來描述請求的客戶端,用戶信息,請求對象和目的,授權信息等。接收到 JWT 的服務在驗證簽名後根據這些 claim 的值來執行相應的業務邏輯。
從用戶信息的角度來看,常見的 JWT 以下有幾種類型:
- User Token
- App Token
- App Asserted User Token
User Token 是由 STS 頒發的包含用戶信息的 token,適用於大多數包含用戶身份的請求,尤其以面向公共網絡的用戶界面和 API 網關為典型用例。以《權利的遊戲》裏的龍母為例,代表她身份的 user token 可能是這樣一種形式:
{
"appid": "92d0312b-26b9-4887-a338-7b00fb3c5eab",
"iss": "https://authority.westerossevenkingdoms.com",
"aud": "https://houseoftargaryen.com/ ",
"iat": "1433978353",
"exp": "1433981953",
"username": " dtargaryen",
"name": "Daenerys Targaryen",
"scp": "soldiers.attack dragons.burn ravens.send",
"roles": "mother_of_dragons queen_of_meereen breaker_of_chains …",
}
這些 claim 的說明如下:
App Token 用於非用戶場景,所包含的 claim 是上面 User Token 的一個子集,沒有用戶相關信息。一些後臺程序,系統工作流,數據聚合整理的調用都不是由用戶請求事件驅動的,適合使用 App Token。
當把整體架構的函數調用或 RPC 調用改造成微服務調用的時候,可以用 App Token 作為基本的橋梁來實現服務之間的驗證。但要註意 App Token 可能容易造成權限範圍過大的問題,一旦泄露的話會影響多個用戶的數據。
App Asserted User Token 介於 User Token 和 App Token 之間。在權限允許的情況下,Client App 從 STS 請求一個 App Token,然後將其作為一個 claim 來自己生成一個 User Token 包裹在外面,形成一個 App Asserted User Token。
事實上這種 token 沒有自己的數字簽名,由 Client App 自己填寫用戶信息。Resource Server 在做驗證時候取出裏面內嵌的 App Token 來驗證數字簽名和 app 權限,並在驗證通過後信任和使用用戶信息。因為 App Token 有較高的重用率,因此也容易被緩存。
App Asserted User Token 可以重復使用同一個未過期的 App Token 來和不同的用戶身份配對,減少調用 STS 的次數,從而避免對系統性能的影響。這種方法和將用戶信息放在 token 之外的做法沒有本質區別,其前提是 Resource Server 對特定 Client App 的信任,但從設計的角度可能會更整潔。
請求的授權
前面的章節提到,STS 在收到 token 請求時,會根據一系列條件決定要頒發的 token 裏所包含的權限集。這裏可能有用戶或者管理員的因素,而所請求的權限應該是 Client App 註冊權限的一個子集,原則上只應該請求當前操作所需要的最小權限。
第二階段的授權校驗發生在 Resource Server 接收端,根據 token 中的各個 claim 的值來決定最後的操作是否被允許,或者決定的操作的具體行為。下面是一些決定授權的因素:
- 根據 scope 來決定當前的操作是否被允許,比如只有讀權限的 token 不能執行 PUT 操作。
- 根據 audience 來決定 token 的訪問對象是不是自己,是否允許在不匹配的情況下繼續操作。
- 根據 roles 來決定某個頁面時候對高級用戶可見。
- 根據 appid 檢查當前 app 是否已經被加入黑名單。
- 根據 username 檢查當前用戶是否已經被軟刪除或者加入黑名單。
基於 HTTP 的 RESTful API 是目前最常用 API 形式,無論是 CRUD 模型還是 DDD 模型,當涉及到授權問題的時候,一個重要的設計原則就是其 URL 必須能夠描述被訪問資源的 scope。
例如下面的 URL 可以很好地和權限 groups.documents.delete 相吻合:
DELETE https://OurAwesomeChat.com/api/v1/groups/11EYKTN682A79T/documents/B002Y27P3M/
而下面的例子僅分析 URL 的話就不知所雲:
DELETE https://OurAwesomeChat.com/api/v1/?objectId=11EYKTN682A79T_B002Y27P3M
另外讀取 HTTP 請求(POST 或 PUT)的正文(body)是有性能代價的,除了解壓和反序列化之外,有的時候由於使用 Transfer-Encoding 還有額外網絡 IO。
被調用的服務在讀取請求正文之前,應該能根據動詞(verb)和 URL 路徑,對照當前接收到的 token 來作出授權決定,從而避免讀取正文。有的系統設計在做反向代理和分發時候會引入授權的因素,而讓代理或者 API 網關去分析(parse)請求正文從設計上和性能上都是很不理想的。
再論微服務
一些讀者可能會覺得我混淆了 SOA 和微服務架構,認為文中討論的基於 STS 的模型太復雜或者代價太高,更適合於服務之間的聯邦化協作而並非可控管的微服務。我們最後再來專門討論一下這個問題。
微服務的其中一個重要理念是系統低耦合,各個服務能夠獨立開發部署。在一個大中型規模的產品中,如果要讓其中一個小團隊能夠很快上線一個微服務,就需要有一個中心化的身份驗證 broker。
新的微服務只需要和 STS 登記註冊,就可以開始調用一些現有的服務,而不需要去和每個被調用者做顯式對接。STS 的引入可以降低整個體統的耦合度,可以被看做是服務發現的一部分,讓快速註冊稱為可能,也避免了重新發明輪子。對於有一定規模的項目,STS 的建設是一件一勞永逸的工作,可以帶來長期的回報。
作為使用雲計算平臺的項目和團隊,新的微服務可能是在獨立環境中開發部署的,甚至可能是從 hackathon 項目進化來的。新的微服務對於已有服務來說很可能是位置透明的,也就是說調用請求是從互聯網的某個角落來的,那麽唯一要關心的問題就是請求裏是不是帶著一個由 STS 頒布的合法 token,而 token 是怎麽來的卻不是被調用者應該關心的問題。
被調用的微服務只需保證自己的計算能力可以在客戶端增長的情況下彈性增長,而不需要根據新的調用者來改變自己的安全策略。雲計算平臺容器和無服務架構的流行讓微服務的搭建變得更加動態,連傳統的後臺程序都可以改造成事件驅動模型,而 STS 簡化了這些動態微服務的進入和退出。
API 網關是很多微服務架構中的重要組件。當整個系統有統一的認證協議時,就很容易使用 API 網關來做部分甚至全部的認證工作。對於接受外部用戶請求的微服務系統,API 網關和 STS 協同工作可以分擔一些內部微服務的工作量,尤其是核心數據服務。在一些設計中,SSL 卸載可以和 STS 協同工作以達到優化內部微服務調用性能的目的。
需要指出的是,常見的 OAuth 2.0 協議也是基於 STS 的,但 OAuth 2.0 所解決問題的側重點不一樣。OAuth 2.0 本質上是一個授權協議,它強調用戶在授權過程中的角色,要求用戶與瀏覽器的參與,而且其中某些模式甚至完全淡化 app 自身的身份和權限問題。
在 OAuth 2.0 中,STS 被稱為 Authorization Server,通常由獨立於 Client App 和 Resource Server 之外的第三方來實現。而微服務對整體架構的拆分往往聚焦於改進自身架構,因此 STS 的引入是為了解決系統耦合問題,其目的並非為了改變用戶的登錄和授權方式。
理論上講,一個系統無論是選擇整體架構還是微服務架構,對用戶來說都應該是透明的。根據微服務的具體需要,STS 甚至可以是私有的。值得一提的是,OAuth 2.0 中的 Client Credential Flow 嚴格意義上不是 OAuth 2.0 所要解決的授權問題,但這個模式卻可能是最適用於微服務架構的模型。
個人認為基於 STS 的身份認證方案可以滿足多種認證要求,無論是微服務系統內部,外部服務之間,還是混合雲的應用場景,都有 STS 的用武之地。當然,脫離業務需求的架構設計都是空談,也沒有什麽解決方案是萬金油。本文討論的一些概念和思想,比如 JWT,數字簽名和令牌緩存,可以本著因地制宜的原則去采納。
微服務架構提高了很多項目和團隊的敏捷性和創新能力,並且同雲計算的理念一致。我在最近幾年的工作中也在做各種形式的微服務實踐,覺得受益頗多,很贊同這種設計思想,也感嘆當年 AWS 的前瞻性。
有時候剛進入行業的同學處於好奇問我用工作用什麽編程語言,我會開玩笑說我用的語言叫做 UJJ,中文讀作“優加加”,其實是 URI、JSON 和 JWT。
引用自:http://www.infoq.com/cn/articles/micro-service-authorization-sts-jwt
基於STS和JWT的微服務身份認證