正确生成JWT需三步:准备匹配算法的密钥(HS256用字符串密钥)、构造含iat/exp的payload、调用JWT::encode();验证时须确保密钥一致、时区设为UTC、捕获具体异常;存储应使用HTTP-only+Secure+SameSite Cookie;黑名单推荐Redis短时效jti缓存。

PHP 使用 firebase/php-jwt 生成 JWT 令牌的正确姿势
直接用 firebase/php-jwt 生成 JWT,核心就三步:准备密钥、构造 payload、调用 JWT::encode()。但很多人卡在签名算法和密钥类型不匹配上——比如用 RSA 私钥却选了 HS256,结果抛出 DomainException: Algorithm not allowed。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
-
HS256(最常用)必须配字符串密钥,例如'my_secret_key_123';不能传数组或空字符串 - payload 中推荐至少包含
iat(签发时间)、exp(过期时间),exp建议设为time() + 3600这类动态值,别写死 - 不要手动拼接 header 或 base64 编码——
JWT::encode()内部已处理,自己做反而会破坏签名 - 示例代码片段:
$payload = ['user_id' => 123, 'iat' => time(), 'exp' => time() + 3600];<br>$token = \Firebase\JWT\JWT::encode($payload, 'my_secret_key_123', 'HS256');
验证 JWT 时为什么总报 “Invalid token” 或 “Expired token”
验证失败八成不是库的问题,而是时间、密钥或字段校验没对齐。尤其是本地开发环境时区与服务器不一致,exp 和 iat 校验极易失败。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 确保验证时用的密钥和生成时完全一致(注意前后空格、换行符);RSA 场景下,验证必须用公钥,且格式需为 PEM(以
-----BEGIN PUBLIC KEY-----开头) - 调用
JWT::decode()前,先确认date_default_timezone_set('UTC')已设置,避免时区偏差导致exp判定错误 - 捕获
ExpiredException、SignatureInvalidException、BeforeValidException等具体异常,比笼统 catchException更利于定位 - 如果需要跳过某些校验(如临时调试 exp),可传入
['verify_exp' => false]第三个参数,但生产环境禁用
如何安全地把 JWT 存进 HTTP-only Cookie 而不被 XSS 窃取
JWT 本身不是密码,但它等价于会话凭证。放在 localStorage 或普通 cookie 里等于裸奔;HTTP-only + Secure + SameSite=Strict 才是底线配置。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 写入 cookie 用
setcookie(),而非$_COOKIE直接赋值:setcookie('auth_token', $token, [<br> 'expires' => time() + 3600,<br> 'path' => '/',<br> 'domain' => 'example.com',<br> 'secure' => true,<br> 'httponly' => true,<br> 'samesite' => 'Strict'<br>]); - 前端 JS 完全无法读取该 cookie,所以后续请求靠浏览器自动携带即可,后端用
$_COOKIE['auth_token']取值 - 别在 URL 参数或 request body 里重复传 token——既增加泄露面,又可能触发 WAF 规则拦截
- 如果用 Nginx 反向代理,确认
proxy_cookie_path和proxy_cookie_domain配置没覆盖掉原始 cookie 属性
JWT 黑名单/退出登录怎么实现才不违背“无状态”原则
JWT 本质是无状态的,所谓“退出登录”只能靠外部机制弥补。硬删 token 字符串没意义,因为客户端还存着,服务端也没法主动废止。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 最轻量方案:维护一个 Redis 的短时效黑名单(key 是
jwt_jti,value 可为空,TTL 设为和 token 相同),验证前先查是否存在。jti 字段需在生成时显式写入:'jti' => bin2hex(random_bytes(16)) - 避免用 MySQL 存黑名单——单次验证多一次 DB 查询,QPS 上千就吃不消;Redis 查
EXISTS是 O(1) - 如果业务允许,更推荐“换密钥”策略:定期轮换签名密钥,旧 key 签发的 token 自动失效(需配合密钥版本号字段
kid) - 别尝试解析 JWT 后改 payload 再重签——签名密钥一旦暴露,整个机制就崩了
getenv('JWT_SECRET') 加载密钥却没在 .env 里定义,或者忘记在 CLI 脚本中调用 date_default_timezone_set(),都会让 JWT 在某些环节静默失败。



















