使用OPA實現Spring安全授權 | baeldung

banq發表於2022-05-15

在本教程中,我們將展示如何將 Spring Security 的授權決策外部化到 OPA——開放策略代理

跨應用程式的一個共同要求是能夠根據策略做出某些決定。當這個策略足夠簡單並且不太可能改變時,我們可以直接在程式碼中實現這個策略,這是最常見的場景。
但是,在其他情況下,我們需要更大的靈活性。訪問控制決策是典型的:隨著應用程式變得越來越複雜,授予對給定功能的訪問許可權可能不僅取決於您是誰,還取決於請求的其他上下文方面。這些方面可能包括 IP 地址、時間和登入身份驗證方法(例如:“記住我”、OTP)等。
此外,將上下文資訊與使用者身份相結合的規則應該易於更改,最好不會導致應用程式停機。這一要求自然會導致一個專用服務處理策略評估請求的架構
這種靈活性的代價是在呼叫外部服務時增加了複雜性和效能損失。另一方面,我們可以在不影響應用程式的情況下發展甚至完全替換授權服務。此外,我們還可以與多個應用程式共享這個服務,從而在它們之間實現一致的授權模式。

什麼是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: available


OPA評估用REGO編寫的策略,REGO是一種經過最佳化的宣告性語言,用於執行對複雜物件結構的查詢。這些查詢的結果然後由客戶應用程式根據具體的使用情況來使用。在我們的案例中,物件結構是一個授權請求,我們將使用策略來查詢結果,以授予對特定功能的訪問。

需要注意的是,OPA的策略是通用的,不以任何方式與表達授權決策相聯絡。事實上,我們可以在其他傳統上由Drools等規則引擎主導的場景中使用它。

編寫策略
這是用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策略使用包來組織規則,它們在評估傳入的請求時也起著關鍵作用,我們將在後面展示。我們可以在多個目錄下組織策略檔案。

接下來,我們定義實際的策略規則。
  • 一個預設的規則,以確保我們最終總會得到一個授權變數的值
  • 主聚合器規則,我們可以理解為 "當沒有拒絕訪問的規則和至少有一個允許訪問的規則時,授權為真"
  • 允許和拒絕規則,每一條都表達了一個條件,如果匹配,將分別在允許或拒絕陣列中增加一個條目。

對OPA策略語言的完整描述超出了本文的範圍,但規則本身並不難讀。在看這些規則時,有幾件事要記住。
  • 形式為a :=b或a=b的語句是簡單的賦值(不過它們不一樣)。
  • a = b { ......conditions }或a { ......conditions }形式的語句意味著 "如果條件為真,則將b分配給a”
  • 在策略檔案中的順序出現是不相關的

除此之外,OPA還有一個豐富的內建函式庫,為查詢深度巢狀的資料結構進行了最佳化,同時還有更多熟悉的功能,如字串操作、集合等等。

評估策略
讓我們使用上一節中定義的策略來評估一個授權請求。在我們的例子中,我們將使用一個包含傳入請求的一些片段的JSON結構來建立這個授權請求。

{
    "input": {
        "principal": "user1",
        "authorities": ["ROLE_account:read:0001"],
        "uri": "/account/0001",
        "headers": {
            "WebTestClient-Request-Id": "1",
            "Accept": "application/json"
        }
    }
}


請注意,我們已經將請求屬性包裝在一個單一的輸入物件中。這個物件在策略評估過程中成為輸入變數,我們可以用類似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": []
  }
}


結果屬性是一個包含由策略引擎產生的結果的物件。我們可以看到,在這種情況下,授權屬性是假的。我們還可以看到,allow 和 deny 是空陣列。這意味著沒有特定的規則與輸入相匹配。因此,主要的授權規則也沒有匹配。

Spring Authorization Manager整合
現在我們已經看到了OPA的工作方式,我們可以繼續前進,把它整合到Spring授權框架中。在這裡,我們將專注於它的反應式Web變體,但一般的想法也適用於基於MVC的常規應用。

首先,我們需要實現ReactiveAuthorizationManager Bean,它使用OPA作為其後臺。

@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()
      .and()
      .authorizeExchange(exchanges -> {
          exchanges
            .pathMatchers("/account/*")
            .access(opaAuthManager(opaWebClient));
      })
      .build();
}


我們只將我們自定義的AuthorizationManager應用於/account API。這種方法背後的原因是,我們可以很容易地擴充套件這個邏輯以支援多個策略檔案,從而使它們更容易維護。例如,我們可以有一個配置,使用請求URI來選擇一個合適的規則包,並使用這些資訊來建立授權請求。

在我們的案例中,/account API本身只是一個簡單的控制器/服務對,它返回一個用假餘額填充的賬戶物件。

測試
最後但並非最不重要的是,讓我們建立一個整合測試,把所有東西放在一起。首先,讓我們確保 "快樂路徑 "的工作。這意味著,給定一個認證的使用者,他們應該能夠訪問自己的賬戶。

@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 伺服器。

結論
在本文中,我們展示瞭如何使用 OPA 將基於 Spring Security 的應用程式的授權決策外部化。像往常一樣,完整的程式碼可以在 GitHub 上找到
 

相關文章