Skip to content

客户端凭据(client_credentials) ​

本页说明没有用户参与时,一个后端服务如何用自己的凭据向 wicket 换取访问令牌,以及被调用的 API 如何校验这枚令牌。本页的请求与响应来自 快速开始 示例程序(examples/quickstart)在 v0.1.4 上的实际运行结果;关于密钥 Expiration 的结论来自在同一装配上加了测试客户端的实验程序。

适用场景 ​

  • 定时任务、消息消费者、内部微服务之间互相调用;
  • 令牌代表的是客户端本身,令牌里没有 sub,也不会签发 id_token 或 refresh_token。

需要代表某个用户访问资源时,请改用 授权码 + PKCE 或 CIBA。

1. 定义客户端与资源 ​

以下片段摘自快速开始示例,完整装配(签名密钥、hosting.ScopeMiddleware 等)见 快速开始:

go
// API 作用域与 API 资源
api := models.NewApiScope()
api.Name = "orders.read"

apiSecret := models.NewSecret()
apiSecret.Value = utils.Sha256("api-secret") // 资源自身的密钥,内省时使用
orders := models.NewApiResource()
orders.Name = "orders"
orders.Scopes = []string{"orders.read"}
orders.ApiSecrets = []*models.Secret{apiSecret}

// 机器客户端
secret := models.NewSecret()
secret.Value = utils.Sha256("change-me")

client := models.NewClient()
client.ClientId = "billing-worker"
client.ClientSecrets = []*models.Secret{secret}
client.AllowedScopes = []string{"orders.read"}
if err := client.SetAllowedGrantTypes(wicket.GrantTypesClientCredentials); err != nil {
	log.Fatal(err)
}

cfg := root.ServerConfig()
cfg.SetApiScopes([]*models.ApiScope{api})
cfg.SetApiResources([]*models.ApiResource{orders})
cfg.SetClients([]*models.Client{client})

要点:

  • 密钥存哈希。Secret.Value 存的是 utils.Sha256(明文)(SHA-256 后 Base64),客户端请求时发送明文。不要把明文写进配置或数据库。
  • 一个客户端可以同时挂多个密钥,任一匹配即通过。轮换密钥时先追加新密钥,客户端切换完成后再移除旧密钥。
  • 授权类型通过 SetAllowedGrantTypes 设置(字段本身不可直接赋值),它会拒绝重复项和含空格的值。wicket.GrantTypesClientCredentials 等是预定义的组合。
  • NewClient() 的默认值:RequireClientSecret = true、AccessTokenLifetime = 3600 秒、AccessTokenType 为 JWT。

2. 请求令牌 ​

令牌端点是 POST /connect/token,表单编码:

bash
curl -s -u billing-worker:change-me \
  -d grant_type=client_credentials \
  -d scope=orders.read \
  http://localhost:5000/connect/token

实际响应(令牌已截断):

json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InVBdm9RSHVz...",
  "expires_in": 3600,
  "id_token": "",
  "refresh_token": "",
  "scope": "orders.read",
  "token_type": "Bearer"
}

响应里的空字段

v0.1.4 的令牌响应总会带上 id_token 与 refresh_token 两个键,未签发时值为空字符串。客户端应按“值为空即不存在”处理,不要只判断键是否存在。

省略 scope 参数时,wicket 会按客户端 AllowedScopes 中全部可用的作用域签发(本例结果同样是 orders.read)。

3. 令牌里有什么 ​

访问令牌是 JWT,解码后的头部与载荷(实际运行结果):

