Skip to content

授权策略

授权策略负责把端点上的声明式要求(requireAuthorization())转换成可执行的授权规则(AuthorizationPolicy)。

适用场景

  • 需要基于登录态、角色、声明或自定义条件控制接口访问
  • 希望端点只声明"需要什么权限",判定规则由授权系统集中管理
  • 多认证方案并存时,不同接口需要指定不同的认证方式

前置条件

授权策略依赖以下包:

用途
soulsoft_web_authorization授权框架核心(中间件、策略、Handler、requirement)
soulsoft_web_authentication认证框架,授权阶段按策略执行认证
soulsoft_web_httpIAuthorizeData 元数据接口

所有包均通过 cjpm.toml 声明依赖后即可使用。

快速开始

一个最小但完整的授权示例:注册授权服务、定义命名策略、在端点上引用。

注册部分:addAuthorization() 注册授权服务,addPolicy() 定义命名策略。端点部分:requireAuthorization(["AdminOnly"]) 把策略挂到端点上。

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

这段代码跑了完整链路:请求 /admin → 授权中间件从端点元数据中读到 "AdminOnly" → 查到策略 [requireAuthenticatedUser + role=admin] → 执行认证 → 执行授权 → claim 匹配则放行,不匹配则 403。

核心概念

定位与职责

授权策略处于端点声明和权限判定之间,是一层翻译层

端点声明                    策略                          判定
requireAuthorization()  →  AuthorizationPolicy  →  IAuthorizationHandler
("需要AdminOnly策略")     (requirements列表         (逐个检查requirement
                          + authenticationSchemes)     是否满足)

策略本身不执行判定——它只是把端点上的声明式元数据转换成一组 requirements 和认证方案,真正的判定工作由授权处理器(IAuthorizationHandler)完成。

端点元数据如何变成策略

这是授权中间件的核心逻辑。AuthorizationMiddleware 从端点元数据中提取 IAuthorizeData,交给 AuthorizationPolicy.combine() 做组合:

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

combine() 内部按以下顺序处理每条 IAuthorizeData

  1. policy 名称 → 从 IAuthorizationPolicyProvider 查找命名策略,合并其 requirements 和 authenticationSchemes
  2. roles → 自动添加 RolesAuthorizationRequirement
  3. authenticationSchemes → 加入方案的集合
  4. 上述都没有且没有附加策略 → 使用 defaultPolicy

这一步的结果是一个可执行的 AuthorizationPolicy,其中只有两样东西:requirements 列表和 authenticationSchemes 列表。

三种策略层次

层次配置方式默认值何时生效
端点显式策略requireAuthorization(["名称"])端点标注了策略名称
defaultPolicyoptions.setDefaultPolicy { ... }requireAuthenticatedUser()requireAuthorization() 不传名称时
fallbackPolicyoptions.setFallbackPolicy { ... }None端点无任何授权元数据时

fallbackPolicy 默认为 None 意味着未标注授权元数据的端点直接放行。如果希望全局要求登录,设置 fallbackPolicy 即可:

cangjie
options.setFallbackPolicy { policy =>
    policy.requireAuthenticatedUser()
}

设为 None 可恢复默认行为(关闭全局回退):

cangjie
options.setFallbackPolicy(None)

AllowAnonymous 的作用

allowAnonymous() 不会跳过认证——认证中间件照常执行,context.user 照常填充。它只是告诉授权中间件:授权失败时不要拦截,直接放行

源码中对应的逻辑:

if (endpoint.metadata.getMetadata<IAllowAnonymous>().isSome()) {
    next(context)  // 跳过授权失败处理,但 User 已填充
    return
}

策略缓存

策略缓存是否启用由 IAuthorizationPolicyProvider.allowsCachingPolicies 属性控制。DefaultAuthorizationPolicyProvider 将该属性返回 true,因此默认启用缓存——同一个端点的策略只计算一次,后续请求命中缓存直接返回。自定义 IAuthorizationPolicyProvider 实现时,可通过覆盖 allowsCachingPolicies 属性来控制缓存行为(返回 true 启用,false 关闭),而非由继承关系自动决定。

常见用法

添加命名策略

把常见的访问规则沉淀为命名策略,端点上按名称引用。三种典型模式:

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

requireClaim(claimName) 只检查声明是否存在,requireClaim(claimName, allowedValues) 进一步检查值是否在允许列表中,requireClaim(claimName) { predicate } 用自定义断言校验值。三种重载对应 ClaimsAuthorizationRequirement 的不同构造参数。

多方案认证

当多个认证方案并存时,策略可以通过 authenticationSchemes 指定使用哪个方案认证:

cangjie
options.addPolicy("ApiOnly") { policy =>
    policy.requireAuthenticatedUser()
    policy.addAuthenticationSchemes(["Bearer"])
}

app.mapGet("/api/data") { ctx =>
    ctx.response.write("data")
}.requireAuthorization(["ApiOnly"])

授权阶段会按策略指定的方案列表逐个调用 context.authenticate(scheme),将多个方案的结果合并到 context.user。如果策略没有指定 authenticationSchemes,则复用认证中间件的默认方案结果。

修改默认策略

defaultPolicy 的默认值是"只要登录即可"。如果全局要求更严格的默认规则,可以替换:

cangjie
options.setDefaultPolicy { policy =>
    policy.requireAuthenticatedUser()
    policy.requireClaim("role", "user")
}

修改后,所有 requireAuthorization() 不带参数调用的端点都自动要求 role = "user"

生产建议

  • 先把常见访问规则沉淀为命名策略,再挂到端点上。避免在每个端点重复写相同的 requireClaim 组合——一旦规则变化,只需改策略定义,不用逐个端点修改。
  • defaultPolicyfallbackPolicy 要区分清楚,不要混用defaultPolicy 作用于标注了 requireAuthorization() 的端点,fallbackPolicy 作用于没标注的端点。前者是"要求了授权,但没指定具体规则",后者是"完全没提授权这件事"。
  • 多认证方案项目里,优先明确策略对应的认证方案。如果策略不指定 authenticationSchemes,授权阶段只依赖默认认证方案的结果——当一个端点需要特定方案认证时,策略里显式声明更安全。

常见问题

requireAuthorization() 不传参数时会怎样

使用 defaultPolicy。默认值是 requireAuthenticatedUser()——只要登录了就能访问。可以通过 options.setDefaultPolicy() 修改。

端点上能挂多个策略吗

可以。requireAuthorization(["PolicyA", "PolicyB"]) 会创建两个 IAuthorizeDatacombine() 依次合并每个策略的 requirements 和 authenticationSchemes。最终该端点需要满足所有策略的所有 requirements

requireClaimrequireRole 有什么区别

requireRole("admin")requireClaim(ClaimTypes.Role, ["admin"]) 的简写,内部使用 RolesAuthorizationRequirement,通过 user.isInRole() 判断。功能等价,requireRole 更语义化。

相关专题