bigywater.tech
Flask · 108

API Token 认证

API 令牌怎么发、怎么存、怎么验?签名令牌安全吗?怎么吊销?放在 URL 里行不行?这篇把每个问题都用实际请求验证一遍,包括真实 gunicorn 访问日志的检查。

401 vs 403
没有有效凭证与权限不足的状态码区分
明文可读
签名令牌的 payload 不需要密钥就能解出
进访问日志
放在查询参数里的令牌会出现在 gunicorn 访问日志里

Bearer 与状态码

实测 · 本机执行输出
  没有 Authorization 头                   → 401 {"error":"unauthorized","reason":"missing"} | WWW-Authenticate: Bearer realm="api"
  Authorization: Token abc(不是 Bearer)  → 401 {"error":"unauthorized","reason":"malformed"} | WWW-Authenticate: Bearer realm="api"
  Authorization: Bearer(没有令牌)          → 401 {"error":"unauthorized","reason":"malformed"} | WWW-Authenticate: Bearer realm="api"
  Authorization: Bearer 错误令牌           → 401 {"error":"invalid token"} | WWW-Authenticate: Bearer error="invalid_token"
  Authorization: bearer 正确令牌(小写)       → 200 {"uid":1} | WWW-Authenticate: None
  Authorization: Bearer 正确令牌           → 200 {"uid":1} | WWW-Authenticate: None
  401 与 403:普通用户访问 /admin → 403 | 管理员 → 200

上一篇:Idempotency-Key;下一篇:JWT 登录认证。

令牌的存储

实测 · 本机执行输出
  令牌 qU6G0vk4TljGScGmXZWbKxq_LZMolXY5LwXX2GgL-fE (长度 43,secrets.token_urlsafe(32) = 32 字节随机数 → 43 个字符)
  服务端存的是 sha256 十六进制: 585ce88ff6eb6e19e442702a… (长度 64)——数据库泄漏时拿不到可用令牌
  对比 hmac.compare_digest("a", "a") = True (比较哈希时用它,不用 ==)

签名令牌

实测 · 本机执行输出
  令牌: eyJ1aWQiOjF9.ar87ow.HaKy25Hyhou809lkrNYY6CGlgkg
  不需要密钥就能解出 payload: {"uid":1}
  验证通过 → 200 {'uid': 1}
  篡改签名 → 401 {'error': 'invalid token'}
  改 payload 里的 uid 为 2(沿用原签名)→ 401
  max_age=1 秒,等 2 秒后 → 401 {'error': 'token expired'}

吊销

实测 · 本机执行输出
  不透明令牌删除后 → 401
  签名令牌:吊销动作之后(这里没有存储可删)→ 200 (仍然 200,要吊销必须另存黑名单或检查用户状态)
  用户被删除后签名令牌 → 401 (因为视图里还检查了用户是否存在)

查询参数里的令牌

实测 · 本机执行输出
  日志里能搜到查询参数里的令牌: True | 能搜到头里的令牌: False
  访问日志行示例: 127.0.0.1 - - [02/Oct/2026:13:05:42 +0800] "GET /me?token=SECRET-IN-QUERY HTTP/1.1" 200 2 "-" "-"

分步演示

API Token 认证
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。itsdangerous、gunicorn 访问日志
    • 没有验证:compare_digest 与 == 的计时差别、令牌的作用域(scope)与过期策略、客户端存放令牌的位置(浏览器 localStorage 与 cookie 的风险)、令牌泄漏后的检测与轮换、OAuth 2.0 的完整流程
    京ICP备2026067502号-1京公网安备11010502063487号