Skip to content

自定义授权要求

当内置 requireClaimrequireAuthenticatedUser 不够用时,通过实现 IAuthorizationRequirement + IAuthorizationHandler 编写自定义判定逻辑。

适用场景

  • 需要判断当前时间、租户、风险等级、资源归属
  • 需要查询数据库、调用外部服务或打审计日志
  • 希望把复杂权限逻辑沉淀成可复用规则

简单场景(检查声明值、检查角色)优先用内置 requirement,不要为了"统一风格"而自定义。

前置条件

自定义授权要求依赖以下包:

用途
soulsoft_web_authorizationIAuthorizationRequirementIAuthorizationHandlerAuthorizationHandler<T>AuthorizationHandlerContext
soulsoft_identity_claimsClaimsPrincipal,从 context.user 读取声明
soulsoft_extensions_injectionDI 注册 Handler(分离模式时需要)

如需日志记录,额外依赖 soulsoft_extensions_logging

快速开始

下面是一个极简示例——只允许指定部门的用户访问。DepartmentRequirement 同时实现 IAuthorizationRequirementAuthorizationHandler<Self>(自处理模式),handleRequirement() 从 claims 中读取部门名称做比对:

cangjie
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)
        }
    }
}

注册到策略,挂到端点:

cangjie
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"])

这个 requirement 是自处理模式——类自身同时承载"条件定义"和"判定逻辑",无需额外注册 Handler。PassThroughAuthorizationHandler 在授权阶段自动扫描并执行。

核心概念

定位与职责

自定义授权要求遵循Requirement + Handler 分离模型IAuthorizationRequirement 描述"需要满足什么条件",IAuthorizationHandler 判断"条件是否满足"。

框架提供两种实现模式:

模式做法何时用
自处理Requirement 同时实现 IAuthorizationHandler,由 PassThroughAuthorizationHandler 自动发现逻辑简单、无外部依赖
分离Requirement 只做数据载体,Handler 单独实现并从 DI 获取依赖需要日志、数据库、HTTP 客户端

自处理模式

Requirement 同时实现 IAuthorizationRequirementAuthorizationHandler<Self>

cangjie
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)
        }
    }
}

注册时直接把 requirement 实例加进策略——无需注册 Handler:

cangjie
builder.services.addAuthorization() { options =>
    options.addPolicy("WorkingHoursOnly") { policy =>
        policy.requireAuthenticatedUser()
        policy.requirements.add(WorkingHoursRequirement(9, 18))
    }
}

PassThroughAuthorizationHandler 在授权阶段遍历所有 requirement,筛选出实现了 IAuthorizationHandler 的项,逐个调用 handle()。你的 handleRequirement() 只负责判断条件 → succeed() 或什么都不做。

分离模式

当 handler 需要依赖注入时,把 Requirement 与 Handler 拆开——Requirement 只携带数据,Handler 从 DI 获取依赖。

cangjie
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()
        }
    }
}

Requirement 和 Handler 分开注册

cangjie
builder.services.addAuthorization() { options =>
    options.addPolicy("RiskLevel3") { policy =>
        policy.requireAuthenticatedUser()
        policy.requirements.add(RiskLevelRequirement(3))
    }
}

builder.services.addTransient<IAuthorizationHandler, RiskLevelHandler>()

RiskLevelRequirement 是纯数据类,RiskLevelHandler 是带 ILogger 依赖的处理器。addTransient 将 Handler 注册到 DI,DefaultAuthorizationHandlerProvider 收集所有 IAuthorizationHandler 后进行类型匹配派发。

succeed() 与 fail()

AuthorizationHandlerContext 维护三个关键状态:

状态含义影响
_succeedCalled是否有 requirement 被 succeedtrue + _failCalledfalse + 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。内置 requireClaimrequireRole 已覆盖大部分场景,自定义 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。

相关专题