JWT应使用firebase/php-jwt库而非手写,必须设置leeway=60、校验exp/nbf/iss,密钥须正确格式,refresh_token需独立存储并绑定设备且单次有效。

JWT生成要用 firebase/php-jwt 而不是自己拼base64
手写 base64 编码 header/payload + HMAC 签名极易出错:比如没处理 padding、没做 URL-safe 替换(+ → -,/ → _),导致其他语言或前端库无法解析。直接用成熟库最稳。
- 安装:
composer require firebase/php-jwt - 必须设置
JWT::$leeway = 60,否则服务器时间与客户端稍有偏差(常见于手机端)就会报Expired token - 私钥推荐用
file_get_contents('private.key')读 PEM 格式,别硬编码字符串——PEM 中的换行和空格必须原样保留 - 签名算法优先选
HS256(共享密钥)或RS256(非对称),别用none——某些老版本库默认支持它,是严重安全隐患
验证时必须检查 exp、nbf 和 iss 三个字段
只验签名等于裸奔。很多 PHP 开发者只调 JWT::decode() 就完事,结果 token 过期了还放行,或被第三方服务伪造 issuer。
-
exp(过期时间):由JWT::$leeway控制容错秒数,不设会严格校验到毫秒级,容易失败 -
nbf(生效时间):常被忽略,但用于“延后生效”场景(如邮箱验证后才允许登录),不检查会导致提前使用 -
iss(签发方):必须和你自己的服务域名或 ID 严格比对,防止其他 JWT 提供商的 token 混入 - 错误示例:
JWT::decode($token, $key, ['HS256'])—— 没传$options数组,默认不校验exp/nbf,PHP-JWT 旧版甚至不校验iss
JWT::decode() 抛 DomainException 大概率是密钥类型或格式不对
这个异常不是业务逻辑问题,而是底层解密失败。90% 情况下跟密钥有关,而不是 token 本身损坏。
- 用
HS256时,密钥必须是字符串,且长度建议 ≥32 字节;若用短字符串(如'123'),OpenSSL 可能静默失败 - 用
RS256时,公钥必须是 PEM 格式(以-----BEGIN PUBLIC KEY-----开头),不能是 DER 或 JWK;私钥同理,且要确保权限为 600,file_get_contents()才能读取 - 如果 token 是从 HTTP Header 的
Authorization: Bearer xxx中取的,注意前后可能有空格或换行——用trim()再解码 - 调试技巧:把 token 拆成三段,用
base64_decode(strtr($part, '-_', '+/'))手动解 header/payload,看是否 JSON 格式合法
刷新 token 不能只延长 exp,得用独立的 refresh_token 机制
单纯把旧 token 的 exp 改大再重签,等于没刷新——攻击者一旦拿到旧 token,就能无限续期。真正的刷新必须绑定设备、IP 或单次使用。
立即学习“PHP免费学习笔记(深入)”;
- 不要在 access token 里存用户密码、手机号等敏感字段;refresh token 必须存在服务端数据库,关联 user_id + fingerprint(如 UA + IP 前缀)
- 每次刷新成功后,立刻让旧 refresh_token 失效(删库 or 设为 used);access_token 本身保持无状态,别存 DB
- refresh_token 过期时间建议 7–30 天,access_token 控制在 15–60 分钟;两者过期策略必须分离
- PHP 里别用
$_SESSION存 refresh_token——session 默认文件存储,高并发下 I/O 成瓶颈,且无法跨服务器共享











