题目与适用场景
一个多租户 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,例如 ak_live_7F3KQ2.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
)key_id 可以公开并用于查询;name 帮助管理员区分“账单导出”和“仓库同步”。status 至少支持 active 与 revoked,过期时间单独判断。created_by 和近似的 last_used_at 改善归属追踪。不要在每个请求里同步更新 last_used_at,这会制造写热点;分钟级精度足够时,可以异步聚合或采样更新。
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(server_pepper, complete_secret) 作为 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 的安全原因码。
验证成功后创建包含 key_id、tenant_id、环境和 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。
last_used_at 是否必须精确?
通常不必。每个请求都更新同一行会增加写负载和竞争。可以发送采样或聚合使用事件,定期更新。安全审计仍可在请求层追加记录,管理界面则明确标注 last_used_at 是近似值。
吊销与正在处理的请求发生竞争时怎么办?
要明确边界。认证层可以保证传播完成后启动的请求被拒绝。敏感操作可以在提交前重新检查授权,或把 Key 状态绑定进事务策略。吊销无法撤回已经提交的操作,因此事件响应必须调查这一时间窗。
Key 是否应该自动过期?
过期能限制无限期暴露,但不能代替轮换和吊销。平台应提前通知所有者、展示旧 Key 使用情况、允许安全重叠,并在期限后拒绝。最长寿命取决于风险,以及是否存在更好的短期凭证。
IP 白名单能解决 Key 被盗吗?
它可以作为固定出口地址客户的额外限制,但不能代替 secret 验证。网络变化或共享代理还可能导致故障或虚假安全感。把它当成一层信号或策略,不要当作身份根基。
创建 Key 的响应应该包含什么?
只在这次响应中返回完整 Key,同时返回 public ID 或前缀、名称、环境、scopes、过期时间和创建元数据。响应应禁止缓存,绝不返回 verifier 或 pepper。后续列表接口只返回公开标识与元数据。