自定义Basic认证
当内置 Bearer、Cookie 等方案不满足需求时,通过继承
AuthenticationHandler实现自定义认证协议——以 Basic 认证为完整示例。
适用场景
- 需要接入 Basic、HMAC、签名头、网关票据等自定义协议
- 希望统一复用 Spire 认证中间件与授权体系
- 不想把认证逻辑直接散落在业务代码里
不适合能用 JWT Bearer 或 Cookie 解决的标准场景——优先复用内置方案,只有协议确实不兼容时才自定义。
前置条件
自定义认证方案依赖以下包:
| 包 | 用途 |
|---|---|
soulsoft_web_authentication | 认证框架核心(AuthenticationHandler 基类、AuthenticationBuilder、结果类型) |
soulsoft_identity_claims | 身份声明模型(Claim、ClaimsIdentity、ClaimsPrincipal) |
soulsoft_extensions_options | IOptionsMonitor 接口,用于在 Handler 中读取配置 |
如需日志记录,额外依赖 soulsoft_extensions_logging。
快速开始
下面以 Basic 认证为例,展示完整的自定义方案实现。分三段:定义选项 → 实现 Handler → 注册入口。
核心代码分两段:BasicAuthenticationHandler.handleAuthenticate() 是唯一需要编写的业务逻辑,extend AuthenticationBuilder 是暴露给调用方的注册入口。
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")
}
}
extend AuthenticationBuilder {
public func addBasic(authenticateScheme: String,
configureOptions: (BasicAuthenticationOptions) -> Unit): AuthenticationBuilder {
this.addScheme<BasicAuthenticationOptions, BasicAuthenticationHandler>(
authenticateScheme, None, configureOptions)
}
}注册时调用 addBasic(),与内置方案完全一致的风格:
builder.services.addAuthentication("Basic")
.addBasic("Basic") { options =>
options.username = "admin"
options.password = "123456"
}这段代码接入后,请求携带 Authorization: Basic YWRtaW46MTIzNDU2 头即可通过认证。认证成功后的授权流程与 JWT Bearer、Cookie 完全一致——都由同一套授权中间件处理。
核心概念
定位与职责
自定义 Basic 认证方案分三步:
- 定义
Options——继承AuthenticationSchemeOptions,承载方案的配置参数 - 继承
AuthenticationHandler<TOptions>——重写handleAuthenticate(),实现凭证读取与校验 - 通过
extend AuthenticationBuilder暴露addXxx()注册入口——让调用方像使用内置方案一样注册
框架接管了中间件调度、handler 初始化与缓存、转发逻辑——你只需关注凭证如何读取、如何校验、成功时如何创建票据。
handleAuthenticate() 才是认证逻辑入口
中间件、handler 初始化、缓存和转发逻辑由框架接管。
你真正需要实现的是:
- 如何读取凭证——从请求头、请求体还是 query string
- 如何校验凭证——查数据库、验签名、对密码
- 成功时如何创建
AuthenticationTicket——构建ClaimsPrincipal并包装为票据
noResult() 与 fail() 不一样
AuthenticateResult.noResult():当前请求里没有可处理的凭证(如没有 Authorization 头或不是 Basic 格式)。表示"跳过这个请求",不产生副作用。AuthenticateResult.fail(...):确实拿到了凭证,但校验失败(如密码错误、签名不匹配)。表示"凭证无效",会影响后续的 challenge 行为和诊断日志。
两者的选择直接影响多方案并存时的行为:noResult() 让框架有机会尝试下一个方案,fail() 则终结认证链路。
三种返回结果的决策指南
| 场景 | 返回结果 | 示例 |
|---|---|---|
| 请求头不存在或格式不匹配 | noResult() | 没有 Authorization 头、不是 "Basic " 前缀 |
| 凭证存在但校验失败 | fail("原因") | 密码错误、Base64 解码失败、签名不匹配 |
| 凭证校验通过 | success(ticket) | 用户名密码匹配,构建 ClaimsPrincipal |
注意:success() 的 AuthenticationTicket 中 ClaimsIdentity 的 authenticationType 参数会影响 isAuthenticated 的判定——如果传 None,即使认证"成功",授权阶段也会认为用户未认证。
常见用法
多方案并存
自定义方案可以和内置方案同时注册,由不同接口指定使用哪个方案:
builder.services.addAuthentication(JwtBearerDefaults.Scheme)
.addJwtBearer(JwtBearerDefaults.Scheme) { options =>
// JWT Bearer 配置...
}
.addBasic("Basic") { options =>
options.username = "admin"
options.password = "123456"
}
// 默认走 JWT Bearer
app.mapGet("/api/data") { ctx => ctx.response.write("data") }
.requireAuthorization()
// 显式指定走 Basic
app.mapGet("/api/admin") { ctx => ctx.response.write("admin") }
.requireAuthorization(["Basic"])每个方案的 handler 互不干扰。useAuthentication() 按默认方案执行认证,requireAuthorization(["Basic"]) 则让授权阶段按指定方案认证。
配合授权策略
自定义方案认证成功后,可以和内置方案一样使用授权策略做细粒度权限控制:
builder.services.addAuthorization() { options =>
options.addPolicy("AdminOnly") { policy =>
policy.requireClaim("role", ["admin"])
}
}
app.mapGet("/admin") { ctx =>
ctx.response.write("admin")
}.requireAuthorization(["AdminOnly"])认证只负责"用户是谁",授权策略负责"这个用户能不能访问"——两者的分工与内置方案完全一致。
生产建议
- 优先复用现有 Bearer / Cookie / OIDC 方案,只有确实需要时再自定义。内置方案经过充分测试,自定义方案的密钥管理、时效控制、安全防护都需要自己保证。
- 自定义方案也应尽量接入配置、日志和统一 challenge 行为。不要让自定义方案成为"野生代码"——至少要做到配置化(通过 Options)、可排查(通过 ILoggerFactory)、行为一致(challenge 返回合理的 HTTP 状态码)。
- 如果需要多方案并存,配合认证转发和授权策略一起设计。规划好哪个方案是默认方案、哪些接口用特定方案、认证失败时是否 forward 到另一个方案重试。
常见问题
自定义方案如何接入配置文件
继承 AuthenticationSchemeOptions 本身不会自动绑定配置文件。JWT Bearer 等内置方案之所以支持从 authentication.schemes.<方案名> 节点读取配置,是因为它们额外实现了 IConfigureNamedOptions<TOptions>(如 JwtBearerConfigureOptions)并显式从配置节读取属性。自定义方案如需同样的配置绑定能力,需要自行实现对应的 IConfigureNamedOptions<YourOptions> 并注册到 DI 容器。
handleAuthenticate() 里能调用异步操作吗
当前框架的认证处理器不支持异步。如果凭证校验需要访问数据库或外部服务,建议在校验前通过事件扩展或中间件预处理,将必要数据挂到 HttpContext 上,再在 handleAuthenticate() 中读取。
fail() 和 noResult() 之后请求会怎样
两者都不会让请求立即中断。认证中间件只是不走入 context.user,请求继续往后走到授权中间件。授权中间件根据端点是否标注 requireAuthorization() 决定:未标注的接口直接放行,标注了的接口触发 challenge() 返回 401。
相关专题
- 需要理解认证整体体系 → 认证总览
- 需要标准 API 认证 → JWT Bearer 认证
- 需要浏览器登录态 → Cookie 认证
- 需要控制访问权限 → 授权策略