授权策略
授权策略负责把端点上的声明式要求(
requireAuthorization())转换成可执行的授权规则(AuthorizationPolicy)。
适用场景
- 需要基于登录态、角色、声明或自定义条件控制接口访问
- 希望端点只声明"需要什么权限",判定规则由授权系统集中管理
- 多认证方案并存时,不同接口需要指定不同的认证方式
前置条件
授权策略依赖以下包:
| 包 | 用途 |
|---|---|
soulsoft_web_authorization | 授权框架核心(中间件、策略、Handler、requirement) |
soulsoft_web_authentication | 认证框架,授权阶段按策略执行认证 |
soulsoft_web_http | IAuthorizeData 元数据接口 |
所有包均通过 cjpm.toml 声明依赖后即可使用。
快速开始
一个最小但完整的授权示例:注册授权服务、定义命名策略、在端点上引用。
注册部分:addAuthorization() 注册授权服务,addPolicy() 定义命名策略。端点部分:requireAuthorization(["AdminOnly"]) 把策略挂到端点上。
builder.services.addAuthorization() { options =>
options.addPolicy("AdminOnly") { policy =>
policy.requireAuthenticatedUser()
policy.requireClaim("role", ["admin"])
}
}
let app = builder.build()
app.useRouting()
app.useAuthentication()
app.useAuthorization()
app.mapGet("/admin") { ctx =>
ctx.response.write("admin panel")
}.requireAuthorization(["AdminOnly"])2
3
4
5
6
7
8
9
10
11
12
13
14
15
这段代码跑了完整链路:请求 /admin → 授权中间件从端点元数据中读到 "AdminOnly" → 查到策略 [requireAuthenticatedUser + role=admin] → 执行认证 → 执行授权 → claim 匹配则放行,不匹配则 403。
核心概念
定位与职责
授权策略处于端点声明和权限判定之间,是一层翻译层:
端点声明 策略 判定
requireAuthorization() → AuthorizationPolicy → IAuthorizationHandler
("需要AdminOnly策略") (requirements列表 (逐个检查requirement
+ authenticationSchemes) 是否满足)2
3
4
策略本身不执行判定——它只是把端点上的声明式元数据转换成一组 requirements 和认证方案,真正的判定工作由授权处理器(IAuthorizationHandler)完成。
端点元数据如何变成策略
这是授权中间件的核心逻辑。AuthorizationMiddleware 从端点元数据中提取 IAuthorizeData,交给 AuthorizationPolicy.combine() 做组合:
private static func computePolicy(policyProvider, endpoint): ?AuthorizationPolicy {
let authorizeData = endpoint.metadata.getOrderedMetadata<IAuthorizeData>()
let authorizePolicies = endpoint.metadata.getOrderedMetadata<AuthorizationPolicy>()
return AuthorizationPolicy.combine(policyProvider, authorizeData, authorizePolicies)
}2
3
4
5
combine() 内部按以下顺序处理每条 IAuthorizeData:
- 有
policy名称 → 从IAuthorizationPolicyProvider查找命名策略,合并其 requirements 和 authenticationSchemes - 有
roles→ 自动添加RolesAuthorizationRequirement - 有
authenticationSchemes→ 加入方案的集合 - 上述都没有且没有附加策略 → 使用
defaultPolicy
这一步的结果是一个可执行的 AuthorizationPolicy,其中只有两样东西:requirements 列表和 authenticationSchemes 列表。
三种策略层次
| 层次 | 配置方式 | 默认值 | 何时生效 |
|---|---|---|---|
| 端点显式策略 | requireAuthorization(["名称"]) | 无 | 端点标注了策略名称 |
defaultPolicy | options.setDefaultPolicy { ... } | requireAuthenticatedUser() | requireAuthorization() 不传名称时 |
fallbackPolicy | options.setFallbackPolicy { ... } | None | 端点无任何授权元数据时 |
fallbackPolicy 默认为 None 意味着未标注授权元数据的端点直接放行。如果希望全局要求登录,设置 fallbackPolicy 即可:
options.setFallbackPolicy { policy =>
policy.requireAuthenticatedUser()
}2
3
设为 None 可恢复默认行为(关闭全局回退):
options.setFallbackPolicy(None)AllowAnonymous 的作用
allowAnonymous() 不会跳过认证——认证中间件照常执行,context.user 照常填充。它只是告诉授权中间件:授权失败时不要拦截,直接放行。
源码中对应的逻辑:
if (endpoint.metadata.getMetadata<IAllowAnonymous>().isSome()) {
next(context) // 跳过授权失败处理,但 User 已填充
return
}2
3
4
策略缓存
策略缓存是否启用由 IAuthorizationPolicyProvider.allowsCachingPolicies 属性控制。DefaultAuthorizationPolicyProvider 将该属性返回 true,因此默认启用缓存——同一个端点的策略只计算一次,后续请求命中缓存直接返回。自定义 IAuthorizationPolicyProvider 实现时,可通过覆盖 allowsCachingPolicies 属性来控制缓存行为(返回 true 启用,false 关闭),而非由继承关系自动决定。
常见用法
添加命名策略
把常见的访问规则沉淀为命名策略,端点上按名称引用。三种典型模式:
builder.services.addAuthorization() { options =>
options.addPolicy("Authenticated") { policy =>
policy.requireAuthenticatedUser()
}
options.addPolicy("AdminOnly") { policy =>
policy.requireClaim("role", ["admin"])
}
options.addPolicy("AtLeast18") { policy =>
policy.requireClaim("age") { age => age.toInt64() >= 18 }
}
}2
3
4
5
6
7
8
9
10
11
requireClaim(claimName) 只检查声明是否存在,requireClaim(claimName, allowedValues) 进一步检查值是否在允许列表中,requireClaim(claimName) { predicate } 用自定义断言校验值。三种重载对应 ClaimsAuthorizationRequirement 的不同构造参数。
多方案认证
当多个认证方案并存时,策略可以通过 authenticationSchemes 指定使用哪个方案认证:
options.addPolicy("ApiOnly") { policy =>
policy.requireAuthenticatedUser()
policy.addAuthenticationSchemes(["Bearer"])
}
app.mapGet("/api/data") { ctx =>
ctx.response.write("data")
}.requireAuthorization(["ApiOnly"])2
3
4
5
6
7
8
授权阶段会按策略指定的方案列表逐个调用 context.authenticate(scheme),将多个方案的结果合并到 context.user。如果策略没有指定 authenticationSchemes,则复用认证中间件的默认方案结果。
修改默认策略
defaultPolicy 的默认值是"只要登录即可"。如果全局要求更严格的默认规则,可以替换:
options.setDefaultPolicy { policy =>
policy.requireAuthenticatedUser()
policy.requireClaim("role", "user")
}2
3
4
修改后,所有 requireAuthorization() 不带参数调用的端点都自动要求 role = "user"。
生产建议
- 先把常见访问规则沉淀为命名策略,再挂到端点上。避免在每个端点重复写相同的
requireClaim组合——一旦规则变化,只需改策略定义,不用逐个端点修改。 defaultPolicy和fallbackPolicy要区分清楚,不要混用。defaultPolicy作用于标注了requireAuthorization()的端点,fallbackPolicy作用于没标注的端点。前者是"要求了授权,但没指定具体规则",后者是"完全没提授权这件事"。- 多认证方案项目里,优先明确策略对应的认证方案。如果策略不指定
authenticationSchemes,授权阶段只依赖默认认证方案的结果——当一个端点需要特定方案认证时,策略里显式声明更安全。
常见问题
requireAuthorization() 不传参数时会怎样
使用 defaultPolicy。默认值是 requireAuthenticatedUser()——只要登录了就能访问。可以通过 options.setDefaultPolicy() 修改。
端点上能挂多个策略吗
可以。requireAuthorization(["PolicyA", "PolicyB"]) 会创建两个 IAuthorizeData,combine() 依次合并每个策略的 requirements 和 authenticationSchemes。最终该端点需要满足所有策略的所有 requirements。
requireClaim 和 requireRole 有什么区别
requireRole("admin") 是 requireClaim(ClaimTypes.Role, ["admin"]) 的简写,内部使用 RolesAuthorizationRequirement,通过 user.isInRole() 判断。功能等价,requireRole 更语义化。