API 密钥(Personal API Keys)

Client App 的 Owner 或 Admin 必须在 App 设置中启用 Personal API Key,且 App 必须通过审核并处于启用状态。新建及升级前已有 App 默认关闭;CIMD App 不可使用。若没有可用 App,请联系 Owner,或前往“我的 App”启用后重试。关闭设置会立即停止新申请及已有 Key 的使用。重新启用只会恢复尚未到期且未撤销的 Key;暂停中的 Key 仍可查看及撤销。

个人 API 密钥 是由已登录用户自行创建的不透明凭证(前缀 sgk_),给那些跑不了 OAuth 流程的调用端用:shell script、CI job、cron 定时任务,或是没有浏览器、也没地方安放 client secret 的老旧系统。

发送方式跟 bearer token 一样,但它 不是 JWT — 没有任何 claim、不能刷新、也无法离线验证。Resource server 必须问 Signet 这把密钥还有效吗,而这正是撤销能实时生效的原因。

何时该用密钥,何时不该

场景 该用
CI job、cron 定时任务,或一次性 script 以您的身份 执行 个人 API 密钥
交互式 CLI 或无头设备,但有人能打开浏览器 设备流程
后端服务以 自己的身份 调用,完全没有用户 客户端凭证
任何有用户又有浏览器的场景 授权码 + PKCE

只有在没有任何流程适用时 才动用密钥。流程给您的是短命 token、可离线 JWKS 验证、磁盘上没有长期秘密;密钥把这三项全部放弃,换来的是一行 curl 就能跑。另外请注意密钥绑在 您的 账号上 — 您离职那天,所有靠它运行的东西一起停摆。属于团队而非个人的东西,请管理员开一个 Client Credentials 客户端。

您的部署可能整个关掉了这项功能(PERSONAL_API_KEYS_ENABLED=false)。若 /account/api-keys 回 404,原因就在这 — 请找管理员。

密钥属性

属性 值
格式 sgk_ + 52 个小写 base32 字符(共 56 字符)。里面没有编码任何信息 — 它只是一个随机查表用的把手
显示 只有一次,就在创建完成后那一页。之后只会再看到 sgk_ab12…wxyz 这样的片段
绑定 恰好一个 客户端应用,创建时选定
Scope 该客户端应用的 scope,在 Signet 填查表缓存时从应用读取 — 密钥上不存任何副本
到期 必填,创建时选定,并受部署上限约束(默认上限 90 天)。不存在永不过期的密钥。
数量 每位用户有上限(默认 10 把有效密钥)
撤销 立即生效 — 下一次验证就会失败
验证方式 只能在线验(/oauth/tokeninfo 或 /oauth/introspect)— 没有 JWKS、无法本地验证

Scope 跟着应用走,不跟着密钥走。 一般 tokeninfo/introspect scope 不逐把密钥保存副本:验证所报的 scope,是 Signet 上次为这把密钥填查表缓存时该客户端应用所拥有的 scope。管理员把应用的 scope 放宽,绑在上面的每把密钥跟着放宽;收窄同理 — 但不是立即生效,所以千万别把「改了应用的 scope」当成「旗下密钥立刻降权」。请挑「刚好够您的 script 用」的那个应用。

运作方式

sequenceDiagram participant User as 用户 participant Signet participant Script as 脚本 participant API as Resource Server User->>Signet: /account/api-keys → 创建(名称、客户端应用、到期) Signet-->>User: sgk_…(只显示这一次) note over User,Script: 存进 secrets manager / CI secret Script->>API: GET /resource(Authorization: Bearer sgk_…) API->>Signet: GET /oauth/tokeninfo(Bearer sgk_…) Signet-->>API: {active, user_id, scope, token_type: personal_api_key} API-->>Script: 200 OK note over User,Signet: 在 /account/api-keys 撤销 → 下次验证即 401

步骤 1:创建密钥

前往 /account/api-keys → + 创建密钥。三个字段:

