jwt需黑名单因无状态特性无法主动作废token,exp仅控最长有效期,登出、强制下线等场景须即时失效;生产环境必须用redis缓存jti实现黑名单校验,且须在decode后、业务前执行,返回401/403但不泄露失效原因。

JWT Token 生成时必须用 think\facade\Cache 存储黑名单或用 Redis 做主动失效
ThinkPHP 默认不提供 JWT 黑名单机制,但实际项目中几乎必然需要支持登出、密码修改后强制 token 失效。直接靠过期时间(exp)不够——用户登出后 token 仍可继续使用,直到自然过期。
推荐做法是:生成 token 时,把 jti(唯一标识)和对应用户 ID 写入 Redis,设置与 token 相同的 TTL;验证时先查 Redis 是否存在该 jti,存在即拒绝访问。
-
jti必须由服务端生成(如uniqid(true)),不能依赖客户端传入 - Redis key 建议格式为
jwt:blacklist:{jti},避免 key 冲突 - 若用
think\facade\Cache,注意默认缓存驱动可能不支持原子性操作,高并发下建议直连 Redis
中间件里解析 token 必须校验 iss、aud 和 alg,不能只验 exp
很多 TP 项目只用 firebase/php-jwt 的 verify 方法校验签名,却忽略 issuer(iss)和 audience(aud)字段,导致 token 被跨系统复用(比如管理后台 token 被用于 APP 接口)。
TP 中间件里做鉴权时,应显式检查:
-
iss必须等于你配置的发行方(如https://api.example.com) -
aud必须匹配当前请求模块(如admin或user) -
alg必须限制为HS256或RS256,禁止none算法(历史上有漏洞) - 解码前先用
JWT::getPayload($token)提取 header,确认alg合法再调用decode
用户权限控制必须绑定到 Auth 中间件之后的独立中间件,而非写在控制器里
TP 的 Auth 中间件只负责身份认证(Authentication),不是鉴权(Authorization)。把角色判断、菜单权限、接口白名单逻辑混进控制器,会导致复用困难、测试难覆盖、且容易绕过。
正确方式是定义一个 CheckPermission 中间件,在 Auth 之后执行,从 token payload 里读取 role 或 permissions 字段做判断:
- payload 中不要放敏感权限列表(如数据库字段名),应映射为预定义的操作码(如
user:edit、order:delete) - 权限规则建议存在数据库或配置文件中,避免硬编码;TP6 可用
config('auth.permissions')加载 - 遇到
403 Forbidden时,返回统一结构(如['code'=>403, 'msg'=>'Insufficient permissions']),不要暴露具体缺失哪条权限
TP6.1+ 的 validateJwt 验证器无法直接复用,需重写 scene 并禁用默认规则
TP 自带的 validateJwt 验证器默认只校验 token 格式是否为三段式,不校验签名、过期、发行方等关键项。直接拿来用等于没验。
实际应新建验证器(如 app\validate\JwtToken.php),并在 scene 中关闭默认规则,手动集成 JWT 解析逻辑:
- 构造函数里注入
JWT实例,避免每次 new - 在
check方法里调用JWT::decode(),捕获ExpiredException、SignatureInvalidException等异常并转为验证失败 - 不要用
rule数组写正则校验 token —— JWT base64url 编码含特殊字符,正则极易误判
复杂点在于:token 有效性、黑名单、权限三者必须串行校验,任一环节失败都应立即中断,且错误响应要区分是“未登录”还是“无权限”,否则前端无法精准提示。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











