1. 引言

本文将展示如何将Spring Security的授权决策外部化到OPA(Open Policy Agent)。通过这种方式,我们可以实现更灵活的权限控制策略管理。

2. 前言:为什么需要外部化授权

跨应用场景中,基于策略做决策是常见需求。当策略简单且稳定时,直接硬编码实现是最常见的方案。但现实往往更复杂:

  • 访问控制决策:随着应用复杂度提升,授权可能不仅依赖用户身份,还需结合请求上下文(如IP地址、访问时间、认证方式等)
  • 策略动态变更:规则需要能实时调整,最好不影响应用运行
  • 多应用一致性:跨系统统一权限模型的需求

架构图

这种架构的代价是增加了复杂度和外部调用开销,但带来的收益很明显:

  • 独立演化授权服务
  • 零停机更新策略
  • 多应用共享策略引擎

3. OPA是什么?

Open Policy Agent(OPA)是用Go实现的开源策略评估引擎。最初由Styra开发,现为CNCF毕业项目。典型应用场景包括:

  • Envoy授权过滤器
  • Kubernetes准入控制器
  • Terraform计划评估

安装非常简单,参考官方文档即可。安装后通过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语言编写策略——这是专门为复杂对象结构查询优化的声明式语言。注意:OPA的策略是通用的,并非专门用于授权决策,完全可用于传统规则引擎(如Drools)覆盖的场景。

4. 编写策略

下面是一个简单的REGO授权策略示例:

package baeldung.auth.account

# 默认拒绝访问
default authorized = false

authorized = true {
    count(deny) == 0
    count(allow) > 0
}

# 允许访问/public路径
allow["public"] {
    regex.match("^/public/.*",input.uri)
}

# 账户API需要认证用户
deny["account_api_authenticated"] {
    regex.match("^/account/.*",input.uri)
    regex.match("ANONYMOUS",input.principal)
}

# 账户访问授权
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]
}

关键点解析:

  • package语句:用于组织规则,在请求评估时扮演重要角色
  • 默认规则:确保authorized变量必有值
  • 核心聚合规则:当没有拒绝规则且至少有一个允许规则时授权通过
  • 允许/拒绝规则:匹配条件时向allow/deny数组添加条目

REGO语言注意事项

  • a := ba = b 是赋值(但两者有细微差别
  • a = b { ... } 表示条件满足时赋值
  • 规则顺序无关紧要

OPA内置了强大的函数库,支持深度嵌套数据结构查询、字符串操作、集合处理等。

5. 评估策略

用上节策略评估授权请求。请求数据构建为JSON结构:

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

注意:所有请求属性被封装在input对象中,评估时可通过JavaScript风格语法访问。

测试策略是否生效:

  1. 启动本地OPA服务器:
    $ opa run  -w -s src/test/rego
    
    参数说明:
  • -s:服务器模式
  • -w:自动重载规则文件
  • src/test/rego:策略文件目录
  1. 发送测试请求: ```bash $ 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" } } }'

**关键**:URL中的`/v1/data/baeldung/auth/account`对应策略包名(点号替换为斜杠)。

响应示例:
```json
{
  "result": {
    "allow": [],
    "authorized": false,
    "deny": []
  }
}

结果解析:

  • authorized: false:请求被拒绝
  • allow/deny为空:表示没有匹配具体规则,导致主授权规则也未生效

6. Spring授权管理器集成

现在将OPA集成到Spring Security框架(基于WebFlux的响应式版本,MVC版本原理类似)。

6.1 实现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由配置类初始化
  • toAuthorizationPayload:从AuthenticationAuthorizationContext构建请求体
  • toDecision:将响应映射为AuthorizationDecision

6.2 配置安全过滤器链

@Bean
public SecurityWebFilterChain accountAuthorization(ServerHttpSecurity http, @Qualifier("opaWebClient") WebClient opaWebClient) {
    return http.httpBasic(Customizer.withDefaults())
      .authorizeExchange(exchanges -> exchanges.pathMatchers("/account/*")
        .access(opaAuthManager(opaWebClient)))
      .build();
}

设计思路

  • 仅对/account API应用自定义授权管理器
  • 扩展时可支持多策略文档(例如根据请求URI选择策略包)

示例中的/account API是简单控制器,返回模拟账户余额数据。

7. 测试

构建集成测试验证功能:

7.1 正常访问测试

@Test
@WithMockUser(username = "user1", roles = { "account:read:0001"} )
void testGivenValidUser_thenSuccess() {
    rest.get()
     .uri("/account/0001")
      .accept(MediaType.APPLICATION_JSON)
      .exchange()
      .expectStatus()
      .is2xxSuccessful();
}

7.2 权限不足测试

@Test
@WithMockUser(username = "user1", roles = { "account:read:0002"} )
void testGivenValidUser_thenUnauthorized() {
    rest.get()
     .uri("/account/0001")
      .accept(MediaType.APPLICATION_JSON)
      .exchange()
      .expectStatus()
      .isForbidden();
}

7.3 无权限测试

@Test
@WithMockUser(username = "user1", roles = {} )
void testGivenNoAuthorities_thenForbidden() {
    rest.get()
      .uri("/account/0001")
      .accept(MediaType.APPLICATION_JSON)
      .exchange()
      .expectStatus()
      .isForbidden();
}

测试前提:运行测试前需启动OPA服务器并指定策略文件目录。

8. 结论

本文展示了如何使用OPA外部化Spring Security应用的授权决策。完整代码可在GitHub获取。这种架构特别适合需要动态调整策略或跨系统统一权限模型的场景,虽然增加了外部调用开销,但带来的灵活性和可维护性提升是值得的。


原始标题:Spring Security Authorization with OPA