json
{ "alg": "RS256", "kid": "uAvoQHusrZOFYuv6MBTpWlh6Av1iJlt38tmDkQ6-d0s", "typ": "at+jwt" }
json
{
  "aud": ["orders"],
  "client_id": "billing-worker",
  "exp": 1794542360,
  "iat": 1794538760,
  "iss": "http://localhost:5000",
  "nbf": 1794538760,
  "scope": ["orders.read"]
}
  • typ 为 at+jwt,资源服务可据此区分访问令牌与 id_token。
  • aud 来自作用域所属的 API 资源名(orders),不是作用域名。
  • scope 在 JWT 里是数组;在内省响应里是空格分隔的字符串(见下文)。
  • kid 与 /.well-known/openid-configuration/jwks 中发布的公钥一致,资源服务可以离线验签,详见 本地验签与服务端交换。

4. 客户端认证方式 ​

发现文档中 token_endpoint_auth_methods_supported 为 client_secret_basic 与 client_secret_post,两种都由内置的密钥解析器处理。

方式写法说明
client_secret_basicAuthorization: Basic base64(client_id:client_secret),即 curl -u推荐;凭据不进入表单与访问日志
client_secret_post表单中带 client_id 与 client_secret适合无法设置请求头的环境

client_secret_post 的等价请求(实际返回 200 与访问令牌):

bash
curl -s \
  -d client_id=billing-worker \
  -d client_secret=change-me \
  -d grant_type=client_credentials \
  http://localhost:5000/connect/token

只带 client_id、不带密钥的请求会得到 invalid_client,因为该客户端 RequireClientSecret 为 true。

v0.1.4:不要给密钥设置 Expiration

v0.1.4 对密钥 Expiration 的处理有误:设置了 Expiration 的密钥可能被错误地拒绝(返回 invalid_client)。在修复版本发布前,请保持 Expiration 为零值,用“增删密钥”的方式做轮换;停用的密钥要直接从配置中删除,不要依赖 Expiration 让它失效。

5. API 端用内省验证令牌 ​

不想在 API 侧做 JWT 验签时,可以调用内省端点 POST /connect/introspect。调用方是 API 资源,不是客户端:用 ApiResource.Name 作为用户名、ApiSecrets 中的明文作为密码做 Basic 认证。没有配置 ApiSecrets 的资源无法调用内省端点。

bash
curl -s -u orders:api-secret \
  -d token="$ACCESS_TOKEN" \
  http://localhost:5000/connect/introspect

实际响应:

json
{
  "active": true,
  "aud": ["orders"],
  "client_id": "billing-worker",
  "exp": 1794542360,
  "iat": 1794538760,
  "iss": "http://localhost:5000",
  "nbf": 1794538760,
  "scope": "orders.read"
}

其他情况(均为实际结果):

请求结果
token 是无法识别的字符串200,{"active":false}
API 资源密钥错误(-u orders:wrong)401,空响应体
用客户端凭据调用(-u billing-worker:change-me)401,空响应体

内省不是授权

active: true 只说明令牌有效。API 仍需自行检查 scope 是否包含当前接口要求的权限。

6. 常见错误 ​

以下均为实际运行结果。注意 v0.1.4 的错误响应状态码统一为 400(包括 invalid_client),Content-Type 为 text/plain; charset=utf-8,响应体是 JSON 并带有一个 custom 字段:

json
{"custom":{},"error":"invalid_client","error_description":""}
error触发条件排查方向
invalid_client客户端不存在、密钥错误、需要密钥但未提供确认 Secret.Value 存的是 utils.Sha256(明文),请求发的是明文;确认没有给密钥设置 Expiration(见上文警告)
invalid_scope请求了客户端 AllowedScopes 之外或服务端未定义的作用域(实测 orders.write、openid 均返回此错误)检查 AllowedScopes 与已注册的 ApiScope;客户端凭据流没有用户,不要请求 openid 等身份作用域
unauthorized_client客户端没有被允许使用该 grant_type检查 SetAllowedGrantTypes
invalid_request用 GET 访问令牌端点、缺少必需参数、表单中含控制字符令牌端点只接受 POST 表单

客户端代码的兼容写法

标准建议 invalid_client 使用 401。如果你的客户端库只在 401 时才判定凭据错误,请同时按响应体中的 error 字段判断。

相关阅读 ​

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