Skip to content

扩展授权类型 ​

本页回答一个问题:能否让 wicket 的令牌端点接受自定义的 grant_type(例如 urn:example:grant-type:sms)?在 v0.1.4 中答案是不能。下文给出依据,以及当前可用的替代方案。

现状 ​

项目v0.1.4 的情况
扩展授权接口 validators.ExtensionGrantValidator存在
令牌端点对未知 grant_type 的分派逻辑存在,会去查找已注册的扩展校验器
宿主注册自定义校验器的入口不存在
发送自定义 grant_type 的实际结果unsupported_grant_type
发现文档 grant_types_supported不会出现任何扩展类型

为什么注册不进去 ​

  1. ServerConfig 没有对应的设置方法。 宿主向 wicket 注入行为的入口都集中在 root.ServerConfig() 上,其中与授权校验相关的只有 SetResourceOwnerValidator(password 授权)和 CIBA 的用户校验器,没有扩展授权校验器。
  2. 容器在编译期就已确定。 di.NewRoot() 返回的容器是预先生成的代码,能解析哪些服务在生成时就固定了,NewRoot() 之后没有追加注册服务的 API。生成的代码里,validators.ExtensionGrantValidator 这个接口没有任何注册项,所以令牌端点拿到的扩展校验器列表永远是空的。

实测现象 ​

客户端声明了自定义授权类型,SetAllowedGrantTypes 不会报错:

go
xc := models.NewClient()
xc.ClientId = "ext-app"
// ... 密钥、作用域
_ = xc.SetAllowedGrantTypes([]string{"urn:example:grant-type:sms"}) // 返回 nil

但请求令牌端点时:

bash
curl -s -u ext-app:x-secret \
  -d grant_type=urn:example:grant-type:sms -d scope=orders.read \
  http://localhost:5000/connect/token
json
{"custom":{},"error":"unsupported_grant_type","error_description":""}

wicket 的日志里会出现 No validator is registered for the grant type,字段 grantType 为请求中的值。

发现文档方面,opts.Discovery.ShowExtensionGrantTypes 默认为 true,会把已注册扩展类型追加到 grant_types_supported;由于列表为空,实际不会多出任何一项。

接口长什么样 ​

供了解代码结构参考(在 v0.1.4 中实现它也无处注册):

go
// validators 包
type ExtensionGrantValidator interface {
	Validate(ctx *validators_contexts.ExtensionGrantValidationContext) error
	GrantType() string
}

令牌端点处理扩展授权时的检查顺序是:客户端的 AllowedGrantTypes 是否包含该类型 → 是否有校验器的 GrantType() 与之相同 → 作用域校验 → 调用校验器 → 若结果带用户主体,再检查用户是否处于启用状态。

替代方案 ​

方案一:password 授权(凭据能放进用户名 + 密码时) ​

password 授权(资源所有者密码凭据)默认是关闭的:容器里的默认实现直接拒绝所有请求,发现文档也不会列出 password。宿主通过 cfg.SetResourceOwnerValidator 注入自己的校验器即可开启。

go
import (
	"time"

	"github.com/gonewx/wicket/validators"
	validators_contexts "github.com/gonewx/wicket/validators/contexts"
	validators_models "github.com/gonewx/wicket/validators/models"
)

type pwdValidator struct{}

func (pwdValidator) Validate(ctx *validators_contexts.ResourceOwnerPasswordValidationContext) error {
	// 示例:实际应查询用户库,并对密码哈希做恒定时间比较
	if ctx.UserName == "alice" && ctx.Password == "s3cret" {
		ctx.Result = validators_models.NewGrantValidationResult(
			validators_models.WithGrantValidationResultSubject("user-1001", "pwd", time.Now(), nil, "local", nil),
		)
	}
	return nil
}

var _ validators.ResourceOwnerPasswordValidator = pwdValidator{}

装配(在 cfg.Freeze() 之前):

go
pc := models.NewClient()
pc.ClientId = "legacy-app"
pc.ClientSecrets = []*models.Secret{s2} // s2.Value = utils.Sha256("pwd-secret")
pc.AllowedScopes = []string{"orders.read"}
if err := pc.SetAllowedGrantTypes(wicket.GrantTypesPassword); err != nil {
	log.Fatal(err)
}

