thinkphp 8.0 实现 jwt 无状态鉴权需严格遵循“不存 session、不查库验身份、每次请求独立验证”原则,同时必须补足黑名单(redis 存储 jti)、iss/aud 发行方与受众校验、认证与鉴权分离(authcheck 与 permissioncheck 中间件解耦)等关键环节,否则存在登出延迟、跨模块越权、中间件绕过等安全风险。

ThinkPHP 8.0 实现 JWT 无状态鉴权,核心在于“不存 session、不查库验身份、每次请求独立验证”,但必须补足黑名单、发行方校验、权限解耦等关键环节,否则看似无状态,实则存在登出失效延迟、跨模块越权、中间件绕过等风险。
Token 生成要带必要字段和安全约束
生成 JWT 时不能只塞 uid 和 exp。payload 至少应包含:
-
iss(Issuer):固定为你的 API 域名(如
https://api.example.com),用于防止 token 被其他系统复用; -
aud(Audience):按业务区分,比如
user或admin,鉴权中间件需比对当前请求所属模块; -
jti(JWT ID):服务端生成唯一值(如
uniqid(true)),作为后续黑名单 key; - exp:合理设置(如 2 小时),避免过长导致风险窗口扩大;
- 不存敏感信息:密码、手机号、身份证号等禁止写入 payload。
验证流程必须前置黑名单与标准字段校验
TP8 的中间件中,JWT 解析不能只调 JWT::decode()。正确顺序是:
- 从
Authorization: Bearer xxx提取 token; - 先用
JWT::getHeader($token)检查alg是否为HS256(禁用none算法); - 再用
JWT::decode($token, new Key($secret, 'HS256'))解析; - 立即检查
iss和aud是否匹配当前配置; - 查询 Redis 中
jwt:blacklist:{jti}是否存在,存在则直接返回401; - 最后才将用户 ID 和角色信息写入
think\facade\Request::with('auth', [...])供后续使用。
黑名单必须用 Redis 主动管理
TP 默认缓存驱动(如 file 或 apcu)不支持原子操作,高并发下黑名单可能失效。生产环境必须:
- 使用 Redis 存储黑名单,key 格式统一为
jwt:blacklist:{jti}; - 设置 TTL 与 token 过期时间完全一致(如
7200秒); - 登出接口或密码修改后,主动执行
Redis::setex('jwt:blacklist:'.$jti, $ttl, 1); - 避免用
think\facade\Cache替代 Redis,尤其在集群部署时。
鉴权逻辑必须与认证分离
TP8 的 Auth 中间件只做身份认证(Authentication),不是权限控制(Authorization)。真正的鉴权要单独写中间件:
- 在
app/middleware/AuthCheck.php中完成 token 解析与黑名单校验; - 另建
app/middleware/PermissionCheck.php,根据路由或注解读取所需权限(如@permission('user:edit')); - 权限数据建议从 Redis 缓存加载(如
user:perms:{uid}),而非每次查数据库; - 控制器里禁止写
if ($user->role !== 'admin') { ... },所有判断统一收口到 PermissionCheck。











