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 := b
或a = 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风格语法访问。
测试策略是否生效:
- 启动本地OPA服务器:
参数说明:$ opa run -w -s src/test/rego
-
-s
:服务器模式 -
-w
:自动重载规则文件 -
src/test/rego
:策略文件目录
- 发送测试请求: ```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
:从Authentication
和AuthorizationContext
构建请求体 -
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获取。这种架构特别适合需要动态调整策略或跨系统统一权限模型的场景,虽然增加了外部调用开销,但带来的灵活性和可维护性提升是值得的。