PHP JWT令牌实战指南:从入门到安全部署(附完整代码示例)
目录导读
- 什么是JWT?为什么PHP项目需要它?
- JWT的三大结构:Header、Payload、Signature深度拆解
- 环境准备:PHP 7.4+与Composer依赖安装
- PHP生成JWT令牌:手写HMAC签名与RS256非对称加密
- PHP验证JWT令牌:过期时间、签名校验与异常处理
- 完整实战:登录接口签发Token,API中间件鉴权
- 高频问答:过期刷新、注销黑名单、跨域CORS等10个坑
- 安全底线:防CSRF、密钥管理、算法混淆攻击
什么是JWT?为什么PHP项目需要它?
JWT(JSON Web Token)是一种轻量级的开放标准(RFC 7519),它通过加密签名在客户端与服务器之间安全传输JSON对象,在PHP开发中,JWT主要用于无状态身份认证——服务器无需存储Session,客户端每次请求携带Token即可证明身份,相比传统Session跨域困难、扩展性差的问题,JWT天然支持分布式架构和移动端,因此成为RESTful API安全的首选方案。

JWT的三大结构:Header、Payload、Signature深度拆解
一个标准JWT由三部分组成,用点号()分隔:
xxxxxxxxx.yyyyyyyyy.zzzzzzzzz
- Header(头部):JSON对象,声明类型(
typ: "JWT")和签名算法(alg: "HS256"或RS256),PHP中通过base64UrlEncode(json_encode($header))生成。 - Payload(负载):存放实际数据,包括官方字段(如
iss发行者、exp过期时间、sub用户ID)和自定义字段(如user_role)。注意:Payload仅Base64编码,未加密,严禁放敏感信息如密码。 - Signature(签名):将Header和Payload拼接后,用密钥(HS256对称加密)或私钥(RS256非对称加密)进行哈希运算,防止数据被篡改。这是JWT安全的核心。
环境准备:PHP 7.4+与Composer依赖安装
推荐使用firebase/php-jwt库(GitHub星标超5k),安装命令:
composer require firebase/php-jwt
若需RS256算法,另需安装openssl扩展(PHP默认开启),手动实现亦可,但生产环境务必用成熟库,避免时间戳函数time()与$_SERVER['REQUEST_TIME']的时区问题。
PHP生成JWT令牌:手写HMAC签名与RS256非对称加密
案例1:HS256(对称加密,适用于单一服务端)
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$key = 'your-secret-key-至少32位随机字符串'; // 存于.env,勿硬编码
$payload = [
'iss' => 'https://api.example.com', // 发行者
'aud' => 'https://frontend.com', // 受众
'iat' => time(), // 签发时间
'exp' => time() + 3600, // 1小时后过期
'uid' => 1024, // 用户ID
'role' => 'admin'
];
$jwt = JWT::encode($payload, $key, 'HS256');
echo $jwt; // 输出如 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
案例2:RS256(非对称加密,适合微服务多端验证)
$privateKey = file_get_contents('/path/to/private.pem');
$jwt = JWT::encode($payload, $privateKey, 'RS256');
// 验证端使用公钥
$publicKey = file_get_contents('/path/to/public.pem');
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
PHP验证JWT令牌:过期时间、签名校验与异常处理
验证必须包含三步操作:
- 捕获异常:
ExpiredException(过期)、SignatureInvalidException(签名错误)、BeforeValidException(未到生效时间)。 - 严格校验
aud和iss,防止Token在其他系统被滥用。 - 使用
JWT::decode()后立即转换为数组:
try {
$decoded = JWT::decode($jwt, new Key($secret, 'HS256'));
$data = (array) $decoded; // 转为关联数组
} catch (\Exception $e) {
http_response_code(401);
echo json_encode(['error' => $e->getMessage()]);
exit;
}
完整实战:登录接口签发Token,API中间件鉴权
登录接口(login.php):
// 验证用户名密码(此处省略查询过程)
if ($user && password_verify($pass, $user['password'])) {
$payload = [
'sub' => $user['id'],
'exp' => time() + 7200,
'scope' => 'read write'
];
echo json_encode(['token' => JWT::encode($payload, $secret, 'HS256')]);
}
API中间件(auth_middleware.php):
$headers = apache_request_headers();
if (!isset($headers['Authorization'])) {
http_response_code(401);
exit('Missing token');
}
$token = str_replace('Bearer ', '', $headers['Authorization']);
try {
$data = JWT::decode($token, new Key($secret, 'HS256'));
$GLOBALS['user_id'] = $data->sub; // 存储到全局变量供后续业务使用
} catch (Exception $e) {
http_response_code(403);
exit('Invalid token');
}
高频问答:过期刷新、注销黑名单、跨域CORS等10个坑
-
问:Token过期后如何实现“无感刷新”?
答:签发双Token——短期Access Token(如30分钟)+长期Refresh Token(存数据库),Access Token过期时,用Refresh Token调用/refresh接口换取新Token。Refresh Token需存服务器并设置失效逻辑。 -
问:用户退出登录,JWT还能用吗?
答:JWT本身无法主动失效,常见方案是维护黑名单(Redis Key-Value),将退出用户的jti(JWT ID)存入并设置过期时间,中间件检查黑名单。 -
问:跨域请求时,Token放请求头还是Cookie?
答:SPA(前后端分离)推荐放Authorization Header,避免CSRF攻击,若浏览器插件无法自定义Header,可放Cookie并设置SameSite=None; Secure(需HTTPS)。 -
问:HS256密钥泄露了怎么办?
答:立即在.env中更换密钥,并通知所有用户重新登录,更安全方案是使用RS256,泄露私钥概率低,且公钥可公开。 -
问:
JWT::decode()后为什么返回对象而非数组?
答:库的返回值是stdClass对象,如果想直接使用数组,用(array) $decoded转换,但嵌套对象需递归转换。 -
问:Payload中可以放用户密码MD5吗?
答:绝对严禁! 即使Base64编码,攻击者可轻易解码查看,可放用户ID、角色、权限标识,不敏感信息。 -
问:如何防止Token被重放攻击?
答:在Payload中加入jti(唯一ID),服务端存储已用ID,配合iat时间戳限制,对金融类API建议配合Nonce一次性值。 -
问:Nginx/Apache服务器如何获取Authorization Header?
答:Apache用getallheaders(),Nginx默认隐藏该头,需在配置中加入underscores_in_headers on;或使用$_SERVER['HTTP_AUTHORIZATION']。 -
问:JWT长度会膨胀,影响请求性能吗?
答:单Token通常2KB内,远小于HTTP包体限制,但如果存放大量自定义字段会显著增加体积,建议只存sub和必要的scope。 -
问:PHP-FPM中JWT解密性能如何优化?
答:HS256算法耗CPU极低(微秒级),RS256较重,可用APCu缓存公钥,减少file_get_contentsI/O操作。
安全底线:防CSRF、密钥管理、算法混淆攻击
- 密钥管理:密钥写在
.env文件中,且永远不要提交到Git仓库,定期轮换(如每90天)。 - 算法混淆攻击:攻击者可能篡改Header的
alg为none,务必在JWT::decode指定允许的算法,例如new Key($secret, 'HS256'),禁止动态获取alg字段。 - 防CSRF:若使用Cookie存储Token,必须校验
Origin或Referer头,并设置SameSite=Lax。 - 敏感操作二次验证:修改密码、支付等关键操作,即使Token有效也应要求输入密码或短信验证码。
PHP搭配JWT并非简单调用两个函数,而是需要从生成、验证到安全策略的全链路设计,掌握上述核心思想后,建议阅读firebase/php-jwt源码中JWT::encode和JWT::decode的实现,你会发现时间戳处理、Base64填充符的细节处理正是生产级代码的严谨所在,从今天起,用JWT替换你的Session认证,拥抱无状态API时代。