Client Credentials Flow(客户端凭证流程)

Client Credentials Grant(RFC 6749 §4.4)用于机器对机器(M2M)认证。没有用户 — 服务以自己的身份(client_id 与 client_secret)进行认证。

何时使用此流程

  • 微服务、守护进程、CI/CD 流水线 调用受保护的 API
  • 没有用户 — 纯服务身份
  • 服务可以 安全保管 client_secret(服务器端 secrets manager、环境变量 — 绝对不要 放在浏览器、移动 App,或发放给终端用户的 CLI)

接入之前

请管理员创建一个 confidential 客户端,具备:

  • 启用 Client Credentials Flow
  • 为此服务注册所需 scope(依部署而定的自定义 API scope)
  • 不需要 redirect URI

您会拿到:

  • client_id — 可以写进 log
  • client_secret — 存进 secrets manager;一旦泄露立即轮换

受限的 scope:openid 与 offline_access 在此流程不合法,会以 invalid_scope 被拒。scope 是对应到一个合成的服务身份,不是用户。

运作方式

sequenceDiagram participant Service participant Signet participant API Service->>Signet: POST /oauth/token(grant_type=client_credentials、Basic 认证) Signet-->>Service: access_token + expires_in note over Service: 缓存 token,接近过期时再刷 Service->>API: GET /resource(Authorization Bearer) API->>API: 以 JWKS 本地验证 JWT API-->>Service: 200 OK note over Service: Token 过期,重新请求(无 refresh token)

步骤 1:请求 access token

以 HTTP Basic 认证(推荐)或 form body 两种方式。端点只接受 application/x-www-form-urlencoded 正文。

HTTP Basic(依 RFC 6749 §2.3.1 推荐使用):

curl -X POST https://your-signet/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials"

表单内容:

curl -X POST https://your-signet/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET"

省略 scope 会拿到此客户端已注册的全部 scope。只有在您想要 子集 时才带 scope=...。

带 Resource Indicator(RFC 8707):

curl -X POST https://your-signet/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "resource=https://api.example.com"

resource 为选填、可重复(最多 10 个),必须是绝对 http(s) URI、无 fragment、≤ 1024 字符。带入后,签发 JWT 的 aud 会绑定到这些 resource — resource server 端对自己的标识符验证 aud(见 JWT 验证 §Audience Binding)。每个值都必须在您客户端的 allowed-resources 白名单内,该白名单 默认全部拒绝:白名单为空的客户端,送任何 resource= 都会得到 400 invalid_target — 请管理员把此服务要访问的 resource 标识符加进白名单。不带 resource 时 aud 回退到部署级别的 JWT_AUDIENCE。格式不正确 或 不在白名单内的值会返回 400 invalid_target — 见 错误处理。

多 RS 部署:若同一服务需要调用多个身份不同的 resource server,请对每个 resource 各自请求一张 token(各自缓存)。一张 token 共用于多个 RS 会让 audience binding 失效 — 任何能接受此 token 的 RS 都会变成中继点。

上面用 $CLIENT_SECRET 环境变量展示没问题;但在生产环境 不要把字面 secret 直接丢到命令行,argv 会出现在 ps 与 shell 历史里。改用 curl --netrc、配置文件(-K)或语言端的 SDK。

响应:

{
  "access_token": "eyJhbG...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "<此客户端被授予的 scope>"
}

此 grant 不会签发 refresh token(RFC 6749 §4.4.3)。access token 过期时重新请求。/oauth/revoke 与 /oauth/tokeninfo 仍可用 — 见 Token 与撤销。

若索取的 scope 超过此客户端允许范围,Signet 会返回:

{
  "error": "invalid_scope",
  "error_description": "Requested scope exceeds client permissions or contains restricted scopes (openid, offline_access are not permitted)"
}

完整错误清单见 错误处理。

步骤 2:使用 token

curl -H "Authorization: Bearer ACCESS_TOKEN" https://api.example.com/resource

Resource server 应 以 Signet 的 JWKS 在本地验证 JWT — 见 JWT 验证。

辨识 M2M token:Client Credentials 发出的 token 其 JWT sub(与 user_id)字段为 client:<client_id>。resource server 可以依此区分服务调用与用户委派调用。aud claim 是您请求的 resource indicator(或 JWT_AUDIENCE 回退值)— 与用户 token 一样,要对 RS 自己的标识符验证。

步骤 3:缓存与续约

不要每次调用都换一个新 token。缓存在内存中,接近过期再换(减去安全缓冲):

// Go 伪代码
if time.Now().Add(30 * time.Second).After(expiresAt) {
    accessToken, expiresAt = requestNewToken()
}
# Python
if time.time() + 30 >= expires_at:
    access_token, expires_at = request_new_token()

避免多副本同时续约的雪崩:在 30 秒缓冲上加入少量随机抖动,或使用共享缓存(Redis)搭配 single-flight 续约。

安全检查清单

要求 说明
安全保管 secret Secrets manager 或 runtime 注入环境变量 — 绝对不要 commit 到版本控制
全程 HTTPS 每次 token 请求都会把 client_secret 送上链路
一服务一客户端 单独撤销与细粒度的服务 scope 控制
只要最小 scope 最小权限原则
泄露即轮换 请管理员重新生成 secret;更新 secrets manager
退避重试 /oauth/token 有速率限制 — 见 Token 与撤销;处理 429 与 Retry-After
缓存 token,不要重抓 尊重 expires_in;只在接近过期时续约
监控 audit log 请管理员对异常的 CLIENT_CREDENTIALS_TOKEN_ISSUED 事件设警报

相关文档