thinkphp 8.0 jwt鉴权需构建认证、续期、黑名单、透传、权限分离的五层稳定链路:认证中间件仅核验token并注入用户信息;无感刷新通过redis原子操作实现滑动窗口续期;黑名单基于jti实现登出即时失效;权限中间件依赖路由名与用户上下文动态校验。

ThinkPHP 8.0 做 JWT 鉴权,核心不是“写个中间件解析 token 就完事”,而是把认证、续期、黑名单、透传、权限分离这五件事串成一条稳定链路。中间件是入口,但逻辑必须分层,否则容易出现 token 过期不续、登出仍可用、前端换 token 失败、权限校验被绕过等问题。
认证中间件:只做身份核验,不查库不跳转
新建 app/middleware/AuthToken.php,在 handle() 中完成以下动作:
- 从
$request->header('authorization')安全提取 Bearer token,用正则/^Bearer\s+(?<token>[^\s]+)$/i</token>防空格异常 - 调用
JWT::decode($token, new Key($secret, 'HS256'), ['HS256']),显式传算法数组,避免 none 算法漏洞 - 解码前检查 header 中
alg是否为HS256;解码后强制校验iss(如https://api.example.com)和aud(如web),防跨域复用 - 验证通过后,把
$payload->uid、$payload->role等必要字段注入请求:$request->withAttribute('auth', (array)$payload) - 不要抛
$this->error(),统一 throwHttpException(401),交由全局异常处理器返回标准 JSON
无感刷新:后端驱动,滑动窗口 + Redis 原子操作
续期不是前端定时换 token,而是在每次合法请求中自动触发:
- 认证通过后,立即读取 payload 中的
exp,若$exp - time() (即剩余不到 10 分钟),就生成新 token - 新 payload 复用原
uid、role、jti,重设iat = time()、exp = time() + 7200(2 小时) - 用
Redis::setex('token:' . sha256($oldToken), 7500, $newToken)缓存新 token(比 exp 多 5 分钟缓冲) - 立刻执行
Redis::del('token:' . sha256($oldToken)),并把旧jti加入黑名单:Redis::setex('jwt:blacklist:' . $payload->jti, $remaining + 300, 1) - 通过
$response->withHeader('X-Auth-Token', $newToken)透传,不污染响应体
黑名单机制:登出/改密即时失效的唯一可靠方式
JWT 默认无状态,仅靠 exp 无法满足登出、踢人等业务需求:
- 签发 token 时,必须生成服务端
jti = uniqid(true),不能由客户端提供 - 每次 decode 前,先查
Redis::exists('jwt:blacklist:' . $payload->jti),命中直接 throw 401 - 黑名单 key 的 TTL 设为与 token 剩余时间一致(或加 300 秒容错),避免 Redis 内存无限增长
- 禁用第三方扩展自带的黑名单——tp-jwt 等包常默认关闭或实现有竞态,手写更可控
权限中间件:认证之后再鉴权,路由名驱动规则匹配
权限控制必须独立于认证中间件,且依赖其注入的用户上下文:
- 在 app/middleware.php 中确保
AuthToken::class排在CheckPermission::class之前 - 权限中间件里用
$request->getAttribute('auth')['uid']拿用户,用$request->rule()->getName()拿路由名(如user.edit) - 权限规则存在数据库或配置中,按
user.edit查对应角色或操作码,不硬编码 if/else - 敏感操作(如删除订单)可额外校验
payload中的scope字段,如scope: ['order:delete']
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











