ThinkPHP中JWT鉴权需严格遵循三原则:生成token时payload必须含iat/exp/data且密钥≥32字节;JWT::decode须用Key实例并捕获所有Firebase\JWT异常;中间件需正则提取Authorization头、兼容多端来源并校验aud字段。

ThinkPHP 里用 JWT 做鉴权,核心就三件事:生成 token 要带关键字段、验证时必须捕获全部异常、中间件拦截必须区分请求来源。漏掉任意一环,都会导致 401 不明原因返回或跨端登录态混乱。
JWT::encode 时 payload 必须包含 iat/exp/data,且密钥长度不能少于 32 字节
很多项目在 config/app.php 里配的 jwt.key 是短字符串(比如 'abc123'),结果调用 JWT::encode() 时静默失败或签名无效——HS256 算法要求密钥至少 32 字节(256 bit),低于该长度会触发底层 OpenSSL 报错但不抛出异常。
正确做法是:
- 用
bin/console make:jwt-key命令生成(需先注册自定义命令)或直接运行php -r "echo bin2hex(random_bytes(32));"获取 64 位 hex 字符串 - payload 至少保留
iat(签发时间)、exp(过期时间)、data(用户数据),不要删掉iat—— 否则JWT::$leeway失效,服务器时间轻微偏差就会验签失败 -
exp值建议从配置读取,避免硬编码;例如'exp' => time() + config('app.jwt.expire')
JWT::decode 必须用 Key 类实例,且要 catch 所有 Firebase\JWT 异常类型
直接传字符串密钥给 JWT::decode() 在新版 firebase/php-jwt(v6.10+)中已被废弃,会触发 Deprecated 警告并最终报错。更严重的是,只 try-catch Exception 会漏掉 ExpiredException、SignatureInvalidException 等子类,导致 token 过期时返回 500 而非 401。
立即学习“PHP免费学习笔记(深入)”;
验证逻辑应严格按以下结构写:
use Firebase\JWT\Key;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
use Firebase\JWT\BeforeValidException;
try {
$decoded = JWT::decode($token, new Key($key, 'HS256'));
return (array) $decoded->data;
} catch (ExpiredException $e) {
throw new HttpException(401, '登录已过期');
} catch (SignatureInvalidException $e) {
throw new HttpException(401, '签名无效');
} catch (BeforeValidException $e) {
throw new HttpException(401, 'token 尚未生效');
} catch (\Exception $e) {
throw new HttpException(401, 'token 解析失败');
}
Auth 中间件里必须提取 Authorization Header,且要兼容 Bearer 和空格容错
前端传 token 常见写法是 Authorization: Bearer eyJhbGciOi...,但中间件若只做简单 strpos($header, 'Bearer') 判断,遇到 Bearer\t(制表符)或 Bearer (多余空格)就会截断失败。另外,有些小程序客户端会把 token 放在 X-Token 头里,不处理就直接放行。
提取逻辑建议:
- 优先从
request->header('authorization')读取,再 fallback 到request->header('x-token') - 对
Authorization值做正则匹配:/^Bearer\s+(.+)$/i,确保捕获空格后全部内容 - 若 header 为空,再检查 cookie 中的
token字段(适用于 httpOnly 场景),但需确认该 cookie 已设SameSite=Lax且路径正确
多端共用一套 token 时,payload 必须携带 aud 或 client_type 字段
Web、App、小程序共享同一套 JWT 鉴权逻辑,但安全策略不同:Web 需校验 Origin 头,App 要验 app_id,小程序必须限定 aud === 'miniapp'。如果 payload 里没存设备标识,中间件根本无法分流处理。
登录时生成 token 应明确写入:
$payload = [
'iat' => time(),
'exp' => time() + $expire,
'aud' => request()->header('user-agent') ?
(str_contains(request()->header('user-agent'), 'MiniProgram') ? 'miniapp' : 'web') :
'app',
'data' => ['uid' => $user['id']]
];
中间件验证后,立即根据 $decoded->aud 做分支校验,比如小程序端额外检查 request()->header('x-wx-scheme') 是否存在。
真正容易被忽略的是:密钥轮换时旧 token 不会自动失效,必须配合 Redis 存储已注销的 jti(JWT ID)做黑名单;而大多数教程连 jti 字段都没塞进 payload 里。



















