Skip to content

JWT Bearer 认证 ​

本页说明如何用 auth/jwt 让宿主 API 接受 Authorization: Bearer <JWT> 形式的访问令牌,令牌可以来自 wicket 自己,也可以来自其他签发方。

它不是登录流程:没有跳转、没有 cookie,只在每个请求上验证一次令牌并给出身份。

可运行示例 ​

下面的装配在 v0.1.4 上编译并实测:合法令牌返回 200;从自定义头取令牌返回 200;缺少业务 claim、或不带令牌均返回 401。

go
import (
	"fmt"
	"log/slog"
	"net/http"

	"github.com/gonewx/godi"
	wicket "github.com/gonewx/wicket"
	"github.com/gonewx/wicket/auth"
	"github.com/gonewx/wicket/auth/jwt"
	"github.com/gonewx/wicket/claims"
	"github.com/gonewx/wicket/di"
	optspkg "github.com/gonewx/wicket/di/options"
	"github.com/gonewx/wicket/hosting"
	"github.com/gonewx/wicket/services"
	"github.com/lestrrat-go/jwx/v3/jwk"
)

type fixedMonitor[T optspkg.Entity[T]] struct{ opts T }

func (m fixedMonitor[T]) CurrentValue() (T, error) { return m.opts, nil }
func (m fixedMonitor[T]) Get(string) (T, error)    { return m.opts, nil }

// keys 是签发方的公钥集合(jwk.Set),例如用 jwk.Fetch 从对方的 JWKS 地址取得。
func wireBearer(root *di.Root, logger *slog.Logger, keys jwk.Set) error {
	cfg := root.ServerConfig()
	cfg.SetAuthenticationHandlerProvider(wicket.JwtBearerAuthenticationScheme, func(p godi.Provider) auth.Handler {
		o := jwt.NewOptions()
		o.TokenValidationParameters.ValidIssuer = "https://issuer.example.com"
		o.TokenValidationParameters.ValidAudience = "orders-api"
		o.TokenValidationParameters.Keys = keys
		o.SaveToken = true

		ev := jwt.NewEvents() // 从 NewEvents 出发,未覆盖的钩子保持空操作
		ev.OnMessageReceived = func(ctx *jwt.MessageReceivedContext) error {
			ctx.Token = ctx.Request.Header.Get("X-Api-Token") // 留空则回落到 Authorization 头
			return nil
		}
		ev.OnTokenValidated = func(ctx *jwt.TokenValidatedContext) error {
			if ctx.Principal.FindFirst("tenant") == nil {
				ctx.Fail(fmt.Errorf("missing tenant claim"))
			}
			return nil
		}
		o.SetEvents(ev)
		return jwt.NewHandler(fixedMonitor[*jwt.Options]{o}, logger,
			godi.GetRequired[services.SystemClock](p))
	})
	return root.Ptr_SchemeProvider().AddScheme(auth.NewScheme(wicket.JwtBearerAuthenticationScheme, "jwt"))
}

// 在宿主路由里按需认证
func ordersHandler(w http.ResponseWriter, r *http.Request) {
	svc := godi.GetRequired[auth.Service](hosting.ScopeFrom(r.Context()))
	result, err := svc.Authenticate(r, w, wicket.JwtBearerAuthenticationScheme)
	if err != nil || !result.Succeeded() {
		w.Header().Set("WWW-Authenticate", "Bearer")
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}
	sub := result.Principal.FindFirst(claims.NameIdentifier)
	fmt.Fprintf(w, "hello %s\n", sub.Value)
}

要点:

  • wireBearer 在 cfg.Freeze() 之前调用;宿主 handler 要挂在 hosting.ScopeMiddleware(root)(srv.UseWicket()(app)) 之内,才能从请求上下文取到作用域。
  • 方案名是 Bearer(wicket.JwtBearerAuthenticationScheme)。容器的默认方案固定为 wicket,所以 Bearer 认证需要像上面这样显式调用 Authenticate,它不会自动作用于每个请求。
  • 容器自带的 Bearer handler 没有任何验证密钥,必须通过 SetAuthenticationHandlerProvider 换成你自己构造的 handler。

令牌从哪里取 ​

  1. 先触发 OnMessageReceived。钩子可以给 ctx.Token 赋值(从其他头、cookie、查询参数取令牌),也可以直接设置 ctx.Result 结束处理。
  2. 钩子没有给出令牌时,读取 Authorization 头,前缀 bearer 不区分大小写。
  3. 两处都没有令牌:返回“无结果”(匿名),不是失败。

验证参数(TokenValidationParameters) ​

