分佈式系統下的認證與授權[轉]

文章轉載自 : https://www.bmpi.dev/dev/authentication-and-authorization-in-a-distributed-system/ 非原創

在軟件系統設計中,如何讓應用能夠在各種環境中安全高效的訪問是個複雜的問題,這個問題的背後是一系列軟件設計時需要考慮的架構安全問題:架構安全性|鳳凰架構

  • 認證:系統如何識別合法用戶,也就是解決 你是誰 的問題;
  • 授權:系統在識別合法用戶後,還需要解決 你能做什麼 的問題;
  • 憑證:系統如何保證它與用戶之間的承諾是雙方真實意圖的體現,是準確、完整且不可抵賴的;
  • 保密:如何安全的持久化用戶的賬戶信息,確保不會被任何人竊取與濫用;
  • 傳輸:在複雜的用戶環境中,如何安全的傳遞用戶信息,保證不被第三方竊聽、篡改和冒充。

在漫長的架構演進歷史中,業界對這些問題已經有很成熟的解決方案。在架構安全這塊,最好的是遵循技術標準與最佳實踐,儘可能不重複造輪子或“創新”。下面這個思維導圖就是針對這些問題的常見的技術標準及方案:

img

在研究分佈式系統的認證和授權問題前,讓我們回到單體架構的時代,看看在單體架構上這些問題是如何被解決的。

單體系統

認證

認證主要解決 你是誰 的問題,從方式上來看有以下三種:認證|鳳凰架構

  • 基於通信信道:建立通信信道之前需要證明 你是誰。在網絡傳輸(Network)場景中的典型是基於SSL/TLS傳輸安全層的認證。
  • 基於通信協議:在獲取資源之前需要證明 你是誰。在互聯網(Internet)場景中的典型是基於HTTP協議的認證。
  • 基於通信內容:在提供服務之前需要證明 你是誰。在萬維網(World Wide Web)場景中的典型是基於Web內容的認證。

在單體系統時代,認證方式一般是在通信信道上開啓HTTPS,在通信協議上利用 HTTP Basic/Digest/Bearer/HOBA/OCRA 等方式並在通信內容上結合表單或 TOTP 等的認證組合方式。這樣可以從通信的不同階段獲得相應的安全保證。

如果想對基於HTTP協議的認證方式做進一步的瞭解,可以參考這兩篇文章:

  1. 認證|鳳凰架構
  2. 細說API -認證、授權和憑證- Thoughtworks洞見

