cswork / 系统设计

第 56 章 / 共 57 章 · 约 5 分钟

API 设计 4:认证授权与限流

本页目录
  1. 认证与授权
  2. 限流与配额
  3. 扩展讨论
  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)的访问令牌:

OAuth 2.0 授权码流程加 PKCE:第三方 App 代表用户调用 API
滚动到此处后加载图表…
图表可横向滚动,展开后可缩放查看。
查看图表源码
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 和令牌的调用日志,出了安全事件能追溯是谁、在什么时候、调用了什么。

章节测验

3 道选择题
  1. 手机 App 使用 OAuth 授权码流程时,为什么要加 PKCE?
  2. 为什么 JWT 访问令牌通常只给很短的有效期?
  3. 免费版开发者超过了每分钟 60 次的限制,API 应该怎么响应?

章节讨论

正在加载讨论…