1. 引言
在本教程中,我們將演示如何將 Spring Security 的授權決策外部化到 OPA – Open Policy Agent。 (Open Policy Agent)
2. 序言:外部化授權的論據
在應用程序中,一個常見的需求是能夠根據策略做出某些決策。當該策略足夠簡單且不太可能更改時,我們可以直接在代碼中實現該策略,這是最常見的場景。
然而,在其他情況下,我們需要更大的靈活性。訪問控制決策是典型的:隨着應用程序的複雜性增加,訪問特定功能的權限可能不僅取決於你是誰,還取決於請求的其他上下文方面。這些方面可能包括 IP 地址、時間、登錄認證方法(例如“記住我”功能、OTP)等等。
此外,將上下文信息與用户身份相結合的規則應該易於更改,最好在不中斷應用程序的情況下進行更改。這一要求自然地導致了一個架構,其中一個專用服務處理策略評估請求。
在此,這種靈活性帶來的權衡是增加了複雜性和對調用外部服務造成的性能影響。另一方面,我們可以演化或完全替換授權服務,而不會影響應用程序。此外,我們可以與多個應用程序共享此服務,從而在它們之間實現一致的授權模型。
3. OPA 簡介
Open Policy Agent (OPA),簡稱 OPA,是一個使用 Go 語言實現的開源策略評估引擎。 Styra 最初開發了這個項目,但現在它已成為 CNCF 畢業項目。
以下是該工具的一些典型用途列表:
- Envoy 授權過濾器
- Kubernetes 訪問控制控制器
- Terraform 計劃評估
安裝 OPA 非常簡單:請參閲其 官方文檔 以獲取最新版本。 此外,我們將通過將其添加到操作系統 PATH 環境變量來提供它。 我們可以使用一個簡單的命令來驗證它是否已正確安裝:
$ opa version
Version: 0.39.0
Build Commit: cc965f6
Build Timestamp: 2022-03-31T12:34:56Z
Build Hostname: 5aba1d393f31
Go Version: go1.18
Platform: windows/amd64
WebAssembly: availableOPA 評估以 REGO 為例編寫的策略,REGO 是一種聲明式語言,針對在複雜對象結構上執行查詢進行了優化。客户端應用程序會根據具體用例使用這些查詢的結果。在我們的案例中,對象結構是一個授權請求,我們將使用策略來查詢結果以授予對特定功能的訪問權限。
需要注意的是,OPA 的策略是通用的,與表達授權決策無關。事實上,我們還可以將其用於傳統上由規則引擎(如 Drools 等)主導的其他場景。
4. 編寫策略
以下是一個用 REGO 編寫的簡單授權策略的示例:
以下是一個用 REGO 編寫的簡單授權策略的示例:
package baeldung.auth.account
# Not authorized by default
default authorized = false
authorized = true {
count(deny) == 0
count(allow) > 0
}
# Allow access to /public
allow["public"] {
regex.match("^/public/.*",input.uri)
}
# Account API requires authenticated user
deny["account_api_authenticated"] {
regex.match("^/account/.*",input.uri)
regex.match("ANONYMOUS",input.principal)
}
# Authorize access to account
allow["account_api_authorized"] {
regex.match("^/account/.+",input.uri)
parts := split(input.uri,"/")
account := parts[2]
role := concat(":",[ "ROLE_account", "read", account] )
role == input.authorities[i]
}
首先需要注意的是包聲明。OPA 策略使用包來組織規則,並且在評估傳入請求時也發揮着關鍵作用,正如稍後我們將展示的。我們可以將策略文件組織到多個目錄中。
接下來,我們定義實際的策略規則:
- 一個默認規則,以確保我們始終會為authorized變量提供一個值
- 主要的聚合規則,可以被解釋為“authorized在沒有拒絕訪問的規則以及至少有一個允許訪問的規則存在時,為true”
- 允許和拒絕規則,每個規則都表達一個條件,如果匹配,將會向allow或deny數組添加一個條目
OPA 策略語言的完整描述超出了本文的範圍,但規則本身並不難閲讀。在查看它們時,需要注意以下幾點:
- 形式為a := b或a=b的語句是簡單的賦值(儘管它們並不相同,請參考 FAQ)
- 形式為a = b { … conditions }或a { …conditions }的語句意味着“如果conditions為真,則將b賦值給a”
- 策略文檔中出現的順序不重要
除此之外,OPA 附帶了一個功能豐富的內置函數庫,針對深度嵌套的數據結構進行了優化,以及諸如字符串操作、集合和類似功能等更熟悉的特性。
5. 評估策略
讓我們使用上一節中定義的策略來評估一個授權請求。在本例中,我們將使用包含來自傳入請求的一些片段的 JSON 結構構建這個授權請求:
{
"input": {
"principal": "user1",
"authorities": ["ROLE_account:read:0001"],
"uri": "/account/0001",
"headers": {
"WebTestClient-Request-Id": "1",
"Accept": "application/json"
}
}
}
請注意,我們已將請求屬性包裝在一個單一的 input 對象中。該對象在策略評估期間成為 input 變量,並且我們可以使用類似 JavaScript 的語法通過它訪問其屬性。
為了測試我們的策略是否按預期工作,讓我們在服務器模式下本地運行 OPA 並手動提交一些測試請求:
$ opa run -w -s src/test/rego選項 -s 啓用服務器模式,而 -w 啓用自動規則文件重新加載。 src/test/rego 文件夾包含我們示例代碼中的策略文件。 運行後,OPA 將監聽本地端口 8181 的 API 請求。 如果需要,可以使用選項 -a 更改默認端口。
現在,我們可以使用 curl 或其他工具發送請求:
$ curl --location --request POST 'http://localhost:8181/v1/data/baeldung/auth/account' \
--header 'Content-Type: application/json' \
--data-raw '{
"input": {
"principal": "user1",
"authorities": [],
"uri": "/account/0001",
"headers": {
"WebTestClient-Request-Id": "1",
"Accept": "application/json"
}
}
}'請注意 /v1/data 前綴後的路徑部分:它對應於策略的包名,點被斜槓替換。
響應將是一個 JSON 對象,其中包含對輸入數據進行評估時產生的所有結果:
{
"result": {
"allow": [],
"authorized": false,
"deny": []
}
}
結果屬性是一個包含由策略引擎產生的結果的對象。我們看到,在這種情況下,已授權屬性的值為false。我們還可以看到allow 和 deny 屬性均為空數組。這意味着沒有特定的規則與輸入匹配。因此,主要的已授權規則也沒有匹配。
6. Spring 授權管理器集成
現在我們已經瞭解了 OPA 的工作原理,可以將其集成到 Spring 授權框架中。 在這裏,我們將重點關注其反應式 Web 變體,但其基本思想也適用於傳統的 MVC 應用程序。
首先,我們需要實現一個使用 OPA 作為後端,並實現 ReactiveAuthorizationManager 類型的 Bean:
@Bean
public ReactiveAuthorizationManager<AuthorizationContext> opaAuthManager(WebClient opaWebClient) {
return (auth, context) -> {
return opaWebClient.post()
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.body(toAuthorizationPayload(auth,context), Map.class)
.exchangeToMono(this::toDecision);
};
}
在這裏,注入的 WebClient 來自另一個 Bean,我們從中預先初始化其屬性,來源於 @ConfigurationPropreties 類。
處理流水線將職責委託給 toAuthorizationRequest 方法,該方法從當前的 Authentication 和 AuthorizationContext 中收集信息,然後構建一個授權請求負載。 類似地,toAuthorizationDecision 方法接收授權響應並將其映射到 AuthorizationDecision。
現在,我們使用該 Bean 構建 SecurityWebFilterChain。
@Bean
public SecurityWebFilterChain accountAuthorization(ServerHttpSecurity http, @Qualifier("opaWebClient") WebClient opaWebClient) {
return http.httpBasic(Customizer.withDefaults())
.authorizeExchange(exchanges -> exchanges.pathMatchers("/account/*")
.access(opaAuthManager(opaWebClient)))
.build();
}
我們正在將自定義的 AuthorizationManager 應用到 /account API 之外的任何地方。 這樣做的原因是,我們可以輕鬆地擴展此邏輯以支持多個策略文檔,從而使其更易於維護。 例如,我們可以配置使用請求 URI 選擇適當的規則包,並使用此信息構建授權請求。
在我們的情況下,/account API 本身只是一個簡單的控制器/服務對,它返回一個包含假餘額的 Account 對象。
7. 測試
最後但凡,我們來構建一個集成測試,將所有內容整合在一起。首先,我們確保“暢通路徑”能夠正常工作。這意味着一個已認證的用户能夠訪問其賬户:
@Test
@WithMockUser(username = "user1", roles = { "account:read:0001"} )
void testGivenValidUser_thenSuccess() {
rest.get()
.uri("/account/0001")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.is2xxSuccessful();
}
其次,我們還需要驗證已認證的用户只能訪問其賬户。
@Test
@WithMockUser(username = "user1", roles = { "account:read:0002"} )
void testGivenValidUser_thenUnauthorized() {
rest.get()
.uri("/account/0001")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isForbidden();
}
最後,我們還應該測試當認證用户沒有權限的情況:
@Test
@WithMockUser(username = "user1", roles = {} )
void testGivenNoAuthorities_thenForbidden() {
rest.get()
.uri("/account/0001")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isForbidden();
}
我們可以從 IDE 或命令行運行這些測試。請注意,無論採用哪種方式,我們首先必須啓動指向包含我們的授權策略文件的文件夾的 OPA 服務器。
8. 結論
在本文中,我們展示瞭如何使用 OPA(Open Policy Agent)來外部化 Spring Security 應用程序的授權決策。