第 56 章 / 共 57 章 · 约 5 分钟
API 设计 4:认证授权与限流
开放给第三方开发者的 API,要回答两个问题:你是谁、你能做什么(认证和授权),以及你能调用多少(限流和配额)。案例是一个天气开放平台:开发者注册项目拿到 API Key 查询天气预报;另外,用户可以授权第三方 App 读取自己在平台上收藏的地点。
认证与授权
认证回答“你是谁”,授权回答“你被允许做什么”。401 表示认证失败(没带凭证或凭证无效),403 表示认证成功但没有权限。
API Key:识别调用方的应用
查询公开的天气数据,只需要知道是哪个开发者项目在调用,用于计费和限流。API Key 就够了:
- Key 是一个足够长的随机串,只在创建时显示一次。
- 服务端只存 Key 的哈希,数据库泄露也拿不到能用的 Key。因为 Key 本身是高熵随机数,用 SHA-256 这类快速哈希就够了,不需要专门对付弱密码的慢哈希。
- 再存一个前缀用于展示(
wx_live_AbC1),开发者能在控制台里认出是哪个 Key。 - 允许一个项目同时有两个有效的 Key,方便轮换:先发新 Key,部署完成后再吊销旧 Key。
- Key 只能放在服务端。写进网页或 App 安装包里,等于公开了。
import hashlib
import secrets
def new_api_key() -> tuple[str, str, str]:
"""Returns (shown once to the developer, prefix kept for display, hash kept for lookup)."""
key = "wx_live_" + secrets.token_urlsafe(32)
return key, key[:12], hashlib.sha256(key.encode()).hexdigest()
def find_key(db, presented: str):
digest = hashlib.sha256(presented.encode()).hexdigest()
row = db.one("SELECT project_id, revoked FROM api_keys WHERE key_hash = ?", (digest,))
return None if row is None or row.revoked else row.project_id
OAuth 2.0:代表用户访问
“读取用户收藏的地点”涉及用户自己的数据,API Key 解决不了:第三方 App 不能拿着用户的密码来调用。这时用 OAuth 2.0 的授权码流程,用户在平台自己的页面上登录并同意授权,第三方 App 最终拿到一个只包含特定权限(scope,比如 places:read)的访问令牌:
查看图表源码
sequenceDiagram
accTitle: OAuth 2.0 授权码流程加 PKCE:第三方 App 代表用户调用 API
participant U as 用户
participant App as 第三方 App
participant AS as 授权服务器
participant API as 天气 API
App->>App: 生成 code_verifier,计算 code_challenge
App->>AS: 跳转 /authorize(scope、code_challenge)
U->>AS: 登录并同意授权
AS-->>App: 回调,带上一次性的授权码
App->>AS: POST /token(授权码 + code_verifier)
AS-->>App: access_token(1 小时)+ refresh_token
App->>API: GET /v1/me/places(Bearer access_token)
API-->>App: 用户收藏的地点
PKCE 是给手机 App 和单页网页这类“公共客户端”加的保护。它们无法安全地保存客户端密钥,授权码在回调过程中可能被其他程序截获。PKCE 让 App 在开始时生成一个随机的 code_verifier,只把它的哈希 code_challenge 发给授权服务器;换令牌时必须出示原始的 code_verifier。截获授权码的人没有它,就换不到令牌。现在的建议是所有客户端都使用 PKCE。
服务器之间的调用(没有用户参与)用客户端凭证流程:用客户端 ID 和密钥直接换令牌。
令牌:JWT 还是不透明令牌
| JWT | 不透明令牌 | |
|---|---|---|
| 校验方式 | 本地验签,不用查库 | 查库或调用授权服务器的校验接口 |
| 吊销 | 到期前很难吊销 | 删掉记录立即生效 |
| 适合 | 高频调用、多个服务各自校验 | 需要随时吊销的场景 |
用 JWT 时,访问令牌的有效期要短(几分钟到一小时),再配合刷新令牌续期;用户撤销授权时,吊销刷新令牌,已发出的访问令牌最多再活一个有效期。
限流与配额
两个概念要分开:
- 速率限制防突发:每分钟最多 60 次,保护服务不被瞬时流量打垮。常用令牌桶,允许短时突发,长期平均不超过速率。
- 配额是商业约定:免费版每天 1,000 次,付费版每天 100 万次,按天或按月计。
限流的键可以是 API Key、用户、IP,或者它们的组合。未登录的接口只能按 IP 限。计算量差别很大的接口(查当前天气和导出十年历史数据)不应该按次数一视同仁,可以给每个接口定一个“成本”,按成本扣额度。
超限时返回 429,并告诉调用方还要等多久、额度还剩多少:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 12
Content-Type: application/problem+json
{"type": "https://api.example.com/errors/rate-limited", "title": "Too many requests",
"status": 429, "detail": "The free plan allows 60 requests per minute."}
Retry-After 是标准头;剩余额度的头还没有完全统一,常见的是 X-RateLimit-* 这一组,IETF 正在制定标准的 RateLimit 头字段。多台网关共享计数时,计数器放在 Redis 里,具体算法和分布式实现见限流器一章。
扩展讨论
- Key 泄露检测:Key 带固定前缀,代码托管平台的密钥扫描就能识别并通知你,自动吊销。
- 最小权限:Key 也可以有 scope,只读 Key 不能修改数据;按环境区分测试 Key 和生产 Key。
- 审计:记录每个 Key 和令牌的调用日志,出了安全事件能追溯是谁、在什么时候、调用了什么。