认证与授权
认证回答"你是谁",授权回答"你能做什么"。两者解耦:认证产出 HttpContext.user,授权消费它。
打个比方:小区门禁。大门刷卡认出"这是 3 栋 602 的业主"——这是认证。进电梯刷卡只能到 6 楼,按不了 20 楼——这是授权。认证中间件是门禁读卡器,填 HttpContext.user;授权中间件是电梯控制器,用 user 和 Endpoint.metadata 判断能不能放行。
请求 → useRouting() → useAuthentication() → useAuthorization() → 终结点
↓ ↓
填 context.user 读 Endpoint.metadata
+ context.user → 放行/拦截认证中间件
app.useAuthentication() 只做一件事:尝试从请求中解析用户身份,填到 context.user 上。它不返回 401——认证失败请求照样往下走。真正返回 401 的是授权中间件。
请求进入
│
▼
① 有 IAuthenticationRequestHandler?(如 OIDC 登录跳转)
│ ├── 有,且 handler.handleRequest() 返回 true → return(截断管道)
│ └── 没有或未截断 → 继续
▼
② 取默认认证方案 → 创建/获取 handler → handler.authenticate()
│ 成功 → context.user = principal
│ 失败 → 什么都不写
▼
③ next(context)handler 从哪来
AuthenticationHandlerProvider.getHandler(context, scheme) 负责提供 handler 实例:
- 先查同请求内缓存——同一个 scheme 只初始化一次
- 缓存未命中 → 从 DI 获取(或
ActivatorUtilities.createInstance动态创建) - 调
handler.initialize(scheme, context),把 scheme 名、当前 context、IOptionsMonitor注入
initialize() 结束后,handler 拿到了它运行所需的全部上下文:this.scheme、this.context、this.options。
handler.authenticate() 做了什么
// AuthenticationHandler.cj:100 — 你重写的是 handleAuthenticate()
public func authenticate(): AuthenticateResult {
// 1. 如果配置了 forwardAuthenticate,转发到目标 scheme(用于多方案委托)
if (let Some(target) <- resolveTarget(options.forwardAuthenticate)) {
return context.authenticate(target)
}
// 2. 调 handleAuthenticateOnce(),同实例只执行一次(幂等保护)
let result = handleAuthenticateOnce()
// 3. 打日志
if (let Some(failure) <- result.failure) {
logger.info("${scheme.name} was not authenticated. Failure message: ${failure.message}")
}
return result
}三个关键点:
resolveTarget:如果配置了forwardAuthenticate(如forwardAuthenticate = "Bearer"),当前 handler 不做实际认证,而是委托给目标 scheme。这用于"一个方案套另一个"的场景。防自循环——目标如果是自己就跳过。handleAuthenticateOnce:同 handler 实例只执行一次逻辑,第二次调用直接返回缓存结果。这避免中间件和授权中间件反复认证同一个 scheme。handleAuthenticate():你的代码在这里。读 Cookie、解析 Token、调外部服务...成功返回AuthenticateResult.success(ticket),失败返回AuthenticateResult.fail("原因")或AuthenticateResult.noResult()。
认证结果如何传递到授权中间件
认证中间件把结果写到了两个地方:
context.user = principal ← 授权中间件直接读它
context.features.set<IAuthenticateResultFeature>() ← 授权中间件的 PolicyEvaluator 读它授权中间件的 ② 步骤(DefaultPolicyEvaluator.authenticate())会检查 IAuthenticateResultFeature——如果认证中间件已经执行过,它直接用已有结果,不再重复认证。
认证失败不会自动返回 401
认证失败时 middleware 什么都不做——不设 statusCode,不抛异常,不写 response。 next(context) 照常调用。
401 是由后续的授权中间件触发的:它发现用户未认证 + 策略要求认证 → challenge() → 调认证方案的 handleChallenge() → 默认实现是 context.response.statusCode = 401。
授权中间件
app.useAuthorization() 的作用:读取 Endpoint.metadata 和 context.user,决定放行、challenge()(401)还是 forbid()(403)。
整个流程分五步,下面逐一拆解。
请求进入 AuthorizationMiddleware
│
▼
① 解析策略:Endpoint.metadata → AuthorizationPolicy
│
├── 没解析出策略(无 [Authorize] 且无 fallbackPolicy)
│ → 直接 next(),跳过所有授权逻辑
│
└── 解析出策略 → 继续
│
▼
② 执行认证:按策略指定的 scheme 调 context.authenticate()
│
▼
③ AllowAnonymous?→ next(),授权失败不拦截
│
▼
④ 执行授权:遍历所有 IAuthorizationHandler 处理 requirements
│
├── pendingRequirements 全部清空 → 放行
│
└── 有残留 → 看认证结果
├── 未认证 → challenged → ⑤ → 401
└── 已认证 → forbidden → ⑤ → 403① 解析策略:从 Endpoint 到 AuthorizationPolicy
AuthorizationMiddleware.invoke() 先调 resolvePolicy(),把端点上的元数据组合成一个 AuthorizationPolicy。
关键代码在这条链路上:
// AuthorizationMiddleware.cj:113
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) // ②
}第 ① 步:提取元数据。 requireAuthorization()、requireAuthorization(["X"])、allowAnonymous() 都会在 endpoint 上写入 IAuthorizeData。这是"端点声明了什么",还不是最终的策略对象。
第 ② 步:组合成策略。 AuthorizationPolicy.combine() 拿元数据去查实际的 AuthorizationPolicy 对象,合并 requirements 和 authenticationSchemes:
IAuthorizeData.policy = "AdminOnly"
│
▼
policyProvider.getPolicy("AdminOnly")
│
▼
AuthorizationPolicy {
requirements: [DenyAnonymousAuthorizationRequirement, ClaimsAuthorizationRequirement("role", ["admin"])],
authenticationSchemes: []
}如果 IAuthorizeData.policy 为空(即 requireAuthorization() 没传名字),就用 defaultPolicy。
如果端点没有任何 IAuthorizeData,就看 fallbackPolicy 有没有配——配了就全局生效,没配就跳过授权。
总结:元数据 → 策略查找 → 合并,结果是包含 requirements 和 authenticationSchemes 的 AuthorizationPolicy。
② 执行认证:按策略指定的 scheme 认证
策略的 authenticationSchemes 字段决定用什么方案认证。比如多方案并存时:
// 策略指定用 Bearer 方案
options.addPolicy("ApiOnly") { policy =>
policy.requireAuthenticatedUser()
policy.addAuthenticationSchemes(["Bearer"])
}DefaultPolicyEvaluator.authenticate() 遍历 schemes,逐个调 context.authenticate(scheme),把多个方案的结果合并到 context.user:
// DefaultPolicyEvaluator.cj:38
for (scheme in policy.authenticationSchemes) {
let result = context.authenticate(scheme) // 调该方案的 handler
if (result.succeeded) {
newPrincipal = SecurityHelper.mergeUserPrincipal(newPrincipal, result.principal)
}
}
context.user = newPrincipal // 最终挂到 context 上如果策略没有指定 authenticationSchemes(多数情况),就走默认路径:读 IAuthenticateResultFeature(认证中间件写的),或直接用 context.user。
无论认证成功还是失败,请求都继续往下走。 不会在这一步返回 401。
③ AllowAnonymous:跳过授权失败处理
AllowAnonymous 端点仍然会走 ① 和 ②——认证照做,context.user 照样填。只是在授权失败时不拦截,始终 next()。
④ 执行授权:遍历 handler,清空 pendingRequirements
这是核心环节。DefaultAuthorizationService.authorize() 干三件事:
1. 创建 AuthorizationHandlerContext
→ pendingRequirements = policy.requirements 的副本
2. 从 DI 收集所有 IAuthorizationHandler,逐个调 handler.handle(context)
3. IAuthorizationEvaluator 检查 pendingRequirements 是否全部清空handler 从哪来? DefaultAuthorizationHandlerProvider 从 DI 收集所有 IAuthorizationHandler 注册。包括两类:
| 来源 | 说明 |
|---|---|
addAuthorization() 自动注册的 | PassThroughAuthorizationHandler(处理自处理的 requirement) |
手动 addTransient 注册的 | 如 RiskLevelHandler(分离模式的 handler) |
handler 做了什么? 以 DenyAnonymousAuthorizationRequirement 为例:
// 继承 AuthorizationHandler<DenyAnonymousAuthorizationRequirement>
// handle() 从 context.requirements 中筛出 DenyAnonymous 类型
// 然后调 handleRequirement()
protected func handleRequirement(context, requirement): Unit {
if (context.user.identity?.isAuthenticated ?? false) {
context.succeed(requirement) // 从 pendingRequirements 中移除
}
}每个 handler 处理自己关心的 requirement 类型。满足条件就 context.succeed(requirement)——从 pendingRequirements 中删掉该项。
最终判定: IAuthorizationEvaluator.evaluate(context) 检查:
hasSucceeded = !failCalled && succeedCalled && pendingRequirements.size == 0所有 requirement 都被 succeed 了 → 放行。有一个没被 succeed → 失败。
⑤ 处理结果:challenge 还是 forbid
DefaultPolicyEvaluator.authorize() 根据授权结果和认证结果做最终裁决:
// DefaultPolicyEvaluator.cj:100
if (result.succeeded) {
return PolicyAuthorizationResult.success() // → 放行
}
if (authenticationResult.succeeded) {
return PolicyAuthorizationResult.forbid() // → 403,已认证但权限不够
} else {
return PolicyAuthorizationResult.challenge() // → 401,没认证所以不给访问
}DefaultAuthorizationMiddlewareResultHandler 收到结果后:
succeeded→next(context)challenged→context.challenge(scheme)→ 触发认证方案的 challenge 逻辑 → 401forbidden→context.forbid(scheme)→ 403
这就是最终 HTTP 状态码的来源。
Endpoint 授权元数据
// 默认策略
app.mapGet("/profile") { ctx =>
ctx.response.write("profile")
}.requireAuthorization()
// 命名策略
app.mapGet("/admin") { ctx =>
ctx.response.write("admin")
}.requireAuthorization(["AdminOnly"])
// 显式匿名
app.mapGet("/ping") { ctx =>
ctx.response.write("pong")
}.allowAnonymous()策略选择规则
| 端点状态 | 行为 |
|---|---|
| 无授权元数据 + 无 fallbackPolicy | 直接放行 |
| 无授权元数据 + 有 fallbackPolicy | 使用 fallbackPolicy |
requireAuthorization() | 使用 defaultPolicy |
requireAuthorization(["X"]) | 按名称取策略 |
allowAnonymous() | 认证照做,跳过授权失败处理 |
defaultPolicy 的默认值是 requireAuthenticatedUser()——只要登录了就能访问。fallbackPolicy 默认为 None——不设时无授权元数据的端点直接放行。
修改默认值和全局回退策略:
builder.services.addAuthorization() { options =>
// 修改默认策略:除了登录,还要求 role = "user"
options.setDefaultPolicy { policy =>
policy.requireAuthenticatedUser()
policy.requireClaim("role", "user")
}
// 设置全局回退:所有没有 [Authorize] 的端点都要登录
options.setFallbackPolicy { policy =>
policy.requireAuthenticatedUser()
}
// 设为 None 关闭回退策略(恢复默认行为)
// options.setFallbackPolicy(None)
}添加策略
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 }
}
}JWT Bearer 认证
JWT Bearer 负责把 Authorization: Bearer <token> 解析成 ClaimsPrincipal。
完整示例
import std.time.*
import soulsoft_web_http.*
import soulsoft_web_routing.*
import soulsoft_web_hosting.*
import soulsoft_identity_claims.*
import soulsoft_identity_tokens.*
import soulsoft_web_authorization.*
import soulsoft_web_authentication.*
import soulsoft_identity_tokens_jwt.*
import soulsoft_extensions_injection.*
import soulsoft_web_authentication_jwtbearer.*
main(args: Array<String>): Int64 {
let builder = WebHost.createBuilder(args)
builder.services.addRouting()
let signingKey = SymmetricSecurityKey("your-256-bit-secret-your-256-bit-secret".toArray())
// 注册认证方案(校验密钥必须和签发密钥一致)
builder.services.addAuthentication(JwtBearerDefaults.Scheme)
.addJwtBearer(JwtBearerDefaults.Scheme) { options =>
options.tokenValidationParameters.validateIssuer = true
options.tokenValidationParameters.validIssuers = ["spire-demo"]
options.tokenValidationParameters.validateAudience = true
options.tokenValidationParameters.validAudiences = ["spire-api"]
options.tokenValidationParameters.issuerSigningKeys = [signingKey]
}
// 注册授权策略
builder.services.addAuthorization() { options =>
options.addPolicy("AdminOnly") { policy =>
policy.requireClaim("role", ["admin"])
}
}
let app = builder.build()
app.useRouting()
app.useAuthentication()
app.useAuthorization()
// 创建令牌(用相同的 signingKey 签发,校验才能通过)
app.mapPost("/token") { ctx =>
let creds = SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256)
let descriptor = SecurityTokenDescriptor(creds)
descriptor.issuer = "spire-demo"
descriptor.audience = "spire-api"
descriptor.expires = DateTime.nowUTC().addSeconds(3600)
descriptor.claims = [Claim("role", "admin")]
let token = JwtSecurityTokenHandler().createEncodedJwt(descriptor)
ctx.response.write(token)
}
app.mapGet("/public") { ctx =>
ctx.response.write("public")
}.allowAnonymous()
app.mapGet("/admin") { ctx =>
ctx.response.write("admin")
}.requireAuthorization(["AdminOnly"])
app.run()
return 0
}配置文件驱动
{
"authentication": {
"schemes": {
"Bearer": {
"challenge": "Bearer",
"saveToken": true,
"validateIssuer": true,
"validateAudience": true,
"validateLifetime": true,
"validIssuers": ["spire-demo"],
"validAudiences": ["spire-api"],
"signingKeys": [
{
"issuer": "spire-demo",
"value": "Base64EncodedKeyBytes"
}
]
}
}
}
}OIDC 元数据
builder.services.addAuthentication(JwtBearerDefaults.Scheme)
.addJwtBearer(JwtBearerDefaults.Scheme) { options =>
options.metadataAddress = "https://demo.duendesoftware.com/.well-known/openid-configuration"
options.requireHttpsMetadata = true
}事件扩展
在 addJwtBearer 的配置回调中通过 options.events 挂事件:
builder.services.addAuthentication(JwtBearerDefaults.Scheme)
.addJwtBearer(JwtBearerDefaults.Scheme) { options =>
// ... 其他配置 ...
// 自定义 Token 来源:默认从 Authorization header 取,这里改成从 query string 取
options.events.onMessageReceived = { context =>
context.token = context.request.query["access_token"]
}
// 自定义 401 响应内容
options.events.onChallenge = { context =>
context.handleResponse()
context.response.statusCode = 401
context.response.write("invalid or missing bearer token")
}
}Cookie 认证
Cookie 方案适合浏览器登录态。票据保护后写入 Cookie,请求时还原。
与 Bearer 的核心区别:challenge() 默认跳登录页(302),forbid() 默认跳拒绝页——只有 Ajax 请求才返回 401/403。
完整示例
import std.time.*
import soulsoft_web_http.*
import soulsoft_web_routing.*
import soulsoft_web_hosting.*
import soulsoft_identity_claims.*
import soulsoft_web_authorization.*
import soulsoft_web_authentication.*
import soulsoft_extensions_injection.*
import soulsoft_web_authentication_cookies.*
main(args: Array<String>): Int64 {
let builder = WebHost.createBuilder(args)
builder.services.addRouting()
builder.services.addAuthentication(CookieAuthenticationDefaults.Scheme)
.addCookie(CookieAuthenticationDefaults.Scheme) { options =>
options.loginPath = PathString("/login")
options.logoutPath = PathString("/logout")
options.accessDeniedPath = PathString("/denied")
options.expireTimeSpan = Duration.day * 7
options.slidingExpiration = true
options.cookie.name = ".spire.auth"
options.cookie.httpOnly = true
}
builder.services.addAuthorization()
let app = builder.build()
app.useRouting()
app.useAuthentication()
app.useAuthorization()
app.mapPost("/login") { ctx =>
let identity = ClaimsIdentity(Some(CookieAuthenticationDefaults.Scheme))
identity.addClaim(ClaimTypes.Name, "spire")
identity.addClaim("role", "admin")
let principal = ClaimsPrincipal([identity])
let properties = AuthenticationProperties()
properties.isPersistent = true
properties.redirectUri = "/profile"
ctx.signIn(principal, properties)
}
app.mapPost("/logout") { ctx =>
let properties = AuthenticationProperties()
properties.redirectUri = "/"
ctx.signOut(properties)
}
app.mapGet("/profile") { ctx =>
let name = ctx.user.findFirstValue(ClaimTypes.Name) ?? "anonymous"
ctx.response.write("hello ${name}")
}.requireAuthorization()
app.run()
return 0
}服务端会话存储
完整票据存服务端,Cookie 只保留会话键:
public class MemoryTicketStore <: ITicketStore {
private let tickets = HashMap<String, AuthenticationTicket>()
public func store(ticket: AuthenticationTicket): String {
let key = "sid-" + DateTime.now().toString()
tickets[key] = ticket
return key
}
public func renew(key: String, ticket: AuthenticationTicket): Unit {
tickets[key] = ticket
}
public func retrieve(key: String): ?AuthenticationTicket {
tickets[key]
}
public func remove(key: String): ?AuthenticationTicket {
let value = tickets[key]
tickets.remove(key)
return value
}
}
// 注册
options.sessionStore = MemoryTicketStore()事件扩展
在 addCookie 的配置回调中通过 options.events 挂事件:
builder.services.addAuthentication(CookieAuthenticationDefaults.Scheme)
.addCookie(CookieAuthenticationDefaults.Scheme) { options =>
// ... 其他配置 ...
// 自定义主体验证:role = "disabled" 的用户禁止登录
options.events.onValidatePrincipal = { context =>
let role = context.principal.flatMap { p => p.findFirstValue("role") } ?? "guest"
if (role == "disabled") {
context.rejectPrincipal()
}
}
// 把默认的 302 跳转改成直接返回 401(适合 API 场景)
options.events.onRedirectToLogin = { context =>
context.response.statusCode = StatusCodes.Unauthorized
context.response.write("please login first")
}
}自定义认证方案
自定义方案只需三步:定义 Options → 继承 AuthenticationHandler<TOptions> → 重写 handleAuthenticate()。
Basic 认证示例
Options、Handler、Builder 扩展三步走,和 JWT Bearer 同款风格。
import soulsoft_web_http.*
import soulsoft_web_authentication.*
import soulsoft_identity_claims.*
import soulsoft_extensions_options.*
import soulsoft_extensions_logging.*
import stdx.encoding.base64.*
public class BasicAuthenticationOptions <: AuthenticationSchemeOptions {
public var realm: String = "spire"
public var username: String = "admin"
public var password: String = "123456"
}
public class BasicAuthenticationHandler <: AuthenticationHandler<BasicAuthenticationOptions> {
public init(options: IOptionsMonitor<BasicAuthenticationOptions>, logFactory: ILoggerFactory) {
super(options, logFactory)
}
public func handleAuthenticate(): AuthenticateResult {
let header = this.context.request.headers.authorization.first ?? String.empty
if (header.isEmpty() || !header.startsWith("Basic ")) {
return AuthenticateResult.noResult()
}
let credential = header[6..]
if (let Some(bytes) <- fromBase64String(credential)) {
unsafe {
let pairStr = String.withRawData(bytes)
let pair = pairStr.split(":")
if (pair.size == 2 && pair[0] == this.options.username && pair[1] == this.options.password) {
let identity = ClaimsIdentity(Some(this.scheme.name))
identity.addClaim(ClaimTypes.Name, pair[0])
let principal = ClaimsPrincipal([identity])
let ticket = AuthenticationTicket(principal, this.scheme.name)
return AuthenticateResult.success(ticket)
}
}
}
return AuthenticateResult.fail("Invalid credentials")
}
}
// 扩展 AuthenticationBuilder,提供 addBasic() 入口
extend AuthenticationBuilder {
public func addBasic(authenticateScheme: String,
configureOptions: (BasicAuthenticationOptions) -> Unit): AuthenticationBuilder {
this.addScheme<BasicAuthenticationOptions, BasicAuthenticationHandler>(
authenticateScheme, None, configureOptions)
}
}注册:
builder.services.addAuthentication("Basic")
.addBasic("Basic") { options =>
options.username = "admin"
options.password = "123456"
}自定义授权要求
当内置的 requireClaim、requireAuthenticatedUser 不够用时——比如需要判断当前时间、查询数据库、调用外部服务——可以实现自定义授权要求。
有两种实现方式:处理逻辑不需要外部依赖时用自处理模式(一个类全搞定);需要注入 ILoggerFactory、数据库等依赖时用分离模式(Requirement 和 Handler 拆成两个类)。
自处理模式
Requirement 同时实现 IAuthorizationRequirement 和 AuthorizationHandler<Self>,PassThroughAuthorizationHandler 自动发现并执行,无需手动注册 handler。
import std.time.*
import soulsoft_web_authorization.*
// 仅工作时间(9:00-18:00)可访问
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)
}
}
}注册:policy.requirements.add() 传入实例即可。
builder.services.addAuthorization() { options =>
options.addPolicy("WorkingHoursOnly") { policy =>
policy.requireAuthenticatedUser()
policy.requirements.add(WorkingHoursRequirement(9, 18))
}
}
app.mapGet("/admin") { ctx =>
ctx.response.write("admin")
}.requireAuthorization(["WorkingHoursOnly"])运行结果:
| 条件 | 结果 |
|---|---|
| 已认证 + 9-18 之间 | succeed() → 放行 |
| 已认证 + 非工作时间 | 不调用 succeed → forbid() → 403 |
| 未认证 | requireAuthenticatedUser 未满足 → challenge() → 401 |
分离模式
当 handler 需要注入 ILoggerFactory、数据库、HTTP 客户端等依赖时,必须把 Requirement 和 Handler 拆开——自处理模式无法利用 DI。
Requirement:纯数据,仅实现 IAuthorizationRequirement。
Handler:继承 AuthorizationHandler<TRequirement>,通过构造函数注入所需依赖。
import soulsoft_web_authorization.*
import soulsoft_extensions_logging.*
// Requirement:纯数据
public class RiskLevelRequirement <: IAuthorizationRequirement {
public let minimumLevel: Int64
public init(minimumLevel: Int64) {
this.minimumLevel = minimumLevel
}
}
// Handler:注入 ILoggerFactory,创建 logger 记录审计日志
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 注册到 DI——两者分开:
builder.services.addAuthorization() { options =>
options.addPolicy("RiskLevel3") { policy =>
policy.requireAuthenticatedUser()
policy.requirements.add(RiskLevelRequirement(3))
}
}
// Handler 必须手动注册,DefaultAuthorizationHandlerProvider 从 DI 收集所有 IAuthorizationHandler
builder.services.addTransient<IAuthorizationHandler, RiskLevelHandler>()两种模式对比
| 自处理 | 分离 | |
|---|---|---|
| 类数量 | 1 个 | 2 个(Requirement + Handler) |
| Requirement 继承 | IAuthorizationRequirement + AuthorizationHandler<Self> | 仅 IAuthorizationRequirement |
| Handler 继承 | 无(Requirement 就是 Handler) | AuthorizationHandler<TRequirement> |
| DI 注册 Handler | 无需(PassThroughAuthorizationHandler 自动发现) | 必须 addTransient<IAuthorizationHandler, THandler>() |
| Handler 可注入依赖 | 不行 | 可以(ILoggerFactory、数据库、HTTP 客户端等) |
| 适用场景 | 逻辑简单、独立判断 | 需要外部服务、复杂逻辑、日志记录 |
常见问题
JWT vs Cookie 怎么选?
API(移动端、SPA)用 JWT Bearer;浏览器登录态用 Cookie。
Cookie 方案默认跳转而不是返回 401
如果用在 API 场景,通过 onRedirectToLogin 事件改写成直接返回 401。
是否支持多方案并存?
支持。注册多个认证方案,不同终结点用不同方案校验:
// 同时注册 JWT 和 Basic
builder.services.addAuthentication(JwtBearerDefaults.Scheme)
.addJwtBearer(JwtBearerDefaults.Scheme) { options =>
options.tokenValidationParameters.issuerSigningKeys = [signingKey]
}
.addBasic("Basic") { options =>
options.username = "admin"
options.password = "123456"
}
// 默认方案是 JwtBearer,Basic 方案需显式指定
app.mapGet("/api/data") { ctx =>
ctx.response.write("jwt 校验通过")
}.requireAuthorization() // 走默认 JwtBearer
app.mapGet("/api/admin") { ctx =>
ctx.response.write("basic 校验通过")
}.requireAuthorization(["Basic"]) // 指定走 Basic 方案每个方案的 handler 互不干扰,useAuthentication() 按默认方案执行认证,授权时可按终结点指定不同策略。同一个请求也可以携带多种凭证——比如 Cookie 做登录态、Bearer 做 API 鉴权。