Flask-JWT-Extended 配双 Token 需分离生命周期:access token 短期(如15分钟),refresh token 长期(如7天)且必须可撤销;需显式设 expires_delta、refresh=True 装饰刷新接口、黑名单存 jti+user_identity+expires_at,并手动捕获 refresh token 过期异常。

Flask-JWT-Extended 怎么配双 Token(access + refresh)
双 Token 模式不是加两个 create_access_token 就完事,关键在生命周期分离和刷新逻辑闭环。access token 要短(比如 15 分钟),refresh token 要长(比如 7 天)但必须可撤销——否则“长期有效”就等于“永久后门”。
实操要点:
-
create_access_token和create_refresh_token必须传不同expires_delta,且 refresh token 的fresh参数得设为False(默认就是,但显式写出来防误读) - refresh endpoint 必须用
@jwt_required(refresh=True)装饰,不能用普通@jwt_required() - 调用
create_access_token(identity=identity)时,别传fresh=True——那是登录时才用的,refresh 场景下它应该为False
怎么把 refresh token 加进黑名单(避免登出/换密失效)
JWT 本身不可撤回,黑名单是唯一靠谱的实时控制手段。但很多人只存了 token 字符串,没存对应 jti 或没关联用户 ID,导致无法按用户批量踢出或清理过期项。
正确做法:
立即学习“Python免费学习笔记(深入)”;
- 黑名单存储必须包含三个字段:
jti(JWT 唯一 ID)、user_identity(比如 user_id)、expires_at(可选,用于定时清理) - 登出时调用
get_jti(encoded_token)解出jti,再存进 Redis 或 DB;别直接存整个 token 字符串——体积大、检索慢 - 在
@jwt.token_in_blocklist_loader回调里,只查jti是否存在,别做额外数据库 JOIN 或网络请求,否则每次鉴权都拖慢接口
为什么 @jwt.expired_token_loader 捕不到 refresh token 过期?
因为 Flask-JWT-Extended 默认只对 access token 触发过期回调。refresh token 过期时,它直接抛 ExpiredSignatureError 异常,走的是全局异常处理器,不是 JWT 自定义 loader。
要统一处理,得手动补一层:
- 在 refresh endpoint 里用
try/except捕获ExpiredSignatureError,返回 401 并明确提示 “refresh token expired” - 别依赖
@jwt.expired_token_loader覆盖所有场景——它只管 access token - 如果用了自定义
decode_key或非对称算法,还要确认verify_claims配置没关掉exp校验
Redis 黑名单怎么设 TTL 才不漏删?
不能只靠 Redis 的 EXPIRE 设和 token 一样的过期时间——token 过期后,黑名单条目还得留一段时间(比如 24 小时),防止重放攻击;但也不能永远不删,否则内存涨爆。
稳妥方案:
- 写入黑名单时,用
setex设置比 token 过期时间多 24 小时的 TTL(例如 access token 15m → 黑名单存 24h15m) - 定期用
SCAN+TTL清理已过期的 key,别用KEYS *——线上库数据量大时会阻塞 - 如果用 SQLAlchemy 存黑名单,记得给
jti加唯一索引,避免重复插入;同时加created_at字段,方便按时间范围清理
双 Token + 黑名单看着配置项不多,但每个环节的参数含义和触发时机稍有偏差,就会出现“登出无效”“刷新失败但无报错”“黑名单查不到”这类静默故障。最常被跳过的其实是 jti 的生成一致性——确保 create_access_token 和 create_refresh_token 都启用了 additional_claims 里的 jti,否则黑名单根本对不上号。










