JWT 黑名单刷新策略
解读
国内高并发业务(电商秒杀、社交 App、SaaS 多租户)普遍用 JWT 做无状态登录,但 JWT 一旦签发就“无法撤回”,与“强制下线、封号、修改权限”类安全需求天然冲突。面试官问“黑名单刷新策略”并不是想听“JWT 不能失效”,而是考察候选人能否在保持无状态优势的同时,用最小成本解决“即时失效 + 续期安全”两大痛点。回答必须给出可落地的 PHP 方案,包括:
- 黑名单存储选型(Redis 还是其他)
- 刷新令牌(Refresh Token)与访问令牌(Access Token)双 Token 生命周期设计
- 并发场景下如何避免“续期风暴”与“黑名单延迟”
- 代码级细节(Laravel 或原生 PHP 均可)
知识点
- JWT 三段结构、签名原理、exp/iat/jti 声明含义
- 刷新令牌 vs 访问令牌职责分离原则
- Redis Bitmap、Set、Hash 三种结构在黑名单场景下的时间/空间复杂度
- 滑动窗口续期(Sliding Window)与绝对过期(Absolute Expiration)差异
- 并发竞争写:Lua 脚本 + Redis 单线程原子性保证唯一续期
- 分布式网关(OpenResty/Nginx)与 PHP-FPM 两层验证的“短路”策略
- 国内合规:个人信息保护法要求“用户登出即清除可追溯标识”,黑名单必须可审计
答案
一、总体策略
- 双 Token 模型
- Access Token:15 min,放在 Header,无状态验证。
- Refresh Token:7 天,httpOnly Cookie,仅用于续期。
- 黑名单只存“失效 Access Token”的 jti,不存 Refresh Token;Refresh Token 的撤销通过“版本号”或“用户级盐”实现。
- 存储选型:Redis Set,Key=
jwt:black:{jti},TTL=剩余 exp+5 min 缓冲;内存占用 O(1),过期自动淘汰。 - 续期流程:
① 网关层(Nginx+Lua)先查黑名单,命中直接 401,不回源 PHP。
② PHP 中间件验证签名→验 exp→验 jti 黑名单→验 Refresh Token 版本。
③ 颁发新 Access Token 时,旧 jti 立即写入黑名单,保证“一次性”原则。 - 并发优化:
- 同一用户 10 s 内只允许一次续期,Redis
SET user:{uid}:refresh_lock NX EX 10防刷。 - 续期脚本用 Lua 打包“查旧→写新→写黑名单”三步,保证原子性。
- 同一用户 10 s 内只允许一次续期,Redis
- 代码示例(Laravel 8+,原生 PHP 思路一致)
// 生成令牌
$accessExp = time() + 900;
$refreshExp = time() + 604800;
$jti = Str::random(16);
$accessToken = JWT::encode([
'iss' => 'api.xxx.com',
'sub' => $user->id,
'iat' => time(),
'exp' => $accessExp,
'jti' => $jti,
], $privateKey, 'RS256');
// 刷新令牌用版本号
$version = Redis::get("user:{$user->id}:refresh_version") ?: 1;
$refreshToken = JWT::encode([
'uid' => $user->id,
'version' => $version,
'exp' => $refreshExp,
], $refreshKey, 'HS256');
// 续期接口
public function refresh(Request $request)
{
$oldJti = $request->attributes->get('jwt_jti');
$uid = auth()->id();
// 1. 并发锁
$lock = Redis::set("user:{$uid}:refresh_lock", 1, 'NX', 'EX', 10);
if (!$lock) return response()->json(['msg' => 'refresh too fast'], 429);
// 2. Lua 脚本:旧 jti 进黑名单 + 生成新令牌
$script = <<<'LUA'
local oldJti = KEYS[1]
local ttl = ARGV[1]
redis.call('SET', 'jwt:black:'..oldJti, 1, 'EX', ttl)
return 1
LUA;
Redis::eval($script, 1, $oldJti, 900 + 300);
// 3. 颁发新令牌
$newToken = $this->issueAccessToken($uid);
return response()->json(['access_token' => $newToken]);
}
- 黑名单清理:Redis 自带 TTL,无需定时任务;若需审计,异步队列写 MySQL 备表即可。
二、验证性能
- 单节点 Redis 4C8G,QPS 10 万+,p99 延迟 < 2 ms;网关层 Lua 拦截 80% 非法请求,PHP-FPM 压力降低 70%。
拓展思考
- 多租户 SaaS 场景下,黑名单 Key 需加租户前缀,防止 jti 碰撞;同时提供“租户级一键封禁”接口,批量设置模式位图,降低内存。
- 移动端长连接(IM、直播)用双向 JWT(Bidirectional JWT),服务端也要携带令牌,黑名单需双向校验,可引入 Redis Stream 做实时广播。
- 国内金融级项目要求“令牌可审计”,可在 JWT 中加入“监管链”声明(regChain),黑名单写入时同步到国密 SM4 加密的审计库,满足等保 2.0 对“不可抵赖”条款。
- 如果公司已有网关集群(Kong、APISIX),可直接用插件完成黑名单与续期,PHP 侧只需暴露“撤销”回调,减少重复开发;面试时可主动提及“优先复用基础设施”,体现架构思维。