cfg.SetClients([]*models.Client{client, pc})
cfg.SetResourceOwnerValidator(pwdValidator{})

校验器的约定:

  • ctx.Result 初始就是 invalid_grant 错误。校验器不改动它即表示认证失败,令牌端点返回 invalid_grant,error_description 为 invalid_username_or_password。
  • 认证成功时用 WithGrantValidationResultSubject 设置用户主体:参数依次为 sub、认证方式(进入 amr)、认证时间、附加声明、身份提供方(进入 idp)、附加到令牌响应的自定义字段。
  • 返回非 nil 的 error 同样按 invalid_grant 处理。
  • ctx.Request 是已校验的令牌请求,可以从中读取客户端信息和原始参数。

实测(上面的装配):

bash
curl -s -u legacy-app:pwd-secret -d grant_type=password \
  -d username=alice -d password=s3cret -d scope=orders.read \
  http://localhost:5000/connect/token

响应包含 access_token、expires_in: 3600、token_type: Bearer、scope: orders.read。访问令牌的载荷中有 sub: user-1001、amr: ["pwd"]、idp: local、client_id: legacy-app,经内省端点验证为 active: true。注入校验器后,发现文档的 grant_types_supported 中会出现 password;不注入时,同样的请求返回 unsupported_grant_type。

如果你的自定义凭据恰好是“标识 + 一次性凭证”两段(例如手机号 + 短信验证码),可以借用这两个字段,在校验器里按自己的规则核验。但要清楚这样做的代价:

password 授权的适用范围

password 授权要求客户端直接接触用户凭据,OAuth 2.0 的安全最佳实践已不推荐使用它。只应开放给你自己掌控的第一方客户端,并且该客户端应能保管客户端密钥。面向第三方或浏览器的场景,请用方案二。

方案二:在登录页完成自定义认证,走授权码流程 ​

需要用户参与的自定义认证方式(短信验证码、扫码登录、第三方账号等),推荐放到宿主的登录页里完成:wicket 把用户重定向到宿主登录页,宿主用任意方式验证用户后建立登录会话,再回到授权端点继续标准的授权码流程。令牌由 wicket 按标准流程签发,客户端认证、作用域校验、刷新令牌都照常生效。

wicket 自带的 phoneverify、wechatverify、oidcverify 等包就是为这类“在登录页验证身份”的场景准备的,见下方相关阅读。

方案三:宿主自行签发 JWT(仅限特殊场景) ​

容器中有一个 *tools.TokenTools,可以用 wicket 的签名密钥签发任意声明的 JWT。在宿主自己的 handler 里,从请求作用域中解析它:

go
app.HandleFunc("/internal/issue", func(w http.ResponseWriter, r *http.Request) {
	// 宿主必须先自行完成调用方认证与授权判断
	scope := hosting.ScopeFrom(r.Context())
	tt := godi.GetRequired[*tools.TokenTools](scope)
	jwt, err := tt.IssueJwt(r.Context(), 300, []*claims.Claim{
		claims.NewClaim(claims.WithClaimType("sub"), claims.WithClaimValue("user-1001")),
		claims.NewClaim(claims.WithClaimType("aud"), claims.WithClaimValue("orders")),
		claims.NewClaim(claims.WithClaimType("scope"), claims.WithClaimValue("orders.read")),
	})
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Write([]byte(jwt))
})

实测得到的令牌带有 iss、iat、nbf、exp 以及你传入的声明,可以用发现文档公布的 JWKS 验签。但它不是 wicket 意义上的访问令牌:

  • JWT 头部的 typ 是 JWT,而不是 wicket 访问令牌使用的 at+jwt。实测把它交给 wicket 的内省端点,返回 {"active":false},日志显示 typ not match。
  • 不经过令牌端点:没有客户端认证、没有作用域与资源校验、不会签发刷新令牌、不会记录授权。这些都要宿主自己负责。

只有当接收方是你自己的 API、它直接用 JWKS 验签且不要求 at+jwt 时,这个方案才可用。多数情况下应优先考虑方案一或方案二。

相关阅读 ​

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