题目与适用场景
一个多租户 B2B 平台对外提供服务端 API。每个客户可以为不同集成创建多个 Key。系统需要隔离正式与测试环境,支持权限范围、单 Key 与租户级限额、可选过期时间、无停机轮换,以及凭证泄露后的快速吊销。
请设计签发、存储、验证、授权、轮换、吊销与审计链路,并说明哪些场景不适合使用 API Key。调用方是能够保护秘密的可信服务端;浏览器和移动应用不在这种凭证模型内。
设计必须守住五条不变量:
- 完整 secret 只展示一次,绝不以明文存储或写入日志。
- 有效 Key 能识别机器主体,但不会自动授权所有操作。
- 一个 Key 只属于一个租户和一个环境。
- 吊销要在明确且可测试的传播时限内生效。
- 轮换期间新旧 Key 可以重叠使用,同时仍能识别每次请求具体使用了哪个凭证。
面试官在考察什么
第一个信号是候选人能否区分认证与授权。验证 secret 只能确认请求来自哪个 API Key 主体;服务仍要检查 scope、端点策略、资源归属和租户隔离。
第二个信号是秘密处理。好的回答会用密码学安全随机数生成器产生不透明的高熵 secret,只展示一次,只保存 verifier,在所有链路脱敏,并把服务端 pepper 放在专用秘密管理系统中。
第三个信号是生命周期设计。Key 需要名称、环境、状态、过期时间、scope、归属、轮换和吊销。把 Key 当成数据库里一条永久字符串,会导致多人共享和替换中断。
第四个信号是运维推理。缓存、限流、日志、事件响应和可用性都会改变安全边界。如果边缘节点还能用十分钟前的缓存接受已吊销 Key,就不能宣称“立即吊销”。
最后,候选人应主动排除不可信客户端和足够敏感的操作。写进前端代码的静态 bearer secret 可以被用户或攻击者取出。高价值或用户委托场景可能需要短期工作负载凭证、OAuth、双向 TLS、请求签名或增强验证。
回答前要澄清的问题
- 谁持有凭证? 后端服务可以保护秘密;浏览器、移动应用、桌面二进制和公开仓库无法保证秘密不被取出。
- Key 代表谁? 明确它代表租户集成、内部工作负载还是自然人。本题把它定义为租户拥有的机器主体,不是用户会话。
- 操作有多敏感? 只读分析与资金操作不应使用完全相同的控制。
- 吊销和可用性目标是什么? 设定可测量的传播时限,并决定主 Key 存储不可用时认证如何处理。
- 环境如何隔离? 测试与正式 Key 要有不同前缀、数据边界、权限和限额。
- 权限粒度多细? 用粗粒度 scope 加资源级授权;除非产品确实需要,不要先造一个无边界的自定义策略语言。
- 每个租户能建多少 Key? 数量上限可以控制 Key 蔓延,也能阻止客户通过无限建 Key 绕过单 Key 限额。
- 审计与合规有什么要求? 保留期限、创建人、最近使用、审批和紧急访问都可能受约束。
30 秒回答框架
“我会给每个 Key 分配可查询的 public ID 和不透明随机 secret,例如 aklive7F3KQ2.m8…Vw。完整值只返回一次。数据库保存 public ID、租户、环境、scope、状态、过期时间和带密钥的 verifier,不保存明文 secret。
每个 TLS 请求到达后,网关从请求头取 Key,按 public ID 查记录,重新计算 verifier,并以常量时间比较,然后检查状态和过期时间。验证通过后创建机器主体上下文。端点 scope 和租户资源归属要另行校验。限流同时作用于 Key 和租户,审计日志只记录 public ID。
常规轮换时创建第二个 Key,部署后观察两个 ID 的使用情况,再吊销旧 Key。泄露时不留宽限期,立即吊销、排查日志并签发替代 Key。缓存必须在声明的时限内失效。公开客户端、用户委托和高价值操作应改用更强或短期凭证。”
分步深入设计
第一步:建模身份与 Key 记录
把每个 Key 建模成独立机器主体,不要让一个租户的全部集成共享同一 secret。一个实用记录是:
ApiKey(
key_id, tenant_id, environment, name, verifier,
verifier_version, scopes, status, expires_at,
created_at, created_by, last_used_at
)keyid 可以公开并用于查询;name 帮助管理员区分“账单导出”和“仓库同步”。status 至少支持 active 与 revoked,过期时间单独判断。createdby 和近似的 lastusedat 改善归属追踪。不要在每个请求里同步更新 lastusedat,这会制造写热点;分钟级精度足够时,可以异步聚合或采样更新。
scope 表达 invoices:read 这类粗粒度能力,不能代替资源授权。Key 通过验证后,请求发票 123 的查询仍必须受认证上下文里的 tenant_id 约束。调用方提交的 tenant 字段永远不能充当权限依据。
第二步:生成一次、展示一次、只存 verifier
使用密码学安全随机数生成器产生 32 字节随机 secret。这是本设计的具体选择,不是所有协议都必须采用的通用标准。将它编码为适合传输的字符,并与易识别的前缀、public ID 组合:
ak_live_7F3KQ2.m8...opaque-secret...Vw前缀让支持工具在不接触 secret 的情况下识别凭证类型和环境。只在创建成功响应里返回完整 Key,并明确提示它无法找回;丢失后只能创建替代 Key。
数据库保存 HMAC-SHA-256(serverpepper, completesecret) 作为 verifier。pepper 放在秘密管理系统中,与数据库隔离。对于随机性足够的 token,直接保存 SHA-256 摘要也可行;带密钥的 verifier 能在只有数据库泄露时增加一层防护。为 verifier 记录版本,以便迁移算法或 pepper。pepper 迁移需要有界的双版本验证或明确的 Key 重发方案,不能悄悄让所有客户 Key 同时失效。
创建 Key 是受认证的管理操作。生成 secret 前要检查租户角色、Key 数量上限、允许的 scope、环境、过期策略和所需审批。保存记录后通过禁止缓存的响应返回 secret。应用、代理、链路追踪、错误上报和支持工具都必须对认证头与响应体脱敏。
第三步:认证请求,但不扩大权限
强制使用 TLS,并只从认证头或专用请求头接收 Key,绝不放在 URL 查询参数中。URL 经常进入历史记录、分析系统、代理日志和 referrer。先拆解并校验格式,再用 public ID 做索引查询,尽早拒绝格式错误输入。
查到候选记录后,重新计算 verifier 并使用常量时间比较,然后检查环境、active 状态和过期时间。对外为未知、格式错误、过期和已吊销 Key 返回同一种通用错误,避免接口成为 Key 枚举通道;内部只记录不含 secret 的安全原因码。
验证成功后创建包含 keyid、tenantid、环境和 scopes 的上下文。路由策略检查所需 scope,数据层检查租户和资源归属。管理端点或高价值端点可以完全拒绝 API Key 主体,或者要求额外控制。
第四步:用分层限额和监控约束滥用
限流不能证明身份,但能限制被盗或失控凭证造成的损失。对每个 Key 施加突发与持续速率限制,再加租户总限额。租户层可以防止客户通过创建多个 Key 放大容量。昂贵端点还可能需要按成本计权的预算和并发上限。
日志记录 public key ID、租户、路由、决策、延迟、策略允许的来源网络元数据和请求关联 ID,绝不记录完整 Key、verifier 或可复用认证头。对异常地域或网络变化、失败激增、scope 拒绝激增、长期闲置 Key 突然活跃,以及轮换通知后的旧 Key 流量告警。这些都是调查信号,不是自动认定泄露的证据。
Key 管理端点应比普通数据端点受更严格保护:强用户认证、基于 Cookie 控制台的 CSRF 防护、明确授权、审计事件、创建上限,以及按风险增加重新认证或审批。
第五步:协调缓存与快速吊销
用索引查询 verifier 最简单,也能让数据库给出权威决定;请求量很高时才考虑缓存。缓存只按 public ID 保存 verifier 和最少授权元数据,传输要加密,条目要有界;绝不缓存调用方提交的 secret。
吊销先写权威状态,再向各网关发布失效通知。若失效消息丢失,短 TTL 是兜底。本题可以把目标定义为:已吊销 Key 在五秒内被所有网关拒绝,并在丢包和节点重启条件下测试。十分钟 TTL 无法支持这个承诺。
失败策略按风险选择。敏感写操作无法取得足够新的 Key 状态时应失败关闭。少量低风险读取可以明确选择暂时使用已验证的短期旧缓存,但这会违背严格的即时吊销,不能悄悄设为默认行为。
第六步:把轮换和吊销设计成不同流程
常规轮换需要重叠期:
- 创建一个权限不高于旧 Key 的新 Key。
- 通过客户的秘密管理链路交付。
- 部署并灰度验证新 Key。
- 按 public key ID 观察请求,直到旧 Key 不再使用。
- 吊销旧 Key,并确认没有流量继续依赖它。
不要原地修改旧 secret。两个独立 ID 才能保留归属、灰度和迁移期间的回退能力。过期时间可以限制最大寿命,但缺少采用情况观测的强制过期会制造本可避免的故障。
疑似泄露的顺序不同:不留宽限期,先吊销并清理缓存,再确认受影响租户、scope、路由和时间窗,检查审计证据,签发最小权限替代 Key,并修复泄露源。立即删除记录可能损失事件调查证据;应按策略保留不含 secret 的元数据。
第七步:知道 API Key 何时不够用
API Key 是 bearer 凭证,持有者即可使用。它本身不能证明调用方仍运行在预期工作负载上,不能表达终端用户同意,也不能阻止重放。不要把 secret Key 嵌入浏览器 JavaScript、移动二进制、示例代码、容器镜像或代码仓库。
云工作负载有条件时优先使用短期工作负载身份;用户委托访问使用范围明确且会过期的授权协议;特别敏感的服务间调用可考虑双向 TLS 或请求签名,让复制一条数据库值不足以完成冒用。选择应服从威胁模型;给每个 API 同时叠加所有机制只会增加运维复杂度。
第八步:测试安全、生命周期和失败模式
至少覆盖以下对抗矩阵:
- 错误前缀、未知 ID、错误 secret,以及 verifier 的常量时间处理;
- 过期、已吊销、测试 Key 调正式环境和 scope 不足;
- 认证成功后尝试跨租户读取资源;
- 代理、应用、追踪、错误和审计输出中的 secret 脱敏;
- 轮换重叠、旧 Key 使用观测、过期和紧急吊销;
- 旧缓存、失效消息丢失、网关重启和 Key 存储故障;
- 单 Key 与租户限额,包括同一租户使用多个 Key;
- 并发创建、请求处理中途吊销,以及重复管理请求。
还要扫描代码仓库和部署配置中的可识别 Key 前缀。检测器是补救手段,不代表可以把秘密提交到源码。用测试 Key 演练泄露响应:测量从确认吊销到所有网关拒绝的时间,并确认日志保留归属信息却不保留 secret。
高质量示范回答
“我会把每个 API Key 当成属于单一租户和单一环境的命名机器主体。签发值由公开查询 ID 与不透明随机 secret 组成,只通过 TLS 返回一次;数据库保存带密钥的 verifier 和生命周期元数据,所有日志与追踪层都对完整值脱敏。
网关按 public ID 查询记录,重新计算 verifier,以常量时间比较,并检查环境、状态和过期时间。验证通过后得到包含租户与 scope 的上下文;每条路由仍检查 scope,每次数据查询仍检查租户归属。单 Key 限额约束单个集成,租户限额防止多 Key 放大额度。
如果缓存验证元数据,吊销要先更新权威状态并主动推送失效通知,再用短 TTL 兜底。我会定义并实测五秒吊销目标。常规轮换创建第二个 Key,灰度后观察两个 public ID,再吊销旧 Key。疑似泄露不留宽限期:立即吊销,排查该 Key 的 scope 与活跃时间窗,签发权限更小的替代 Key,并修复暴露源。
浏览器和移动应用、终端用户身份,以及高价值操作的唯一控制都不应使用这种静态 secret;这些场景需要委托式、短期、工作负载绑定或更强凭证。”
常见错误
- 保存明文 Key 以便再次展示。 找回方便会让一次数据库读取直接变成凭证泄露;只展示一次并支持替换。
- 只保存全局哈希,不保存生命周期元数据。 单纯验证无法回答租户、环境、scope、过期、归属和吊销问题。
- 把 Key 放进查询参数。 URL 经常被复制和记录;应通过 TLS 请求头传递。
- 把 scope 当成租户授权。
invoices:read不能证明发票123属于认证租户。 - 所有集成共享一个租户 Key。 泄露的爆炸半径更大,也无法准确归属请求。
- 只做单 Key 限流。 租户可以创建或轮换多个 Key 来放大流量。
- 缓存数分钟却声称立即吊销。 定义传播目标,主动失效,并测试缓存故障。
- 通过覆盖 secret 完成轮换。 这样失去重叠期、归属、灰度和清晰回退路径。
- 在公开客户端使用静态 secret。 混淆处理无法创造可信的秘密存储边界。
- 为排查认证问题记录凭证。 只记录 public ID 和安全原因码,绝不记录可复用 secret。
追问与参考答案
为什么使用 public ID 加 secret,而不是散列整个 Key 后扫描所有记录?
public ID 提供索引查询、安全的支持标识和归属信息,secret 才是证明。扫描全部 verifier 既慢,也容易诱发危险日志或二级索引。public ID 本来就不是秘密,因此知道它不能帮助推导 secret。
API Key 的 verifier 可以使用快速哈希吗?
secret 有足够密码学随机性时可以,因为它不像人类密码那样来自可猜词典。带密钥 HMAC 可以在数据库泄露而 pepper 未泄露时增加保护。人类选择的密码仍应使用密码哈希;两者威胁模型不同。
如何轮换服务端 pepper?
每条记录保存 verifier 版本。在有界迁移期内,根据版本选择新旧 pepper;旧版本 Key 成功认证时,内存中已有提交的 secret,可以重新生成新版本 verifier。另一种做法是安排客户重新签发 Key。依赖旧 pepper 的 verifier 尚未迁移或过期前,不能删除旧 pepper。
lastusedat 是否必须精确?
通常不必。每个请求都更新同一行会增加写负载和竞争。可以发送采样或聚合使用事件,定期更新。安全审计仍可在请求层追加记录,管理界面则明确标注 lastusedat 是近似值。
吊销与正在处理的请求发生竞争时怎么办?
要明确边界。认证层可以保证传播完成后启动的请求被拒绝。敏感操作可以在提交前重新检查授权,或把 Key 状态绑定进事务策略。吊销无法撤回已经提交的操作,因此事件响应必须调查这一时间窗。
Key 是否应该自动过期?
过期能限制无限期暴露,但不能代替轮换和吊销。平台应提前通知所有者、展示旧 Key 使用情况、允许安全重叠,并在期限后拒绝。最长寿命取决于风险,以及是否存在更好的短期凭证。
IP 白名单能解决 Key 被盗吗?
它可以作为固定出口地址客户的额外限制,但不能代替 secret 验证。网络变化或共享代理还可能导致故障或虚假安全感。把它当成一层信号或策略,不要当作身份根基。
创建 Key 的响应应该包含什么?
只在这次响应中返回完整 Key,同时返回 public ID 或前缀、名称、环境、scopes、过期时间和创建元数据。响应应禁止缓存,绝不返回 verifier 或 pepper。后续列表接口只返回公开标识与元数据。