JWT解析失败主因是Web服务器未透传Authorization头且TP默认不解析;Firebase验证失败多因算法/公钥不匹配或aud字段错误;设备区分需在payload注入device等字段;API拦截必须返回JSON而非跳转。

JWT token 解析失败:ThinkPHP 中间件里 getBearerToken() 拿不到值
多数人卡在这一步——请求头明明带了 Authorization: Bearer xxx,但中间件里 $request->header('authorization') 返回空或格式不对。ThinkPHP 默认不解析 Authorization 头(尤其 Nginx 下被过滤),且大小写敏感、空格位置容易出错。
- 检查 Web 服务器是否透传:Nginx 需显式加
proxy_set_header Authorization $http_authorization;,Apache 要启用mod_headers并用RequestHeader set Authorization "%{HTTP:Authorization}e" - ThinkPHP 内部读取时统一转小写,所以必须用
$request->header('authorization'),不能写'Authorization'或'bearer' - token 前缀可能带多余空格,建议用
trim($auth) === 'Bearer' ? trim(substr($auth, 7)) : null安全提取,别直接explode(' ', $auth)[1]
Firebase JWT 验证失败:Firebase\JWT\JWT::decode() 报 SignatureInvalidException
不是密钥错了,而是 Firebase JWT 库对算法和 header 字段极其严格——ThinkPHP 默认生成的 token 如果没显式指定 alg 或用了不匹配的签名方式(如 HS256 vs RS256),就会直接拒绝。
- 确认 Firebase SDK 版本 ≥ 6.0,旧版不支持 PS256/ES256 等非对称算法;若用 Firebase Auth 服务端 SDK,必须用
RS256+ 公钥验证,不能用私钥硬编码 - 解码时必须传入完整 header 参数:
JWT::decode($token, $publicKey, ['RS256']),漏掉第三个参数会默认只认HS256 - 公钥格式必须是 PEM(以
-----BEGIN PUBLIC KEY-----开头),不能是 JWK 或 JSON;从 Firebase Console 下载的google-services.json里没有公钥,得去https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com动态获取并缓存
多端用户共用一套 token:如何区分 Web / App / 小程序 的登录态?
JWT payload 里不加设备标识,就无法在中间件里做差异化拦截——比如小程序需要校验 aud 是 miniapp,App 要检查 app_id 字段,而 Web 端依赖 origin 白名单。
- 签发时强制注入字段:
['device' => 'app', 'app_id' => 'com.example.myapp', 'exp' => time() + 3600],别依赖客户端传来的任意值 - 中间件里根据
$payload->device分支处理:App 请求走设备绑定校验,Web 请求检查$request->header('origin')是否在白名单内,小程序额外验证$payload->mp_appid是否匹配已备案 ID - 注意:
aud(audience)字段必须和服务端配置一致,Firebase Auth 默认设为项目 ID,如果自建 JWT 服务,aud必须和 Firebase 控制台里 OAuth 2.0 客户端 ID 完全一致,否则JWT::decode()会因 audience mismatch 报错
中间件拦截后跳转混乱:ThinkPHP 的 halt() 和 redirect() 在 API 场景下失效
API 接口不该返回 HTML 跳转页,但很多人沿用后台逻辑,在认证失败时调 redirect('/login'),结果前端收到 302 + HTML,解析报错。
立即学习“PHP免费学习笔记(深入)”;
- 所有 JWT 中间件必须明确返回 JSON 响应:
return json(['code' => 401, 'msg' => 'Unauthorized'], 401),别用halt()或exit - ThinkPHP 的
Response对象需手动设状态码:response()->code(401)->json([...]),否则默认 200,前端难判断 - 如果项目同时跑 Web 页面和 API,用路由分组隔离:给
api/前缀加中间件,避免和/admin等页面路由混用同一套拦截逻辑
最常被忽略的是 Firebase 公钥的时效性——它每 24 小时轮换一次,硬编码本地文件或只在启动时加载一次,过期后所有新 token 都会验证失败。得做成带 TTL 缓存的动态拉取,且首次失败要触发刷新,不能静默降级。


