字段 说明
名称 最多 100 字符。写清楚谁会用它,例如 CI deploy、nightly backup — 日后就是靠这个对号入座
客户端应用 输入关键字搜索启用中的应用。密钥属于这个应用,并继承它的 scope
到期时间 3 小时 / 1 天 / 7 天 / 30 天 / 自定义… 天数。全部受部署上限约束(默认 90 天),超过上限的预设选项不会出现

下一页 只会显示一次 完整密钥。请直接复制进 secrets manager 或 CI secret store — 该页带有 Cache-Control: no-store,按上一页救不回来,而 Signet 只保存哈希值。丢了?撤销后重建一把,没有「再看一次」这个选项。

离开那一页之前,先跑一次页面上的 现在就验证它 区块。它会给你一段可以直接粘贴、且带有真实密钥的命令,并打印出你这把密钥应该返回的完整内容——你的 user_id、你的 client_id、你的权限范围、你的 exp。把两边对起来,是你唯一能证明密钥完整复制下来的机会,而代价只是粘贴一次。

怎么挑到期时间:挑您有能力自动化处理的最短寿命。一次性迁移用的 3 小时密钥,出事的代价远低于挂在 CI runner 上的 90 天密钥。若撞到每人上限(列表页会显示「已使用 3 / 10 把密钥」),请撤掉没在用的那把,而不是去要更高的上限。

什么会占用上限:只有「有效」密钥会占名额,也就是尚未吊销、也还没到期的那些。已吊销和已过期的密钥仍会留在列表中,作为曾经存在过的记录,但它们会立刻释放名额——这也是为什么列表的行数和标题上的数字经常对不上。让一把密钥自然到期,跟主动吊销它一样能空出名额。

步骤 2:使用密钥

当成 bearer token 发出:

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

在 CI 里放进 secret store,以环境变量注入:

# GitHub Actions
- name: Deploy
  env:
    SIGNET_API_KEY: ${{ secrets.SIGNET_API_KEY }}
  run: ./deploy.sh

保管规则 — 密钥等同一组长期密码:

  • 绝对不要 放进 URL query string、redirect、或任何 GET 参数。URL 会进 access log、proxy 与浏览器历史。
  • 绝对不要 直接写在命令行参数上 — argv 可以被 ps 看见,也会进 shell 历史。请从环境变量或文件读取。
  • 不要 commit、不要贴到 issue,并把它从 CI log 里屏蔽掉(把变量设为 masked)。
  • 一个消费者一把密钥。三支 script 共用一把,撤销时三支一起死,而 最后使用 也无法告诉您是谁在用。

步骤 3:验证密钥(Resource Server 端)

没有任何离线验证的可能。下面两个端点都会回 token_type: "personal_api_key",让您能套用不同策略 — 例如部署 API 接受密钥,但 OIDC 敏感面一律拒绝。

GET /oauth/tokeninfo

最简单的做法:密钥本身就是这次调用的凭证,不需要客户端凭证。

curl -H "Authorization: Bearer sgk_..." https://your-signet/oauth/tokeninfo
{
  "active": true,
  "user_id": "5f6e...",
  "client_id": "d4c3...",
  "scope": "deploy",
  "exp": 1769472000,
  "iss": "https://your-signet",
  "subject_type": "user",
  "token_type": "personal_api_key"
}

user_id 是创建这把密钥的人 — 请把这次调用视为 以该用户身份 发出,并在放行操作前检查 scope。另外注意密钥没有 aud 字段:audience binding(RFC 8707)是 JWT 的功能,所以密钥无法被绑定到单一 resource server。如果您的 API 靠 aud 防止 token 被重放到隔壁服务,那密钥就是错的凭证选择。

任何失败 — 不存在、格式错误、已撤销、已过期、绑定的应用被停用、或功能被关闭 — 都回 同一个 结果:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", resource_metadata="…/.well-known/oauth-protected-resource"

{"error": "invalid_token", "error_description": "Token is invalid or expired"}