單點登錄(SSO

認證的一個常見應用場景是單點登錄。單點登錄主要解決了一個一次登錄訪問多個獨立應用的問題。在單點登錄方案出現之前,每個應用都需要獨立登錄維持各自的會話。相關的技術方案已經很成熟,主要有以下:

  • Kerberos-based:MIT設計的SSO協議,基於對稱密碼學,並需要一個值得信賴的第三方。其廣泛用於操作系統認證,如被Windows 2000和後續的操作系統作爲默認的認證方法。
  • CAS:Yale設計的SSO協議,基於瀏覽器的SSO方案,部署簡單,適用於簡單的應用場景。
  • SAML:基於XML標記語言的認證斷言方案,適用的場景衆多,但技術較複雜。
  • OIDC:在OAuth2的基礎上額外加一個JWT來傳遞用戶信息。功能全面強大,是目前很流行的SSO方案。

授權

授權主要解決 你能做什麼 的問題,從方案上來說有以下幾種:

  • ACL:訪問控制列表(Access-control list)廣泛用於操作系統內部的文件系統、網絡及進程權限控制方面。如在Linux中,可通過 getfacl 獲取目錄的默認ACL設置。
  • RBAC:RBAC通過將權限屬性從ACL方案中的單個用戶抽取成更爲抽象的角色(Role),通過給角色一組權限屬性,再將多個角色賦予某個用戶,實現了比ACL更爲靈活強大的權限控制方案。實際上大部分系統的授權方案採用RBAC就足夠了。但RBAC在面臨複雜的權限控制需求時可能面臨角色爆炸的問題,這時可以考慮採用更細粒度的ABAC方案。
  • ABAC:ABAC是比RBAC更細粒度的權限控制方案。通過引入一組稱爲“屬性“的特徵,包括用戶屬性、環境屬性和資源屬性。例如,ABAC可以對用戶的訪問做進一步的控制,如只允許在特定的時間或與相關員工相關的某些分支機構進行訪問員工信息的操作,而不是讓某部門的人員總是能夠訪問員工信息。但ABAC的問題在於初始設置需要定義大量的屬性,工作量比RBAC要大。
  • OAuth2:OAuth2是爲了解決應用系統給第三方系統授權的問題而設計的授權框架。傳統的客戶端服務器交互模式中,客戶端持有資源訪問憑證(如用戶名密碼),服務端驗證成功後放行。而在給第三方系統提供資源時,如果給第三方系統資源憑證,可能會帶來未知的安全問題,比如憑證泄漏,憑證回收等問題。如果應用系統需面向第三方系統提供服務,那需要使用此方案。同時因爲OAuth2做授權的時候一般需要用戶登錄,也能實現單點登錄的功能。

如果想對授權做進一步的瞭解,可以參考這篇文章:

  1. 授權|鳳凰架構

憑證

憑證是爲了解決在認證授權後如何承載認證授權信息的問題。在單體應用時代,主流的解決方案是基於HTTP協議的Cookie-Session機制爲代表的服務端狀態存儲技術。

由於HTTP協議本身是無狀態的,要維持一個會話(Session),而不是每次訪問都重新認證授權,需要客戶端也就是瀏覽器通過Cookie來存儲服務器端返回的一個憑證信息,這個憑證信息一般是一串隨機的字符串,用來代表用戶此次的會話標識。每次請求瀏覽器都會在HTTP Header中攜帶這個Cookie信息,應用拿到這個會話標識後從內存或緩存(Cache)中查詢出用戶的信息,這樣就定位到了具體的用戶,實現了會話的維持。

這套古老的方案存在以下先天優勢:憑證|鳳凰架構

  • 狀態信息都存儲於服務器,只要依靠客戶端的 同源策略 和HTTPS的傳輸層安全,保證Cookie中的鍵值不被竊取而出現被冒認身份的情況,就能完全規避掉上下文信息在傳輸過程中被泄漏和篡改的風險(但Cookie方案容易受到 CSRF 攻擊,這種可通過 CSRF Token 技術防禦);
  • 另一大優點是服務端有主動的狀態管理能力,可根據自己的意願隨時修改、清除任意上下文信息,譬如很輕易就能實現強制某用戶下線的這樣功能;
  • 服務端也很容易實現如統計用戶在線這類功能;

一切都很美好,直到我們來到了分佈式系統時代。

分佈式系統

分佈式系統與單體系統的一大區別就是狀態管理。分佈式系統通過把單體系統中有狀態的部分轉移到中間件中去管理,從而很容易做到水平擴容,提高系統峯值處理能力。在架構認證和授權部分,分佈式和單體並沒有什麼不同,唯獨有變化的在持有狀態的憑證部分。

我們知道單體應用在服務端管理用戶會話信息,客戶端只持有會話標識。如果服務端要將此用戶會話狀態轉移出去有兩種處理思路:

  • 將用戶會話信息繼續託管至服務端。此時有幾種服務端方案可以選擇:

    • 中心化存儲:轉移到中間件如Redis中去。利用Redis 極高的併發處理能力,也可以做到彈性橫行擴容。不過可能會帶來中間件高可用性維護難的問題,通過租賃雲服務商的託管中間件是降低中間件 單點故障(SPOF) 的一種方式;
    • 會話複製(Session replication):讓各個節點之間採用複製式的Session,每一個節點中的Session變動都會發送到組播地址的其他服務器上,這樣某個節點崩潰了,不會中斷該節點用戶的服務。但Session之間組播複製的同步代價高昂,節點越多時,同步成本越高。
    • 會話粘滯(Sticky session):通過負載均衡算法如Nginx的 IP Hash 算法將來自同一IP的請求轉發至同一服務。每個服務節點都不重複地保存着一部分用戶的狀態,如果這個服務崩潰了,裏面的用戶狀態便完全丟失。

    爲什麼在分佈式系統中共享狀態就這麼困難?這是因爲分佈式系統中有一個不可能三角的理論:CAP。這個理論簡單的理解就是因爲在分佈式系統中,因爲網絡無法做到絕對的可靠(分區容錯性:Partition Tolerance),只能在一致性(Consistency)和可用性(Availability)間選擇一個。

    比如上述的三種服務端方案其實都是犧牲了CAP的某個方面。比如第一種中心化存儲方案我們放棄了中心化存儲的分區容錯性,一旦其網絡分區,整個集羣都會不可用。第二種會話複製方案我們犧牲了可用性,當節點在同步會話數據時,整個服務會短暫的不可用。第三種會話粘滯方案我們犧牲了一致性,一旦某個節點宕機,整個集羣的數據會因該節點的數據丟失而達到不一致的狀態。

  • 將狀態從服務端轉移到客戶端。Cookie-Session是一種引用令牌(Reference tokens),也就是客戶端持有的是服務端存儲的會話引用標識。還有一種自包含令牌(Self-contained tokens),如 JWT 就是這種客戶端保存會話信息的技術,服務端只是去校驗會話信息是否合法。

JWT

如果你對JWT不瞭解,可以先看這兩篇:

  1. JWT |鳳凰架構
  2. The Hard Parts of JWT Security Nobody Talks About

由於JWT的Payload並未做過多限制,所以很容易產生濫用的問題,並且帶來很多誤解。 比如下面的一些問題:

  • 誤把JWT當作Cookie-Session使用(把JWT當作引用令牌使用),會帶來未知的隱患。遵循不重複造輪子和“創新”的指導原則,儘可能不要這麼做;
  • 認爲JWT更安全。雖然JWT採用了一定的加密算法簽名,使其具備了抗篡改的能力。但其Payload大部分都只是採用 base64UrlEncode 編碼,數據並不是加密的。攻擊者可以通過 會話劫持(Session hijacking) 技術拿到JWT會話信息,之後通過 會話重放攻擊(Session Replay Attack) 獲取用戶資源,所以最佳實踐是通過啓用TLS/SSL來加密通信信道。
  • 把JWT存儲到瀏覽器的Local Storage中。此方式很容易受到 XSS 攻擊導致JWT泄漏。可通過服務端啓用 內容安全策略(CSP) 來防禦這種攻擊。
  • 採用對稱加密方式簽名(Signature)。對稱加密密鑰一旦泄漏,會讓整個服務的基礎設施遭受安全威脅。JWT支持非對稱加密算法,只有簽名的服務需要私鑰,其他驗證JWT信息的服務只需要使用公鑰即可。
  • 不校驗JWT的簽名算法。這篇 Critical vulnerabilities in JSON Web Token libraries 文章提到JWT的一種漏洞,通過 none 算法規避令牌驗證。所以最好每次都驗證JWT header中的簽名算法是否是期望的。

相信看了上述的一些問題,你對JWT的簡單、安全有了新的理解。這還沒完,JWT還有以下一些Cookie-Session沒有的問題:

  • 令牌難以主動失效:JWT中雖然有 expnbfiat 這些和時間相關的屬性,但很難在令牌到期之前讓令牌失效,比如很難在用戶退出登錄時立刻讓簽發的令牌全部失效。雖然可能通過一些“黑名單”的技術解決這個問題,不過相比Cookie-Session來說,引入了一定的複雜性;
  • 令牌數據老舊:很難把簽發的令牌全部更新成最新的數據。比如把用戶的權限信息(Role)放在JWT Payload中,當用戶的角色發生變化時,很難把之前簽發的令牌信息更新成最新的數據;
  • 令牌存儲:存儲在客戶端意味着有多種選擇:Cookie?Local Storage?如果放在Cookie中,爲了安全,一般會給Cookie設置 http-onlysecure 的屬性。但這也會帶來一定的不便性,比如客戶端要讀取JWT Payload的內容只能藉助服務端API接口。如果將JWT存儲至瀏覽器Local Storage,雖然方便了客戶端讀取,但可能會帶來XSS攻擊的威脅,又需要去設置CSP來防禦這種威脅;
  • 令牌大小:JWT相比Cookie-Session還是大不少,尤其是要在Payload中存儲一些額外的權限信息。一般服務端都有對HTTP Header的大小限制;
  • 網絡開銷:更大的文本意味着更高的網絡開銷,進一步會需要更復雜的基礎設施,也會產生複雜的運維問題等;
  • 難以統計:服務端無狀態意味着很難做諸如統計用戶在線數量的功能;

JWT解決了Cookie-Session方案在分佈式系統中因CAP的限制而帶來的問題,但同時也帶來了一些新的問題。所以並不能說JWT就是Cookie-Session在分佈式系統中的完美替代。

那麼JWT的最佳使用場景到底是什麼?這篇 Stop using JWT for sessions 給出了以下的結論:JWT更適合作分佈式系統中的一次性令牌使用。分佈式系統繼續使用Cookie-Session做會話管理,但可以在認證鑑權後生成JWT做分佈式系統內部服務調用間的一次性令牌。

讓我們通過一個例子來理解下在分佈式系統下的認證授權場景。

一個例子

img

  1. 此處Auth服務承擔的是授權(Authorization)的職責,而不是認證(Authentication)的職責;
  2. OAuth2在協議中是做授權框架的,但是其一般需要登錄授權,也能實現SSO的功能。
  1. 用戶通過HTTPS訪問我們的應用。當請求發送至微服務網關層(Gateway),網關檢測HTTP Header中的Cookie發現沒有 SESSIONID 這個鍵值對,重定向至SSO登錄頁面。
  2. 用戶通過SSO登錄我們的應用。
    1. 用戶信息存放至AD/LDAP等系統中。管理員提前給用戶配置好角色權限。
    2. SSO集成方案我們選擇OIDC。OIDC集成了AD/LDAP,當用戶提供正確的用戶名和密碼後,SSO重定向至網關。
    3. 網關生成了 SESSIONID 鍵值對並通過HTTPSet-Cookie響應給用戶瀏覽器設置了此Cookie。
  3. 瀏覽器重新發起帶SESSIONIDCookie的請求。網關經過查詢其緩存或中間件(如將會話信息存放至Redis)中的Session信息確認了用戶的身份信息。之後網關請求Auth服務利用其私鑰簽名生成JWT憑證,JWT Payload中可以存放一部分用戶信息和角色信息,這些信息可以從中間件中或AD/LDAP中查詢出。
  4. 網關之後將此JWT憑證通過反向代理轉發至內部的BFF服務,之後請求到達內部的領域微服務。
  5. 各領域微服務接受到請求後,先從HTTP Header中拿出JWT憑證。
    1. 在執行真正的業務邏輯前,先利用之前定時從Auth服務中同步獲取的公鑰。
      1. Auth服務通過一個類似 https://<your_domain>/.well-known/jwks.json 的API提供JWT公鑰的分發。關於 .well-known 前綴,可閱讀 RFC 5785 做進一步瞭解。在 jwks.json 文件中,我們可以找到 JWK 或JSON Web Key,這是我們用來驗證簽名的公鑰。
      2. 校驗JWT這塊邏輯屬於微服務共有的部分,一般可以開發一個SDK包來做這個通用的工作。爲了提高性能,可使用緩存技術,定時從Auth中同步公鑰。
    2. 獲取到公鑰後驗證成功後拿出JWT Payload即可獲取到用戶信息和角色權限。

全部流程就是這樣,我們得到了以下的一些好處:

  • 這個流程裏我們並沒有將JWT返回給用戶,只是在認證授權過後生成一個一次性的JWT令牌憑證用於微服務內部服務間的調用。因爲用戶的權限信息存放至JWT Payload中,內部的服務並不需要從AD/LDAP中獲取用戶權限信息。可能有人覺得內部服務直接從中間件中獲取用戶會話信息也可以,但這又讓我們的應用進一步耦合了中間件,同時也讓一個請求鏈路中產生更多的子請求,不如直接在請求頭中存放用戶信息的方式高效。
  • 在微服務內部間傳遞的是經過非對稱加密算法簽名的JWT憑證,並不是一個JWT Payload信息。就算我們的微服務內部被入侵,攻擊者也並不能通過篡改憑證中用戶的權限信息來搞破壞。這也滿足了分佈式系統中 零信任網絡(Zero Trust) 的部分要求。
  • 與外部第三方應用的通訊(M2M),可以採用OAuth2的方式或Personal Access Token這種方式來集成。
  • 通過引入SDK與定時同步公鑰的機制,我們引入了一定的複雜度。比如SDK在異構編程語言的項目中開發複雜的問題。不過這個問題在雲原生系統時代有了不同的解法,讓我們之後討論這個問題。

架構總是在演進,也許分佈式系統中很多問題我們還沒完全解決,就來到了雲原生時代。

雲原生系統

如果你對雲原生應用開發還不瞭解的話,可以先看看我這篇 K8S雲原生應用開發小記。雲原生系統其實並不是什麼後分布式系統時代。它們兩者都是爲了解決不同場景的問題而出現的解決方案。

在認證授權這塊,雲原生系統的優勢在於可以通過 服務網格(Service Mesh) 做一些業務系統中通用的切面工作,比如我們在分佈式系統中遇到的校驗JWT的SDK其實就可以放入服務網格中的邊車(Sidecar)去實現,讓業務應用更專注特定領域的業務。

由於這篇文章並不主要討論雲原生,對這部分感興趣的可以參考以下兩篇文章做進一步瞭解:

  1. Service Mesh架構下的認證與授權
  2. 微服務下的身份認證和令牌管理

總結

由於篇幅及能力限制,這篇文章我只能從高層次梳理在不同架構演進中認證、授權及憑證這些和架構安全相關的技術的發展過程。由於這些技術涉及了大量的技術標準及實踐,很難在一篇文章中對這些技術做詳盡的分享,更無法去分享如何實現。但有了這些理論支持和最佳實踐,希望能讓你在實現的過程中多了一個指引。如果你想進一步瞭解,可參考文章中的參考文章鏈接。

最後,技術總是在不斷的發展,但並不是新技術總比老技術“先進”。正如文章中對Cookie-Session與JWT的分析對比,技術方案總是充滿了各種 Trade-off。而作爲一個工程師,我們能做的就是認清這些技術的歷史背景及侷限性,選擇最適合項目需求的技術方案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章