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 从另一端解决的问题——它们自己就会过期,不需要数据库参与。

怎样跟密钥相处才不会吃亏

如果密钥够用——大多数集成都够用——那么底线是这些:

  1. 只保存哈希。 服务器把收到的密钥的哈希与存储的哈希比对;原始字符串只在创建时向用户展示一次。我们按 SHA-256 比对。
  2. 给密钥加前缀。 形如 lix_live_... 的字符串,代码仓库和日志管线里的密钥扫描器认得出来。
  3. 每个集成一把独立密钥。 撤销其中一把不会波及其余,审计记录也能看清到底是哪一把出了事。
  4. 绝不要把密钥放进查询串。 地址会留在 Web 服务器日志、Referer 头和浏览器历史里。只走请求头。
  5. 按计划轮换,而不是出事才轮换。 轮换过一次的密钥,下次换得很快;从没换过的那把,最后会发现它被硬编码在四个地方。
  6. 盯住最后使用时间。 半年没被碰过的密钥不是备用件,是一扇敞着的门。

我们用的是什么

Lix.li 的 API 采用密钥认证:X-Api-Key 请求头,密钥在后台创建和撤销,按哈希比对,每把密钥有独立的请求限额。没有 OAuth,这是有意为之:这套 API 的使用场景都在服务端——创建链接、取统计、接收转化——其中没有需要用户点头同意的第三方。

如果哪天需要让别人的应用进入我们客户的账号,密钥就不够用了:用户必须能看见自己授权给了谁什么,并且能只断开一个应用而不弄坏其他的。到那时候,OAuth 就不再是仪式,而是唯一诚实的选项。

具体的调用写在 API 文档里,包括密钥无效与密钥已撤销分别返回什么响应码。相邻话题里,为什么 301 跳转会把 POST 变成 GET 值得所有正在接回传的人一读:带着密钥发出的请求,到达时用的方法可能已经不是发出时那个。