JWT Bearer 认证
JWT Bearer 方案负责把请求里的访问令牌解析成 ClaimsPrincipal,并挂到 HttpContext.user。
它只负责“这是谁”,不直接决定“能不能访问”。真正触发 401 或 403,通常发生在:
- 终结点上的
.requireAuthorization(...) - 授权中间件里的
challenge()/forbid() - 代码里显式调用
context.challenge()/context.forbid()
最小示例
cangjie
import soulsoft_web_hosting.*
import soulsoft_web_http.*
import soulsoft_web_routing.*
import soulsoft_web_authorization.*
import soulsoft_web_authentication.*
import soulsoft_web_authentication_jwtbearer.*
import soulsoft_identity_tokens.*
import soulsoft_extensions_injection.*
main(args: Array<String>): Int64 {
let builder = WebHost.createBuilder(args)
builder.services.addRouting()
// 把 Bearer 设为默认认证方案
builder.services.addAuthentication(JwtBearerDefaults.Scheme)
.addJwtBearer(JwtBearerDefaults.Scheme) { options =>
let key = SymmetricSecurityKey("your-256-bit-secret-your-256-bit-secret".toArray())
// 令牌校验规则
options.tokenValidationParameters.validateIssuer = true
options.tokenValidationParameters.validateAudience = true
options.tokenValidationParameters.validateLifetime = true
options.tokenValidationParameters.requireExpirationTime = true
options.tokenValidationParameters.validIssuers = ["spire-demo"]
options.tokenValidationParameters.validAudiences = ["spire-api"]
options.tokenValidationParameters.issuerSigningKeys = [key]
}
builder.services.addAuthorization()
let app = builder.build()
// 推荐顺序:先路由,再认证,再授权
app.useRouting()
app.useAuthentication()
app.useAuthorization()
// 终结点元数据要求已认证用户
app.mapGet("/profile") { ctx =>
ctx.response.write("hello jwt")
}.requireAuthorization()
app.run()
return 0
}addJwtBearer(...) 注册的不只是一个 Handler。它实际做了三件事:
- 注册
JwtBearerConfigureOptions,支持从配置树读取方案参数 - 注册
JwtBearerConfigureAfterOptions,补齐 OIDC 元数据和configurationManager - 把方案名绑定到
JwtBearerHandler
请求时序
执行流程
useAuthentication()先调用默认认证方案;如果成功,AuthenticationMiddleware才会写入context.user。JwtBearerHandler.handleAuthenticate()先触发options.events.onMessageReceived。这里可以自己放入context.token,也可以直接设置context.result提前结束。- 如果事件没有给出 token,处理器再读取
Authorization头,并且只接受Bearer前缀;前缀判断不区分大小写。 - 取到 token 后,处理器会准备
TokenValidationParameters。如果存在configurationManager,会把远端配置里的issuer和signingKeys合并到本地参数副本。 - 处理器依次调用
options.securityTokenValidators。默认只有一个JwtSecurityTokenHandler()。 - 验证成功后触发
onTokenValidated;如果saveToken = true,会把当前访问令牌以access_token名称存进AuthenticationProperties。 - 验证失败后触发
onAuthenticationFailed;事件如果写入了context.result,就以事件结果为准,否则返回失败结果。 challenge()负责401,forbid()负责403。前者通常表示“还没通过认证”,后者表示“已经认证,但不满足授权策略”。
终结点授权与 JWT 的边界
这个边界最值得注意:
useAuthentication()只尝试解析身份,不会因为缺少 Bearer Token 自动返回401.requireAuthorization()才会消费身份,并在需要时触发challenge()或forbid()- 没带 Token、用了错误方案、Token 无效,通常都会落到
challenge(),最终返回401 - Token 有效,但终结点策略不满足,例如
.requireAuthorization(["AdminOnly"]),会走forbid(),最终返回403
所以 JWT Bearer 与授权元数据是协作关系,不是替代关系。
Challenge 与 Forbid
JwtBearerHandler 对这两个出口做了明确区分:
handleChallenge()会先调用handleAuthenticateOnceSafe(),然后构造WWW-Authenticate- 如果
includeErrorDetails = true且本次存在认证异常,会自动补上error="invalid_token"和error_description error_description会过滤CR/LF,避免非法响应头options.events.onChallenge可以调用context.handleResponse(),完全接管默认401响应handleForbidden()只设置403,然后触发options.events.onForbidden
这意味着:
- “未认证”与“已认证但被拒绝”在响应语义上是分开的
- 你可以改写质询响应,但不用改 Handler 主体
配置方式
代码配置本地验证参数
cangjie
builder.services.addAuthentication(JwtBearerDefaults.Scheme)
.addJwtBearer(JwtBearerDefaults.Scheme) { options =>
let key = SymmetricSecurityKey("your-256-bit-secret-your-256-bit-secret".toArray())
// 本地静态验证参数
options.tokenValidationParameters.validateIssuer = true
options.tokenValidationParameters.validateAudience = true
options.tokenValidationParameters.validateLifetime = true
options.tokenValidationParameters.validIssuers = ["spire-demo"]
options.tokenValidationParameters.validAudiences = ["spire-api"]
options.tokenValidationParameters.issuerSigningKeys = [key]
}创建令牌终结点
如果你使用本地对称密钥校验,通常还会提供一个发放 JWT 的终结点,方便本地联调。
cangjie
import std.collection.*
import std.time.*
import stdx.encoding.json.*
import soulsoft_identity_claims.*
import soulsoft_identity_tokens.*
import soulsoft_identity_tokens_jwt.*
app.mapPost("/token") { ctx =>
let key = SymmetricSecurityKey("your-256-bit-secret-your-256-bit-secret".toArray())
let creds = SigningCredentials(key, SecurityAlgorithms.HmacSha256)
let claims = ArrayList<Claim>()
// 令牌里的主体与业务声明
claims.add(Claim(ClaimTypes.Name, "spire"))
claims.add(Claim("role", "admin"))
let descriptor = SecurityTokenDescriptor(creds)
descriptor.issuer = Some("spire-demo")
descriptor.audience = Some("spire-api")
descriptor.issuedAt = Some(DateTime.nowUTC())
descriptor.expires = Some(DateTime.nowUTC().addSeconds(3600))
descriptor.claims = claims
let handler = JwtSecurityTokenHandler()
let token = handler.createEncodedJwt(descriptor)
let obj = JsonObject()
obj.put("access_token", JsonString(token))
obj.put("token_type", JsonString("Bearer"))
obj.put("expires_in", JsonInt(3600))
ctx.response.contentType = "application/json"
ctx.response.write(obj.toString())
}配置文件驱动
JwtBearerConfigureOptions 会读取:
text
authentication:schemes:<scheme>例如:
json
{
"authentication": {
"schemes": {
"Bearer": {
"challenge": "Bearer",
"includeErrorDetails": true,
"saveToken": true,
"validateIssuer": true,
"requireAudience": true,
"validateAudience": true,
"validateLifetime": true,
"requireExpirationTime": true,
"clockSkew": 300,
"validIssuers": ["spire-demo"],
"validAudiences": ["spire-api"],
"signingKeys": [
{
"issuer": "spire-demo",
"value": "Base64EncodedKeyBytes"
}
]
}
}
}
}这里有两个实现细节需要注意:
signingKeys会按issuer去匹配validIssuersvalue会先做 Base64 解码,再构造成SymmetricSecurityKey
使用 OIDC 元数据
cangjie
builder.services.addAuthentication(JwtBearerDefaults.Scheme)
.addJwtBearer(JwtBearerDefaults.Scheme) { options =>
// 直接指定 OIDC 元数据地址
options.metadataAddress = "https://demo.duendesoftware.com/.well-known/openid-configuration"
// 远端元数据默认要求 HTTPS
options.requireHttpsMetadata = true
}JwtBearerConfigureAfterOptions 的规则是:
- 如果已经提供了
configurationManager,不再做任何补充 - 否则如果提供了
configuration,会包装成StaticConfigurationManager - 否则如果提供了
metadataAddress或authority,会创建远端ConfigurationManager - 只提供
authority时,框架会自动补成/.well-known/openid-configuration - 当
requireHttpsMetadata = true时,元数据地址必须是https://
事件与扩展点
这套设计的可扩展点非常干净:令牌来源、验证完成后的处理、失败后的响应,都不需要改 JwtBearerHandler 本体。
cangjie
builder.services.addAuthentication(JwtBearerDefaults.Scheme)
.addJwtBearer(JwtBearerDefaults.Scheme) { options =>
// 允许从 query 中读取 access_token
options.events.onMessageReceived = { context =>
context.token = context.request.query["access_token"]
}
// 接管默认 challenge 响应
options.events.onChallenge = { context =>
context.handleResponse()
context.response.statusCode = 401
context.response.write("invalid or missing bearer token")
}
}如果你需要更换底层验证器,也可以直接替换:
options.securityTokenValidators
默认实现是 JwtSecurityTokenHandler(),但架构并没有把验证逻辑写死在 Handler 里。