这种一致性是刻意的:扫描者不该有办法得知密钥「为什么」失败。代价是 您自己也看不出来 — 请看下方〈为什么我的密钥突然不能用了〉。

POST /oauth/introspect(RFC 7662)

当您的 resource server 本身就是注册过的 Signet 客户端,而且想要符合 RFC 形状的输出时使用:

curl -X POST https://your-signet/oauth/introspect \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=sgk_..."
{
  "active": true,
  "scope": "deploy",
  "client_id": "d4c3...",
  "token_type": "personal_api_key",
  "exp": 1769472000,
  "iat": 1769385600,
  "sub": "5f6e...",
  "username": "alice",
  "iss": "https://your-signet",
  "jti": "key-uuid"
}

无效的密钥依 RFC 7662 就只是 {"active": false}(绝不会是 4xx)。

这里同样有 ownership 闸门。 默认(INTROSPECTION_REQUIRE_OWNERSHIP=true)只有当您以 密钥所绑定的那个客户端应用 身份认证时,才拿得到上面的完整 metadata。其他客户端只会看到被清空的 {"active": true}。所以一个共用的 API gateway 去 introspect 绑在许多不同应用上的密钥,会什么有用信息都拿不到 — 那种拓扑请改用 /oauth/tokeninfo。

缓存验证结果

能避免的话,不要每个进来的请求都打一次 Signet。密钥是*唯一*真的需要往返的凭证 — JWT access token 必须用 JWKS 本地验证,绝不能每个请求都调这两个端点。两个端点都有速率限制:/oauth/tokeninfo 每 IP(默认每分钟 600 次,您整个 egress IP 共用),/oauth/introspect 每个已认证的客户端应用(各每分钟 600 次,后面再加每 IP 1200 次的上限);见 Token 与撤销。Signet 自己的缓存保护的是 Signet 的数据库,不是您的请求额度 — 收到 429 就代表您这边少了一层验证结果缓存。

// Go 伪代码 — 缓存「结果」,不是缓存密钥
if v, ok := cache.Get(sha256(key)); ok {
    return v
}
v := callTokenInfo(key)          // 401 也要缓存成负面结果
cache.Set(sha256(key), v, 60*time.Second)
  • 缓存键请用密钥的哈希,绝不用密钥本身,也绝不写进 log。
  • TTL 要短 — 您的缓存 TTL 就是您的撤销延迟。30~60 秒是合理的折衷;缓存到 exp 等于把在线验证这件事的意义整个丢掉。
  • 负面结果也缓存一小段时间,这样配置坏掉的客户端重试循环才不会同时打爆您和 Signet。

哪些端点吃密钥

端点 sgk_ 密钥 行为
GET /oauth/tokeninfo 接受 验证用。token_type: personal_api_key
POST /oauth/introspect 接受 验证用,RFC 7662 形状。需客户端认证,且有 ownership 闸门
POST /oauth/revoke 接受 自助撤销 — 持有密钥即是授权。一律回 200
POST /oauth/token(四种 grant 皆然) 拒绝 密钥不是 code、不是 refresh token,也不是 device code → invalid_grant(device_code 是 access_denied)
GET /api/v1/me 有条件接受 管理 API 已启用;此密钥明确取得 account:read,且当前 client/resource 授权仍有效
GET /oauth/userinfo 拒绝 OIDC 面,只吃 JWT access token → 401 invalid_token
本地 JWKS 验证 拒绝 不是 JWT,没东西可验。请改打上面两个端点

撤销与轮换

从界面:/account/api-keys → 吊销。立即生效,且无法恢复;若部署是多节点又没有共享缓存,请预留最多约 1 分钟让每个节点跟上。

从脚本 — /oauth/revoke 不需要客户端凭证就接受密钥,因为持有密钥本身 就是 授权。很适合在任务结束时顺手拆掉一把短命密钥:

curl -X POST https://your-signet/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "token=$SIGNET_API_KEY"

