JWT认证需自建中间件解析Bearer Token,用firebase/php-jwt校验exp/iss/aud等claim,白名单放行登录等接口,refresh token应存Redis并绑定指纹,避免硬编码密钥。

JWT 认证在 ThinkPHP 8 中怎么配 middleware
简单来说,ThinkPHP 8 本身并没有把 JWT 当成默认组件打包进去。这意味着你绕不开一步——自己写一个中间件,用来拦截请求、解析 Authorization 头,并验证 token 是否有效。尤其要注意的是,别指望 think-auth 这类旧版本插件还能直接搬来用,它们大多是为 session 设计的,与 JWT 天生的无状态逻辑完全不兼容。
说干就干,具体操作可以这样走:
- 新建一个中间件类,比如
app/middleware/JwtAuth.php。在它的handle()方法中,用$request->header('authorization')来获取 token。通常客户端会采用Bearer xxx格式,记得要把前缀去掉,只留下 token 本体。 - 引入
firebase/php-jwt库。先通过composer require firebase/php-jwt安装,接着调用JWT::decode($token, $key, ['HS256'])进行解析。需要留个心眼的是,解码过程中极有可能弹出各种异常,比如DomainException或SignatureInvalidException。如果不做捕获处理,直接返回 401,用户体验会很糟糕。 - 验证通过之后,可以把解析出的用户 ID 挂在请求对象上:
$request->withAttr('uid', $payload->uid)。这样后续的控制器就可以用$this->request->attr('uid')拿到当前用户,而不必非要把数据塞到 session 或全局变量里。 - 还有一点容易被忽视:不要在中间件里就去查数据库,加载用户的全部信息。JWT 的 payload 本身应该已经包含了必要字段(比如
uid、role)。真正的数据查询,还是留到业务层按需处理比较好。
Token 签发时哪些 claim 必须设、哪些容易漏
很多新手在签发 token 时常犯的错是:只往 payload 里塞了个 uid 就以为万事大吉。但一旦漏掉关键的 claim,要么校验失败,要么直接捅出安全窟窿。比如说,不设 exp 意味着 token 终身有效,不设 iss 或 aud,别的系统就能检起这个 token 随便用。
以下几点值得重点记下:
- 必设项:
iat(签发时间)、exp(过期时间,比如time() + 3600)、以及业务层面的uid。这里多说一句,uid最好不要用数据库的自增主键直接暴露给前端。 - 建议设项:
iss填你的 API 域名,比如https://api.example.com;aud则标明客户端身份,例如web-app。在解码时,你可以把这两个字段传给JWT::decode()的第三个参数做进一步校验。 - 敏感字段方面,密码、手机号甚至邮箱,绝对不能放进 payload。JWT 本质只是 Base64 编码,不是加密,前端很轻松就能把内容解出来。
- 密钥的存放也是个大问题,千万别在代码里硬编码。最好把它写在
config/app.php或环境变量里,然后用Env::get('JWT_SECRET')来读取,这样运维和不同环境切换都会灵活很多。
ThinkPHP 路由分组怎么跳过 JWT 验证
登录接口、注册接口、健康检查这些敏感点,必须提前放行,否则中间件一挂上去,所有请求直接 401 了。ThinkPHP 的路由分组中间件绑定机制有时会让人产生错觉,以为“分组下所有路由都走同一个中间件”。实际上,可以做到精确排除。
具体操作建议:
- 在
route/app.php文件里,可以用闭包显式定义不需要鉴权的路由组。例如:
Route::group(['middleware' => []], function () { Route::post('login', 'LoginController@login'); }); - 更推荐的做法是在 JWT 中间件内部做白名单判断。比如:
if (in_array($request->url(), ['/api/login', '/api/register'])) { return $next($request); }。这种方式比分散在路由里写更方便统一控制。 - 注意不要试图用类似 Lara vel 的
except()特性来过滤——ThinkPHP 8 的中间件并不原生支持这个参数,硬套的话很容易失效。 - 测试时,记得用
curl -H "Authorization: Bearer invalid-token" http://localhost/api/user来验证拦截是否真的生效,别只盯着成功路径看。
为什么 Token 刷新总失败?关键在 refresh token 的存储和校验逻辑
JWT 本身是不可撤销、也不能直接刷新的。所谓的“刷新”,其实是服务端签发新 token 并作废旧 token 的组合动作。很多开发者卡在“我怎么知道哪个 token 已经被换过”这个问题上,结果刷新接口形同虚设。
实操建议:
- Refresh token 不应该塞进 JWT payload 里。它需要长期保持有效、并且能单独吊销。最稳妥的方案是把它存到 Redis 中,key 设置为
refresh:{uid}:{fingerprint},value 存放对应的 access token 的 jti 及其过期时间。 - 签发新的 access token 时,务必生成新的 jti,并把它写在
JWT::encode()的jti字段里。同时,将 Redis 中旧的refresh:xxx记录删除。 - Access token 校验时,如果发现它的 jti 已经在 Redis 的黑名单(比如一个 set 集合)中,直接拒绝放行——不要等它自然过期。
- 还有一个常被忽略的细节:别把 refresh token 放在 Cookie 里让浏览器自动携带。跨域场景下很容易被拦截,最好用
Authorization: Bearer xxx的形式手动传,与 access token 的行为保持一致。
其实最容易被忽视的是 fingerprint 绑定。如果不记录设备指纹或 UA/IP 信息,攻击者只要拿到 refresh token,就能无限续期。哪怕只是存一个简单的 hash 值,也比完全不绑要安全得多。