API 密钥和 OAuth 解决的是不同的问题,选哪个先从一个问题开始:这套流程里有没有第三方?
如果是你的服务器以自己的名义调用别人的 API,用密钥。如果是别人的应用以你的用户的名义调用你的 API,用 OAuth。没有第三方的时候,OAuth 增加的是仪式,不是安全。
下面讲清楚二者到底差在哪、OAuth 这几年变了什么,以及 API 密钥有一条几乎没人写、却决定你在生产环境怎么跟它相处的性质。
API 密钥:一串说「这个程序可以」的字符
密钥就是一串又长又随机的字符,放进请求头里:
X-Api-Key: lix_live_9f2c...
服务器找到这个密钥,判断它属于哪个账号,然后放行。机制到此为止。
由此推出的性质:
- 密钥不会自己过期。 它一直有效,直到有人撤销它;
- 密钥标识的是程序,不是人。 团队里具体谁发的请求,通常看不出来;
- 密钥能做账号能做的一切,除非你另外限制了权限;
- 密钥是唯一的秘密。 它泄露,访问权就泄露,中间没有缓冲。
OAuth:一份关于授权同意的协议
OAuth 2.0(RFC 6749)回答的是另一个问题。它有四个参与方:数据的所有者、想要数据的应用、授权服务器,以及存放数据的服务器。
流程大致是这样。应用把用户送到服务本身的页面。用户看清是谁在请求、请求什么,然后同意。应用拿到一个 code,换成 access token,再带着它调用 API。这个 token 活得很短——几分钟到几小时;过期后应用用 refresh token 换一个新的。
这带来的东西,是密钥从原理上给不了的:
- 用户的密码从不经过应用。 整件事的意义就在这里;
- 权限被切成 scope。「读取链接」和「删除链接」是两份不同的授权;
- 同意是可见且可撤回的。 用户能看到已连接应用的清单,可以只断开其中一个而不影响其余;
- token 会自己失效。 被偷走的 access token,一小时后就是废纸。
代价是复杂度:授权服务器、应用注册、同意页面、refresh token 的存储与轮换,以及每次调用都要处理过期。
按真正影响决策的维度对比
| API 密钥 | OAuth 2.0 | |
|---|---|---|
| 证明谁的身份 | 程序 | 程序所代表的那个用户 |
| 有效期 | 无限期 | access token 几分钟到几小时 |
| 如何终止访问 | 撤销密钥 | 撤销同意或 token |
| 权限范围 | 通常是整个账号 | 按动作划分的 scope |
| 第三方手里的秘密 | 密钥本身 | 只有 token,没有密码 |
| 接入成本 | 一个请求头 | 一套授权服务器和完整流程 |
| 适用场景 | 服务器对服务器、脚本、公司内部集成 | 公开应用、市场平台、「使用⋯⋯登录」 |
选择的判断规则
问题不在于抽象地比谁更安全,而在于谁把访问权委托给了谁。
你的服务器 → 别人的 API,账号是你的
→ API 密钥
别人的应用 → 你的 API,账号是你的用户的
→ OAuth
定时任务、CI、后端集成
→ API 密钥
用户需要按应用查看并撤销授权
→ OAuth
接收回传是密钥的教科书场景。当你的服务器告诉我们的服务器「这笔订单付款了」,这次交换里没有任何用户,没有人可同意,同意页面也无处安放。反方向的 webhook 同理。
你没留意的时候,OAuth 变了
关于这个话题的文章有一半描述的是 2015 年的 OAuth。此后它收窄了不少。
2025 年 1 月,IETF 发布了 RFC 9700《Best Current Practice for OAuth 2.0 Security》。这份文档汇总了多年积累的真实攻击经验,并正式废弃了两种过去被认为可接受的方式:
- implicit grant——把 token 直接返回在地址栏里的那种;
- resource owner password credentials——应用直接向用户索要账号密码。这恰恰是 OAuth 当初要消灭的东西。
与此同时,PKCE 成为所有客户端类型的强制要求,包括服务端客户端,而不再只针对移动端。
另外值得单独知道:OAuth 2.1 至今仍是草案,不是已发布的 RFC。它把同样这些变化收进一份文档——强制 PKCE、redirect URI 精确匹配、取消 implicit 与 password grant、禁止把 token 放进查询串。现在就把它当作现行标准来引用还太早。
实用结论:如果你读到的 OAuth 教程还在推荐 implicit flow,那份教程过时了。
API 密钥那条很少被提起的软肋
密钥不会过期。这意味着撤销是你唯一的手段。而尴尬之处在于:撤销通常不是即时的。
每个请求都校验密钥,就意味着每次都查数据库。任何有真实流量的 API 都会把这个结果缓存起来。以我们为例,密钥到账号的对应关系在缓存里存放五分钟。于是从点下「撤销」到访问真正被拒,中间最多有五分钟,在这段时间里被泄露的密钥照样能用。
这不是疏忽,是一笔交易:不缓存,每个请求都要敲数据库。几乎所有基于密钥的 API 都做了类似的取舍,只是不太有人明说。知道它有两个理由。其一,真发生泄露时,撤销密钥是第一个动作而不是最后一个,之后还得查这几分钟里发生了什么。其二,这正是 OAuth 的短时效 token 从另一端解决的问题——它们自己就会过期,不需要数据库参与。
怎样跟密钥相处才不会吃亏
如果密钥够用——大多数集成都够用——那么底线是这些:
- 只保存哈希。 服务器把收到的密钥的哈希与存储的哈希比对;原始字符串只在创建时向用户展示一次。我们按 SHA-256 比对。
- 给密钥加前缀。 形如
lix_live_...的字符串,代码仓库和日志管线里的密钥扫描器认得出来。 - 每个集成一把独立密钥。 撤销其中一把不会波及其余,审计记录也能看清到底是哪一把出了事。
- 绝不要把密钥放进查询串。 地址会留在 Web 服务器日志、
Referer头和浏览器历史里。只走请求头。 - 按计划轮换,而不是出事才轮换。 轮换过一次的密钥,下次换得很快;从没换过的那把,最后会发现它被硬编码在四个地方。
- 盯住最后使用时间。 半年没被碰过的密钥不是备用件,是一扇敞着的门。
我们用的是什么
Lix.li 的 API 采用密钥认证:X-Api-Key 请求头,密钥在后台创建和撤销,按哈希比对,每把密钥有独立的请求限额。没有 OAuth,这是有意为之:这套 API 的使用场景都在服务端——创建链接、取统计、接收转化——其中没有需要用户点头同意的第三方。
如果哪天需要让别人的应用进入我们客户的账号,密钥就不够用了:用户必须能看见自己授权给了谁什么,并且能只断开一个应用而不弄坏其他的。到那时候,OAuth 就不再是仪式,而是唯一诚实的选项。
具体的调用写在 API 文档里,包括密钥无效与密钥已撤销分别返回什么响应码。相邻话题里,为什么 301 跳转会把 POST 变成 GET 值得所有正在接回传的人一读:带着密钥发出的请求,到达时用的方法可能已经不是发出时那个。