依 RFC 7009,无论密钥是否存在都回 200,所以这个响应什么都证明不了。有一种情况它并 不会 真的撤销:若该部署把个人 API 密钥整个关掉了(PERSONAL_API_KEYS_ENABLED=false),这个调用照样回 200,但密钥仍在数据库里,等运维把功能开回来就又能用。真的要紧时(例如密钥泄露)请到 /account/api-keys 确认;若那页回 404,请找管理员代为撤销。

轮换,请照这个顺序(千万别反过来,先撤销等于制造停机):

  1. 在 /account/api-keys 创建替代密钥。
  2. 更新每个消费者的 secret,并确认可以正常运行。
  3. 看一下旧密钥的 最后使用 字段;如果还在往前走,表示您漏掉了一个消费者。这一步要在撤销 之前 做 — 已撤销的密钥不会再更新 最后使用,事后那个字段是冻住的,什么都看不出来。
  4. 撤销旧密钥。

密钥一旦泄露,先撤销再追问。 然后创建新的一把 — 轮换客户端应用的 secret 对密钥毫无作用,别以为那样就「洗干净」了。

为什么我的密钥突然不能用了

下面每一种都会产生同一个 401 invalid_token,所以请打开 /account/api-keys 逐项排除:

原因 从哪看得出来 怎么救
已超过到期时间 状态是 已过期 创建新密钥
您或管理员撤销了它 状态是 已吊销 创建新密钥 — 撤销是永久的
您的账号被停用 您连登录都登不进来 您所有密钥都已被撤销。账号重新启用 不会 让它们复活
绑定的客户端应用被停用 密钥仍列在列表上,既未过期也未吊销 不用做任何事 — 管理员把应用重新启用后密钥会 自动恢复(传播最多约 1 分钟)
绑定的客户端应用被删除 密钥变成 已吊销 永久失效。请改绑其他应用创建新密钥
整个部署关掉了这项功能 /account/api-keys 回 404 找管理员
密钥被截断或贴错 列表上看起来一切正常却还是 401 重新复制一次 — 密钥恰好 56 字符,而界面上显示的片段(sgk_ab12…wxyz)不能 当凭证使用
Scope 不再足够 Resource server 回 403,不是 401 该客户端应用的 scope 变了。请找应用的所有者

还有谁看得到您的密钥:您绑定的那个客户端应用的所有者,以及管理员,能看到这把密钥存在 — 名称、片段、到期时间、最后使用时间,旁边还有您的用户名与 email。两者都看不到密钥本身。应用所有者不能撤销您的密钥;管理员可以强制撤销。

安全检查清单

要求 细节
优先用流程 只有在没有 OAuth 流程适用时才用密钥 — 见本页最上面那张表
到期时间取最短可行值 以天为单位,不是以月。部署上限是天花板,不是目标
一个消费者一把密钥 可独立撤销,且 最后使用 才有判读价值
放 secrets manager / CI secret 绝不进版本控制、argv、URL 或 log
绑最小权限的客户端应用 密钥继承应用的 scope — 绑在刚好够用的那个应用上
在线验证,短缓存 验证结果缓存 30~60 秒;TTL 就是撤销延迟
处理 429 tokeninfo 每 IP 限制,introspect 每客户端应用限制 — 若有 Retry-After 就遵守,退避加抖动
泄露先撤销,再轮换 立刻撤销,然后照「创建 → 部署 → 撤旧」
收尾 退役的 script 请撤掉密钥,不要放到过期为止

相关文档

管理 API 权限

启用管理 API 后,/api/v1/me 接受明确授予 account:read 的个人密钥。创建页提供选填管理权限,逐把密钥独立保存;现有密钥没有管理权限。有效权限受当前应用程序授权限制,管理员操作另检查当前管理员角色。管理 API 不使用一般验证缓存,撤销、停用或收窄会在下一次请求生效。管理 grant 绑定服务器设置的精确 resource,但不新增 JWT claims,也不改变 tokeninfo/introspect 的 scope。仅选择获准的应用程序不会自动取得管理权限。