真正决定验证行为的是 Options.TokenValidationParameters。jwt.NewOptions() 已给它一份安全默认值:

字段默认说明
RequireSignedTokenstrue要求验签;为 true 时必须提供 Keys
Keysnil验签公钥集合(jwk.Set),按 kid 选钥;每把钥都必须带 alg,见下方提示
ValidIssuer空非空时要求 iss 完全相等
ValidAudience空非空时要求 aud 包含该值
ValidateLifetimetrue校验 exp / nbf / iat;关掉后仍校验签发方与受众
ClockSkew5 分钟(jwt.DefaultClockSkew)时间比较的容差;0 表示精确比较
Clocknil为空时 handler 注入容器的 SystemClock
ValidateOptions空追加的 jwx 校验选项(如自定义 claim 校验)

Options 字段:哪些在 v0.1.4 中生效 ​

jwt.Options 声明的字段较多,但 v0.1.4 的 handler 只读取其中一部分。未生效的字段设置了也不会有任何效果,请不要依赖:

字段是否生效说明
TokenValidationParameters生效见上表
SecurityTokenValidators生效验证器链,默认只有 jwt.NewSecurityTokenHandler();依次尝试,任一成功即可
SaveToken生效为 true 时把原始令牌以 access_token 名存入认证属性
事件(SetEvents)部分生效见下节
Authority、MetadataAddress、RequireHttpsMetadata未生效handler 不做元数据发现,不会自动拉取 JWKS
RefreshOnIssuerKeyNotFound、AutomaticRefreshInterval、RefreshInterval未生效同上,不存在自动刷新
Audience未生效受众请设在 TokenValidationParameters.ValidAudience
ValidateOptions(Options 级)未生效请用 TokenValidationParameters.ValidateOptions
Challenge、IncludeErrorDetails未生效质询时只写 401 状态码,不写 WWW-Authenticate 头
BackchannelHttpHandler未生效没有出站请求

另外,jwt.NewOptions() 不会给这些字段赋非零默认值——布尔值为 false、时长为 0。

Keys 中的每把公钥都必须带 alg

验证器按 kid 选钥后,使用钥上声明的算法验签,不会根据钥的类型推断算法。v0.1.4 实测:同一组 RSA 公钥去掉 alg 字段后,所有合法令牌都验证失败(401)。很多提供者的 JWKS 带有 alg,但这不是强制字段;拿到 jwk.Set 后请逐把检查,缺失时自行补上,例如:

go
for i := 0; i < keys.Len(); i++ {
	if k, ok := keys.Key(i); ok {
		if _, has := k.Algorithm(); !has {
			_ = k.Set(jwk.AlgorithmKey, jwa.RS256()) // 按签发方实际使用的算法设置
		}
	}
}

需要跟随签发方轮换密钥?

由于 handler 不做元数据发现,密钥来源由你掌控:可以用 jwk.Fetch 启动时取一次,或用 jwx 的 jwk.Cache 定期刷新后在工厂函数里取最新的 jwk.Set(工厂在每次解析 handler 时调用)。

验证成功后的身份 ​

验证通过后,令牌 claims 被投影为一个认证类型为 Bearer 的身份:

  • sub 映射为 claims.NameIdentifier(URI 形式的常量);
  • iss、jti、aud(多值时逐个)以原名保留;
  • exp、nbf、iat 不进入身份,而是写入认证属性的 Expires / Issued;
  • 其余 claim 原名原值保留。

事件钩子 ​

jwt.Events 有五个字段,v0.1.4 的 handler 实际调用其中两个:

钩子触发用法
OnMessageReceived取令牌之前设置 ctx.Token,或设置 ctx.Result 直接结束
OnTokenValidated验签与 claim 校验通过之后追加业务校验,调用 ctx.Fail(err) 拒绝;ctx.Principal 可读可改
OnAuthenticationFailed、OnForbidden、OnChallenge已声明,当前未被调用不要依赖

几条容易踩的细节:

  • 自己构造 &jwt.Events{...} 时未填的字段是 nil,被调用时会 panic。请从 jwt.NewEvents() 出发再覆盖需要的字段。
  • OnTokenValidated 返回非 nil 错误时,handler 会改用验证器链中的下一个验证器;默认链只有一个验证器,于是整体以“没有可用的验证器”失败。要明确拒绝,用 ctx.Fail(err) 并返回 nil。
  • 所有验证器都失败时结果为失败(Succeeded() 为 false),具体原因只写 debug 日志。

相关阅读 ​

以 Apache License 2.0 发布 · 文档内容 © 2026 Wicket 作者