JWT应使用firebase/php-jwt库通过JWT::encode()和JWT::decode()规范生成与验证,避免手动拼接;需严格处理签名、标准声明校验、Authorization头解析、异常分类响应及refresh token的分布式安全存储。

直接上手 JWT 是最可行的路径,别从“自己拼字符串+hash”开始学 —— 那种方式既不安全也不符合主流实践,容易养成坏习惯。
用 firebase/php-jwt 生成和验证 Token 是当前 PHP 的事实标准
这个库封装了 RFC 7519 规范的核心逻辑,避免手动处理 Base64Url 编码、签名拼接、时钟偏差校验等易错环节。安装只需一行:
composer require firebase/php-jwt
关键点在于:必须用 JWT::encode() 和 JWT::decode(),而不是自己 json_encode() + hash_hmac() 拼接。后者无法验证签名完整性,也缺乏对 exp、nbf、iat 等标准声明的自动检查。
-
JWT::encode()第三个参数是算法名,常用'HS256';密钥必须足够长(建议 32 字节以上),且不能硬编码在代码里 -
JWT::decode()返回对象,不是数组;需用try/catch捕获DomainException(签名错误)、InvalidArgumentException(格式错误)、UnexpectedValueException(过期或未生效) - 验证时必须传入
new Key($key, 'HS256'),旧版直接传字符串的方式已被弃用,否则会报Deprecated: Passing a key as a string is deprecated
Authorization 头解析要防空、防格式错、防 token 截断
客户端传来的 Authorization: Bearer xxxxx 并不总是完整可靠。常见问题包括:
立即学习“PHP免费学习笔记(深入)”;
- Header 为空或缺失 ——
getallheaders()在 CLI 或某些 SAPI 下不可用,应改用$_SERVER['HTTP_AUTHORIZATION']或框架提供的统一获取方式 - Bearer 后面没空格,或空格后直接跟换行/截断 —— 建议用正则
/^Bearer\s+(.*)$/i提取,比explode(' ', $header)[1]更健壮 - 前端把 token 存在 localStorage 后,JS 拼错 header(比如漏了
Bearer前缀),导致后端收到的是纯 token 字符串,JWT::decode()会直接抛异常
一个最小可用的提取逻辑示例:
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (preg_match('/^Bearer\s+(.*)$/i', $auth, $matches)) {
$token = $matches[1];
} else {
http_response_code(401);
exit('Invalid Authorization header');
}
Token 过期后不能只返回 401,得区分是过期还是无效
用户感知上,“登录过期”和“token 被篡改”是两回事,但默认异常捕获都走同一个 catch 分支。实际要拆开处理:
- 捕获
Firebase\JWT\ExpiredException→ 返回401+{"error":"token_expired"},前端可触发刷新流程 - 捕获
Firebase\JWT\SignatureInvalidException或DomainException→ 返回401+{"error":"invalid_token"},前端应清空本地存储并跳转登录页 - 没捕获到异常但
$decoded->exp ?说明系统时间不准,或用了低精度时间戳(如毫秒),需检查 <code>time()与 NTP 同步状态
别忽略刷新令牌(refresh token)的存储和校验逻辑
访问令牌(access token)设成 15–60 分钟很合理,但每次过期都让用户重新输密码体验太差。这时需要 refresh token:
- 登录成功时,除返回 access token,还要生成一个长期有效的 refresh token(如 7 天),存入 Redis,key 为
refresh:{user_id},value 可为随机字符串或哈希值 - 刷新接口收到 refresh token 后,先查 Redis 是否存在且未被撤销;存在则签发新 access token,并更新 refresh token 的过期时间(可选)
- 用户登出时,删掉 Redis 中对应的
refresh:{user_id},实现主动失效 —— 这比“等 access token 自然过期”更可控 - 不要把 refresh token 放在 localStorage;它比 access token 更敏感,建议用
HttpOnly+SecureCookie 传输
真正难的不是生成 token,而是让整个生命周期(签发、使用、刷新、吊销)在分布式环境下保持一致性和可观测性。密钥轮换、Redis 故障降级、时钟漂移容忍 —— 这些才是上线后才暴露的问题。



















