自定义授权要求
当内置
requireClaim、requireAuthenticatedUser不够用时,通过实现IAuthorizationRequirement+IAuthorizationHandler编写自定义判定逻辑。
适用场景
- 需要判断当前时间、租户、风险等级、资源归属
- 需要查询数据库、调用外部服务或打审计日志
- 希望把复杂权限逻辑沉淀成可复用规则
简单场景(检查声明值、检查角色)优先用内置 requirement,不要为了"统一风格"而自定义。
前置条件
自定义授权要求依赖以下包:
| 包 | 用途 |
|---|---|
soulsoft_web_authorization | IAuthorizationRequirement、IAuthorizationHandler、AuthorizationHandler<T>、AuthorizationHandlerContext |
soulsoft_identity_claims | ClaimsPrincipal,从 context.user 读取声明 |
soulsoft_extensions_injection | DI 注册 Handler(分离模式时需要) |
如需日志记录,额外依赖 soulsoft_extensions_logging。
快速开始
下面是一个极简示例——只允许指定部门的用户访问。DepartmentRequirement 同时实现 IAuthorizationRequirement 和 AuthorizationHandler<Self>(自处理模式),handleRequirement() 从 claims 中读取部门名称做比对:
import soulsoft_web_authorization.*
import soulsoft_identity_claims.*
public class DepartmentRequirement <: AuthorizationHandler<DepartmentRequirement> & IAuthorizationRequirement {
private let department: String
public init(department: String) {
this.department = department
}
protected func handleRequirement(context: AuthorizationHandlerContext, requirement: DepartmentRequirement): Unit {
let dept = context.user.findFirstValue("department") ?? ""
if (dept == requirement.department) {
context.succeed(requirement)
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注册到策略,挂到端点:
builder.services.addAuthorization() { options =>
options.addPolicy("EngineeringOnly") { policy =>
policy.requireAuthenticatedUser()
policy.requirements.add(DepartmentRequirement("engineering"))
}
}
app.mapGet("/engineering") { ctx =>
ctx.response.write("engineering team only")
}.requireAuthorization(["EngineeringOnly"])2
3
4
5
6
7
8
9
10
这个 requirement 是自处理模式——类自身同时承载"条件定义"和"判定逻辑",无需额外注册 Handler。PassThroughAuthorizationHandler 在授权阶段自动扫描并执行。
核心概念
定位与职责
自定义授权要求遵循Requirement + Handler 分离模型。IAuthorizationRequirement 描述"需要满足什么条件",IAuthorizationHandler 判断"条件是否满足"。
框架提供两种实现模式:
| 模式 | 做法 | 何时用 |
|---|---|---|
| 自处理 | Requirement 同时实现 IAuthorizationHandler,由 PassThroughAuthorizationHandler 自动发现 | 逻辑简单、无外部依赖 |
| 分离 | Requirement 只做数据载体,Handler 单独实现并从 DI 获取依赖 | 需要日志、数据库、HTTP 客户端 |
自处理模式
Requirement 同时实现 IAuthorizationRequirement 和 AuthorizationHandler<Self>。
import std.time.*
import soulsoft_web_authorization.*
public class WorkingHoursRequirement <: AuthorizationHandler<WorkingHoursRequirement> & IAuthorizationRequirement {
private let startHour: Int64
private let endHour: Int64
public init(startHour: Int64, endHour: Int64) {
this.startHour = startHour
this.endHour = endHour
}
protected func handleRequirement(context: AuthorizationHandlerContext, requirement: WorkingHoursRequirement): Unit {
let hour = DateTime.now().hour
if (hour >= requirement.startHour && hour < requirement.endHour) {
context.succeed(requirement)
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
注册时直接把 requirement 实例加进策略——无需注册 Handler:
builder.services.addAuthorization() { options =>
options.addPolicy("WorkingHoursOnly") { policy =>
policy.requireAuthenticatedUser()
policy.requirements.add(WorkingHoursRequirement(9, 18))
}
}2
3
4
5
6
PassThroughAuthorizationHandler 在授权阶段遍历所有 requirement,筛选出实现了 IAuthorizationHandler 的项,逐个调用 handle()。你的 handleRequirement() 只负责判断条件 → succeed() 或什么都不做。
分离模式
当 handler 需要依赖注入时,把 Requirement 与 Handler 拆开——Requirement 只携带数据,Handler 从 DI 获取依赖。
import soulsoft_web_authorization.*
import soulsoft_extensions_logging.*
public class RiskLevelRequirement <: IAuthorizationRequirement {
public let minimumLevel: Int64
public init(minimumLevel: Int64) {
this.minimumLevel = minimumLevel
}
}
public class RiskLevelHandler <: AuthorizationHandler<RiskLevelRequirement> {
private let _logger: ILogger
public init(loggerFactory: ILoggerFactory) {
_logger = loggerFactory.createLogger<RiskLevelHandler>()
}
protected func handleRequirement(context: AuthorizationHandlerContext, requirement: RiskLevelRequirement): Unit {
let raw = context.user.findFirstValue("riskLevel") ?? "0"
let level = Int64.parse(raw)
if (level >= requirement.minimumLevel) {
_logger.debug("risk level check passed: ${level} >= ${requirement.minimumLevel}")
context.succeed(requirement)
} else {
_logger.warn("risk level too low: ${level} < ${requirement.minimumLevel}")
context.fail()
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Requirement 和 Handler 分开注册:
builder.services.addAuthorization() { options =>
options.addPolicy("RiskLevel3") { policy =>
policy.requireAuthenticatedUser()
policy.requirements.add(RiskLevelRequirement(3))
}
}
builder.services.addTransient<IAuthorizationHandler, RiskLevelHandler>()2
3
4
5
6
7
8
RiskLevelRequirement 是纯数据类,RiskLevelHandler 是带 ILogger 依赖的处理器。addTransient 将 Handler 注册到 DI,DefaultAuthorizationHandlerProvider 收集所有 IAuthorizationHandler 后进行类型匹配派发。
succeed() 与 fail()
AuthorizationHandlerContext 维护三个关键状态:
| 状态 | 含义 | 影响 |
|---|---|---|
_succeedCalled | 是否有 requirement 被 succeed | true + _failCalled 为 false + pendingRequirements 为空 → 授权通过 |
_failCalled | 是否有 handler 调用了 fail() | true → 授权必定失败,无论其他 requirement 是否满足 |
_pendingRequirements | 尚未被 succeed 的 requirement 集合 | 全部清空才可能通过 |
succeed(requirement) 从 _pendingRequirements 中移除该项。条件满足时调用,不满足时什么都不做。
fail() 将 _failCalled 设为 true,授权立即标记为失败。可选地传入 AuthorizationFailureReason 记录失败原因,用于日志和诊断。
最终判定:hasSucceeded = !_failCalled && _succeedCalled && pendingRequirements.size == 0。三个条件全部满足才算通过。
两种模式对比
| 项目 | 自处理 | 分离 |
|---|---|---|
| 类数量 | 1 个 | 2 个 |
| DI 注册 Handler | 无需 | 必须手动 addTransient |
| 是否可注入依赖 | 不行 | 可以 |
| 适用场景 | 简单独立判断 | 复杂逻辑、依赖外部服务 |
| 发现机制 | PassThroughAuthorizationHandler 自动扫描 | DefaultAuthorizationHandlerProvider 从 DI 收集 |
生产建议
- 简单规则优先做成命名策略,复杂规则再上自定义 requirement。内置
requireClaim、requireRole已覆盖大部分场景,自定义 requirement 只在确实需要查外部数据或执行复杂判断时才用。 - 如果需要日志、数据库、HTTP 调用,不要硬塞进自处理模式。自处理模式的 requirement 无法从 DI 获取依赖——这类场景应使用分离模式,让 Handler 正常接收注入。
- 自定义 requirement 应尽量做到命名清晰、职责单一、可复用。一个 requirement 只判断一件事(如"风险等级够不够"),不要在一个类里同时检查权限、审计、限流。
常见问题
多个 requirement 之间的关系是什么
全部必须满足。策略中的所有 requirement 都会进入 _pendingRequirements,每个 handler 只能 succeed 自己关心的 requirement。只有 _pendingRequirements 被全部清空,授权才算通过。这相当于"AND"逻辑——如果需要一个"OR"逻辑(如 admin 或 manager),应在策略中用一个 requirement 表达。
fail() 和不等同于不调 succeed() 有什么区别
不调 succeed() 只是让某个 requirement 留在 _pendingRequirements 中,授权最终仍可能因为 pendingRequirements 不为空而失败,但不会阻止其他 handler 执行。fail() 会立即将 _failCalled 设为 true,授权链路提前终止(除非 invokeHandlersAfterFailure = true),且能携带失败原因用于诊断。
分离模式的 Handler 可以用 Singleton 生命周期吗
取决于 Handler 的依赖。如果 Handler 依赖 Scoped 服务(如 DbContext、当前请求上下文相关的对象),必须用 Transient 或 Scoped。如果 Handler 完全无状态且不依赖请求级对象,可以用 Singleton。