一把 key 贴进公开仓库,只需要一次复制粘贴。
一、一把 key 出现在了公开仓库的第 47 行
一位做电商插件的独立开发者,给三家客户开过 API key。其中一家客户的实习生把示例项目传到了公开仓库,config.py 的第 47 行留着生产环境的 key。
以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。
两周后账单涨了 11 倍。他去查是谁在用,才发现自己没有记录任何调用归属:日志里只有 IP 和路径,没有 key 标识,也没有客户维度的用量。他能确定的只有「有人在用」,说不清「是谁、用来做什么」。
他做的第一件事是撤销那把 key,第二件事是给剩下两家客户重新签发,第三件事才是补审计。顺序不算错,但已经晚了两周。
key 是凭据,不是配置项。 配置项可以复制、可以进仓库、可以在群里传;凭据一旦离开受控环境,就只能按「已泄露」处理。判断标准很简单:这段字符串能否直接换到钱或数据。能,它就是凭据,管理方式必须和密码同级。
凭据的生命周期必须能回答四个问题。 谁签发的、给谁的、能做什么、最后一次用是什么时候。四个问题里缺任何一个,泄露时就只能靠猜,而猜出来的影响面无法用于对客户解释,也无法用于计费争议。
公开仓库不是唯一的泄露面。 截图、工单附件、聊天记录、CI 日志、前端打包产物、错误上报里的请求头,都是同一类事故的不同入口。把它们放在一起看,才会得出「默认按已泄露设计」这个结论,而不是「我小心一点就好」。
泄露通常不是被「发现」的,是被「撞见」的。 账单异常、客户来问、平台发邮件、陌生 IP 出现在后台,这四种方式都依赖别人先注意到异常。没有审计日志的人只能等第一种,而账单异常往往滞后一个计费周期。
「查不出是谁在用」不是权限问题,是设计缺口。 只要签发时没记归属、调用时没写 key_id,事后补任何工具都还原不出调用方。这类缺口没法靠加班补,只能在下一把 key 上修。
二、签发与环境隔离
签发环节的目标不是「把字符串发出去」,而是「发出去之后还管得住」。
| 属性 | 做法 | 验收 |
|---|---|---|
| 环境 | 测试与生产使用不同密钥体系,用 sk_test_ / sk_live_ 前缀区分 | 生产 key 无法调用测试端点,反之亦然 |
| 归属 | 一个客户一把 key,不共享;同一客户多环境各一把 | 任意一次调用都能反查到唯一客户 |
| 可识别前缀 | 固定业务前缀 + 8 位随机短码 | 在日志里能凭前缀定位到 key 记录 |
| 明文展示 | 仅在创建时展示一次,之后只显示后四位 | 控制台无法再次读取完整明文 |
| 存储 | 只存哈希与前后缀,不存明文 | 数据库泄露不等于 key 泄露 |
| 创建入口 | 控制台手动创建并留痕,禁止邮件或聊天工具发送 | 每把 key 都有创建人与创建时间 |
只显示一次明文,是为了让「找回」这个动作不存在。 一旦允许找回,明文就必须以可逆形式存储,整套设计立刻退化成「把密码明文存库」。正确做法是丢失即轮换,把找回成本转移给签发流程,而不是转移给数据库。
存哈希要兼顾校验效率。 存 sk_live_ 前缀、8 位短码与完整 key 的 SHA-256,校验时先按前缀取候选行再比对哈希。这样单次校验不需要全表扫描,库里也不存在可直接使用的字符串。
测试环境要能安全地「像生产」。 测试 key 走同一套鉴权与审计代码路径,只换数据源和计费开关。否则测试环境验证过的行为,到生产会多出没测过的分支,而这类分支通常正好在限额和错误处理上。
可识别前缀的价值在日志里,不在界面上。 前缀让 secret scanning 和日志检索能直接命中候选串,也让你在 grep 时不会漏掉。随机部分只负责不可猜测,前缀负责可发现,两者分工不同,不能合并成纯随机。
key 与登录账户是两套凭据,不要混用。 管理 key 的接口必须走会话或管理密钥,业务 key 只能调用业务端点。混用之后,一把客户 key 就能改自己的配额,最小权限的设计从一开始就失效了。
三、作用域、最小权限与审计
作用域的设计原则是:默认拒绝,按需开口,开口要能被收回。
| 作用域 | 允许的操作 | 典型客户 | 主要风险 |
|---|---|---|---|
read:orders | 只读订单列表与详情 | 报表工具 | 数据外泄 |
write:orders | 创建与更新订单 | 自建后台 | 脏数据与重复扣款 |
read:customers | 只读客户资料 | CRM 同步 | 个人信息合规 |
admin | 管理 key、调整配额 | 极少,且应限时 | 全量失控 |
作用域要落到端点,而不是落到文档。 每个端点声明自己需要哪个作用域,未声明的默认拒绝;admin 类作用域额外要求 IP 允许列表与到期时间。文档写「请只申请需要的权限」没有约束力,服务端拒绝才有。
IP 允许列表是取舍项:它能显著降低 key 被搬走后被滥用的概率,但会挡住动态 IP、移动网络与无固定出口的客户。可行做法是「默认不强制,敏感作用域强制」,并在控制台提供自助维护入口,避免每次变更都变成人工工单。
审计日志的字段要围绕「事后能不能算账」来定,而不是围绕「记了多少行」。
| 字段 | 示例 | 用途 |
|---|---|---|
| key_id | key_7f3a | 定位具体凭据,不记录明文 |
| 客户 | acme | 按客户聚合用量与成本 |
| 时间 | ISO 8601 带时区 | 计算暴露时长 |
| 端点 | GET /v1/orders | 判断影响面与数据范围 |
| 结果 | 200 / 401 / 429 | 区分正常调用、鉴权失败与限流 |
| 来源 | IP 与 UA 摘要 | 识别共享 key 与扫描行为 |
| 耗时 | 42 ms | 识别扫描式调用与异常慢请求 |
保留期是成本与追查能力的平衡点。 90 天足以覆盖大多数「两周后才发现的泄露」,再长就要为存储付费。超出保留期的聚合用量应继续保留,只丢弃可识别到单次调用的明细。
隐私边界要在写日志前定好。 不记录请求体中的个人信息,UA 只留摘要,IP 按合规要求做截断或哈希。审计日志本身也是个人数据集合,泄露它就是第二次事故。
共享 key 只能靠行为特征识别。 同一把 key 在一小时内出现在三个国家的 IP 上、调用间隔规律到毫秒级、只打某一个高成本端点,这三类模式比任何声明都可靠。识别之后先联系客户,再决定是否临时冻结。
审计写入不要阻塞主请求。 同步写库会把日志故障放大成接口故障,异步队列加本地缓冲更稳;代价是极端情况下会丢少量记录,因此在响应头里回传请求 ID,让客户侧也能对上账。
端点清单要跟着版本走。 新增端点时同步声明作用域,废弃端点时同步回收作用域。缺少这一步,作用域表会在半年内退化成一张没人维护的文档,最小权限也就只剩下说法。
四、轮换、撤销与泄露响应
轮换的难点不在技术,而在「旧 key 还有人在用」这个事实。解法是重叠期:新旧两把 key 同时有效,客户端按自己的发版节奏切换,服务端在重叠期结束后撤销旧 key。
重叠期 ≥ 客户端最长发版周期 × 1.5。 客户端最长 30 天才发一次版,重叠期就不能少于 45 天;否则一定会有客户在切换前被切断,然后来找你恢复一把已经撤销的 key。
| 时间窗 | 动作 | 负责人 |
|---|---|---|
| 0–1 小时 | 冻结该 key(拒绝新调用但保留日志),确认不是误报 | 值班,通常就是你自己 |
| 1–3 小时 | 正式撤销,签发替代 key 并通过受控渠道单独投递 | 开发者 |
| 3–8 小时 | 按审计日志统计调用量、端点分布与来源 IP | 开发者 |
| 8–24 小时 | 估算影响面与成本,决定是否通知客户与平台 | 开发者与客户对接人 |
| 24–72 小时 | 复盘泄露入口、为何没更早发现、补哪道检查 | 开发者 |
主动轮换的节奏按风险分档。 生产 key 每 90 天轮换一次,测试 key 每 180 天;发生过人员变动、设备丢失或供应商更换的 key 立即轮换,不等周期。撤销必须立即生效且不可恢复,不做「撤销后 24 小时内可恢复」这种便利功能。
泄露响应里最容易被跳过的是影响面估算。 大多数人的动作停在「撤销」,但客户真正会问的是「有没有数据被拿走、产生了多少费用」。这两个问题只能靠第三节的审计字段回答,所以审计不是出事后补的,是签发时就该有的。
双 key 并行期要能分开算账。 新旧 key 的用量分别统计,重叠期结束后用数据证明旧 key 已经归零,再撤销。凭感觉撤销,等于把风险从泄露转移到客户故障。
撤销要配合明确的错误信息。 返回 401 并在响应体里写清「该 key 已撤销,请使用新 key」,比返回一个笼统的鉴权失败省掉一轮支持成本。错误信息里不要回显 key 的任何片段。
五、行动清单与复算验收
- 每把 key 有唯一 ID、归属客户与环境,控制台可查创建人与创建时间
- 生产与测试 key 前缀不同,且不能互相调用
- 完整明文只在创建时展示一次,库里只存哈希与后四位
- 每个端点声明所需作用域,未声明的默认拒绝
- 审计日志至少记录 key_id、时间、端点、结果、来源,明细保留 90 天
- 有一份轮换计划,重叠期 ≥ 客户端最长发版周期 × 1.5
- 泄露后 0–24 小时的处置顺序写成文档,并完整演练过一次
- 公开仓库与 CI 日志接入 secret scanning,命中即告警
清单按上到下的顺序执行即可,前三项决定能不能查,后五项决定能不能停。八项全部打勾之前,不要对外宣传「企业级安全」,那是承诺而不是描述。
验收标准:任取一把线上 key,能在 5 分钟内回答「谁在用、能做什么、最后一次调用是什么时候」;模拟一次泄露,能在 1 小时内完成冻结与撤销,并在 24 小时内给出影响面与成本估算。
泄露暴露时长 = 发现时刻 − 该 key 最后一次合法调用时刻
示例:14:00 发现,09:30 最后调用 → 4.5 小时;目标 < 24 小时
轮换重叠期 ≥ 客户端最长发版周期 × 1.5
示例:最长 30 天 → 重叠期 ≥ 45 天
影响面 = 泄露期间该 key 的调用次数 × 单次最高成本
使用边界与实操验收
本文给的是单条链路的工程判据,不覆盖事故的完整处置。事故响应:被盗、被封、被停机、域名劫持的 72 小时 给事故处置总顺序,本文只讲 key 这一条线。平台合规:隐私政策、数据最小化、封禁申诉与开源许可 讲合规义务,本文讲鉴权与审计的工程判据。
演练方式:在测试环境故意把一把 sk_test_ key 提交到私有仓库,观察 secret scanning 是否告警、值班是否在 1 小时内冻结、审计日志能否还原调用量与端点分布。三个问题里任何一个答不上来,说明缺的是流程而不是工具。
本文不覆盖密钥托管服务选型、mTLS 与 OAuth 授权码流程;那些方案适合有专职运维的团队,对一个人的产品而言,先把 key 的签发、审计与撤销做扎实,收益更大。若客户明确要求单点登录,再单独评估。
关键参考与核验范围
以下链接支持对应的机制说明;本文的案例、数字与成本均为教学假设,不能当作平台统计或安全承诺。核验日期:2026-10-25。
- OWASP API Security Top 10 :API2:2023 针对失效的鉴权,API8:2023 针对安全配置错误,可作为自检清单。
- GitHub secret scanning :公开仓库的推送保护与告警机制,是本文「默认按已泄露设计」的外部兜底。
系列导航
- 上一篇:API 规范:OpenAPI 契约、统一错误模型与分页约定
- 下一篇:配额与限流:免费层、突发额度与超额计费
- 回到目录:一个人把 API 做成产品:对外接口、开发者体验与支持
- 相关阅读:事故响应:被盗、被封、被停机、域名劫持的 72 小时 、平台合规:隐私政策、数据最小化、封禁申诉与开源许可
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。