bigywater.tech
Flask · 110

Refresh Token

为什么要有 refresh token?轮换是什么?令牌被偷了怎么发现?登出之后访问令牌还能用吗?这篇用真实的令牌流程把每种情况的状态码打印出来。

family revoked
旧刷新令牌被重用后,整个登录家族被撤销
401
响应丢失后用旧刷新令牌重试得到的结果(合法用户被误伤)
Path=/refresh
刷新令牌 cookie 只发送给刷新接口

访问令牌与刷新令牌

实测 · 本机执行输出
  登录得到 access + refresh;/me → 200
  2 秒后 /me → 401 {'error': 'ExpiredSignatureError'}
  /refresh → 200 拿到新的 access 与新的 refresh(两者都变了): True True
  新 access 访问 /me → 200

上一篇:JWT 登录认证;下一篇:API 限流。

轮换与重用检测

实测 · 本机执行输出
  再用已经用过的旧 refresh → 401 {'error': 'refresh token reuse detected; family revoked'}
  此后连合法的新 refresh 也失效 → 401 {'error': 'family revoked'}
  (整个「家族」被撤销:攻击者和真用户都得重新登录)

合法重试被误伤

实测 · 本机执行输出
  第一次 /refresh → 200 (但假设响应在路上丢了,客户端没收到新令牌)
  客户端用旧令牌重试 → 401 {'error': 'refresh token reuse detected; family revoked'}
  服务端发出的那个新令牌也作废了 → 401
  (严格的轮换会把这种合法重试当成重用;常见的缓解是给旧令牌一个几秒的宽限期——本篇没有实现)

登出与过期

实测 · 本机执行输出
  登出后用刷新令牌 → 401 {'error': 'family revoked'}
  但登出前签发的访问令牌还能用(过期前):/me → 200
  刷新令牌放了 5 秒之后 → 401 {'error': 'refresh token expired'}
  乱写的刷新令牌 → 401 {'error': 'unknown refresh token'}

放进 cookie

实测 · 本机执行输出
  Set-Cookie: refresh=<令牌>; Expires=Fri, 02 Oct 2026 05:07:52 GMT; Max-Age=5; Secure; HttpOnly; Path=/refresh; SameSite=Strict
  响应正文里只有 access,没有 refresh: ['access']

分步演示

Refresh 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,数字每次运行会有小幅抖动。PyJWT 2.15.1,令牌记录保存在内存字典里(演示用)
    • 没有验证:数据库持久化与并发刷新的竞争、宽限期的实现、跨设备的多个家族管理、浏览器里 HttpOnly 与 SameSite 的实际拦截效果、刷新令牌的滑动过期与绝对过期的取舍
    京ICP备2026067502号-1京公网安备11010502063487号