深入解析 PHP JWT 完整实现:从生成到安全退出
一、JWT是什么?为什么要用JWT?

在前后端分离、分布式系统架构下,传统的Session-Cookie认证方式面临诸多问题:Session依赖服务器存储,分布式部署时需要做Session共享;Cookie存在跨域限制;移动端(APP/小程序)对Cookie支持不友好。
JWT(JSON Web Token)是一种轻量级的身份认证协议,它将用户身份信息加密为JSON格式的字符串,通过Token的方式在客户端和服务端之间传递,核心优势如下:
- 无状态:服务端无需存储Token,仅通过密钥验证签名即可完成认证,适配分布式部署;
- 跨域友好:Token可通过请求头/参数传递,天然支持跨域接口调用;
- 多端兼容:适配Web、APP、小程序等所有客户端类型;
- 可扩展:Token中可自定义存储用户基础信息(如ID、角色),减少数据库查询。
JWT的结构由三部分组成,以.分隔:
- Header(头部):声明Token类型和加密算法(如HS256);
- Payload(载荷):存储用户信息、过期时间、签发者等核心数据;
- Signature(签名):通过Header指定的算法+密钥对Header和Payload加密,防止Token被篡改。
示例JWT Token:
1
| eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ3ZWJtYW4udGlueXdhbi5jbiIsImF1ZCI6IndlYm1hbi50aW55d2FuLmNuIiwiaWF0IjoxNzY3NDkwMzk4LCJuYmYiOjE3Njc0OTAzOTgsImV4cCI6MTc2NzQ5NzU5OCwiZXh0ZW5kIjp7ImlkIjoxLCJjbGllbnQiOiJNT0JJTEUifX0.Zr0hwMXiz7eHaonTViHxZ-2CZ3YN1gCKuNKj0kw7pRE
|
二、PHP实现JWT的环境准备
2.1 依赖安装
PHP官方没有内置JWT处理功能,推荐使用业界成熟的firebase/php-jwt库(目前最主流的PHP JWT实现),通过Composer安装:
1 2 3 4
| composer require firebase/php-jwt
composer require firebase/php-jwt:5.5.1
|
2.2 核心配置
创建jwt.config.php配置文件,统一管理JWT相关参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <?php
return [ 'secret_key' => 'your-32-bit-random-secret-key-12345678', 'algorithm' => 'HS256', 'issuer' => 'webman.tinywan.cn', 'audience' => 'webman.tinywan.cn', 'access_token_expire' => 7200, 'refresh_token_expire' => 604800, 'redis' => [ 'host' => '127.0.0.1', 'port' => 6379, 'password' => '', 'database' => 0, ] ];
|
三、JWT Token的生成(签发)
3.1 核心生成逻辑
创建JwtService.php服务类,封装Token生成方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| <?php
require_once 'vendor/autoload.php'; require_once 'jwt.config.php';
use Firebase\JWT\JWT;
class JwtService { private $config;
public function __construct() { $this->config = include 'jwt.config.php'; }
public function generateTokens(array $userData): array { $now = time(); $issuer = $this->config['issuer']; $audience = $this->config['audience'];
$basePayload = [ 'iss' => $issuer, 'aud' => $audience, 'iat' => $now, 'nbf' => $now, 'extend' => $userData ];
$accessTokenPayload = $basePayload; $accessTokenPayload['exp'] = $now + $this->config['access_token_expire']; $accessToken = JWT::encode( $accessTokenPayload, $this->config['secret_key'], $this->config['algorithm'] );
$refreshTokenPayload = $basePayload; $refreshTokenPayload['exp'] = $now + $this->config['refresh_token_expire']; $refreshToken = JWT::encode( $refreshTokenPayload, $this->config['secret_key'], $this->config['algorithm'] );
return [ 'token_type' => 'Bearer', 'expires_in' => $this->config['access_token_expire'], 'access_token' => $accessToken, 'refresh_token' => $refreshToken ]; } }
|
3.2 生成Token示例
创建generate_token.php测试文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?php
require_once 'JwtService.php';
$userData = [ 'id' => 1, 'username' => 'admin', 'role' => 'super_admin', 'client' => 'MOBILE' ];
$jwtService = new JwtService(); $tokens = $jwtService->generateTokens($userData);
header('Content-Type: application/json; charset=utf-8'); echo json_encode($tokens, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
执行后输出示例:
1 2 3 4 5 6
| { "token_type": "Bearer", "expires_in": 7200, "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ3ZWJtYW4udGlueXdhbi5jbiIsImF1ZCI6IndlYm1hbi50aW55d2FuLmNuIiwiaWF0IjoxNzY3NTAwMDAwLCJuYmYiOjE3Njc1MDAwMDAsImV4cCI6MTc2NzUwNzIwMCwiZXh0ZW5kIjp7ImlkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6InN1cGVyX2FkbWluIiwiY2xpZW50IjoiTU9CSUxFIn19.8Z8Z7X7Y6W5V4U3T2S1R0Q9P8O7N6M5L4K3J2I1H0G9F8E7D6C5B4A3S2D1F0G9H8J7K6L5M4N3B2V1C0X9S8A7Q6W5E4R3T2Y1U0I9O8P7L6K5J4H3G2F1D0S9A8Q7W6E5R4T3Y2U1I0O9P8L7K6J5H4G3F2D1S0A9Q8W7E6R5T4Y3U2I1O0P9L8K7J6H5G4F3D2S1A0Q9W8E7R6T5Y4U3I2O1P0L9K8J7H6G5F4D3S2A1Q0W9E8R7T6Y5U4I3O2P1L0K9J8H7G6F5D4S3A2Q1W0E9R8T7Y6U5I4O3P2L1K0J9H8G7F6D5S4A3Q2W1E0R9T8Y7U6I5O4P3L2K1J0H9G8F7D6S5A4Q3W2E1R0T9Y8U7I6O5P4L3K2J1H0G9F8D7S6A5Q4W3E2R1T0Y9U8I7O6P5L4K3J2H1G0F9D8S7A6Q5W4E3R2T1Y0U9I8O7P6L5K4J3H2G1F0D9S8A7Q6W5E4R3T2Y1U0I9O8P7L6K5J4H3G2F1D0", "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ3ZWJtYW4udGlueXdhbi5jbiIsImF1ZCI6IndlYm1hbi50aW55d2FuLmNuIiwiaWF0IjoxNzY3NTAwMDAwLCJuYmYiOjE3Njc1MDAwMDAsImV4cCI6MTc2ODEwNDgwMCwiZXh0ZW5kIjp7ImlkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6InN1cGVyX2FkbWluIiwiY2xpZW50IjoiTU9CSUxFIn19.9Z9Y8X7W6V5U4T3S2R1Q0P9O8N7M6L5K4J3I2H1G0F9E8D7C6B5A4S3D2F1G0H9J8K7L6M5N4B3V2C1X0S9A8Q7W6E5R4T3Y2U1I0O9P8L7K6J5H4G3F2D1S0A9Q8W7E6R5T4Y3U2I1O0P9L8K7J6H5G4F3D2S1A0Q9W8E7R6T5Y4U3I2O1P0L9K8J7H6G5F4D3S2A1Q0W9E8R7T6Y5U4I3O2P1L0K9J8H7G6F5D4S3A2Q1W0E9R8T7Y6U5I4O3P2L1K0J9H8G7F6D5S4A3Q2W1E0R9T8Y7U6I5O4P3L2K1J0H9G8F7D6S5A4Q3W2E1R0T9Y8U7I6O5P4L3K2J1H0G9F8D7S6A5Q4W3E2R1T0Y9U8I7O6P5L4K3J2H1G0F9D8S7A6Q5W4E3R2T1Y0U9I8O7P6L5K4J3H2G1F0D9S8A7Q6W5E4R3T2Y1U0I9O8P7L6K5J4H3G2F1D0" }
|
四、JWT Token的验证
4.1 封装验证逻辑
在JwtService.php中新增验证方法,同时集成Redis黑名单校验:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
| <?php
use Firebase\JWT\Key; use Firebase\JWT\ExpiredException; use DomainException; use InvalidArgumentException; use UnexpectedValueException; use Redis;
class JwtService {
private function getRedis(): Redis { $redis = new Redis(); $redis->connect( $this->config['redis']['host'], $this->config['redis']['port'] ); if (!empty($this->config['redis']['password'])) { $redis->auth($this->config['redis']['password']); } $redis->select($this->config['redis']['database']); return $redis; }
private function is_valid_json(string $string): bool { if (!is_string($string) || empty($string)) { return false; } json_decode($string); return json_last_error() === JSON_ERROR_NONE; }
public function verifyToken(?string $token): array { if (empty($token) || !is_string($token)) { return [ 'code' => 401, 'msg' => 'Token不能为空且必须为字符串', 'data' => [] ]; }
$redis = $this->getRedis(); $blacklistKey = 'jwt_blacklist:' . md5($token); if ($redis->exists($blacklistKey)) { $redis->close(); return [ 'code' => 401, 'msg' => 'Token已失效(已退出登录)', 'data' => [] ]; }
try { $decoded = JWT::decode( $token, new Key($this->config['secret_key'], $this->config['algorithm']) );
if ($decoded->iss !== $this->config['issuer']) { $redis->close(); return [ 'code' => 403, 'msg' => 'Token签发者非法', 'data' => [] ]; } if ($decoded->aud !== $this->config['audience']) { $redis->close(); return [ 'code' => 403, 'msg' => 'Token受众非法', 'data' => [] ]; }
$userData = (array)$decoded->extend; $redis->close();
return [ 'code' => 200, 'msg' => 'Token验证成功', 'data' => $userData ];
} catch (ExpiredException $e) { $redis->close(); return [ 'code' => 401, 'msg' => 'Token已过期,请刷新Token', 'data' => [] ]; } catch (InvalidArgumentException $e) { $redis->close(); return [ 'code' => 400, 'msg' => 'Token格式无效:' . $e->getMessage(), 'data' => [] ]; } catch (DomainException $e) { $redis->close(); return [ 'code' => 500, 'msg' => 'JWT配置错误:' . $e->getMessage(), 'data' => [] ]; } catch (UnexpectedValueException $e) { $redis->close(); return [ 'code' => 401, 'msg' => 'Token验证失败:' . $e->getMessage(), 'data' => [] ]; } catch (Exception $e) { $redis->close(); return [ 'code' => 500, 'msg' => 'Token验证异常:' . $e->getMessage(), 'data' => [] ]; } }
public function extractTokenFromRequest(): string { $headers = getallheaders(); $authHeader = $headers['Authorization'] ?? ''; if (!empty($authHeader) && strpos($authHeader, 'Bearer ') === 0) { return substr($authHeader, 7); }
if (!empty($_POST['access_token']) && $this->is_valid_json($_POST['access_token']) === false) { return trim($_POST['access_token']); }
if (!empty($_GET['access_token']) && $this->is_valid_json($_GET['access_token']) === false) { return trim($_GET['access_token']); }
return ''; } }
|
4.2 验证Token示例(用户信息接口)
创建user_info.php接口文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <?php
header('Content-Type: application/json; charset=utf-8'); require_once 'JwtService.php';
$jwtService = new JwtService();
$token = $jwtService->extractTokenFromRequest();
$verifyResult = $jwtService->verifyToken($token);
if ($verifyResult['code'] !== 200) { http_response_code($verifyResult['code']); echo json_encode($verifyResult, JSON_UNESCAPED_UNICODE); exit; }
$userData = $verifyResult['data']; $response = [ 'code' => 200, 'msg' => '获取用户信息成功', 'data' => [ 'user_id' => $userData['id'], 'username' => $userData['username'], 'role' => $userData['role'], 'client_type' => $userData['client'], 'token_expire_tips' => 'Access Token将在' . $jwtService->config['access_token_expire'] . '秒后过期' ] ];
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
4.3 测试验证接口
使用CURL测试(替换为实际生成的Token):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| curl -X GET \ http://localhost/user_info.php \ -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ3ZWJtYW4udGlueXdhbi5jbiIsImF1ZCI6IndlYm1hbi50aW55d2FuLmNuIiwiaWF0IjoxNzY3NTAwMDAwLCJuYmYiOjE3Njc1MDAwMDAsImV4cCI6MTc2NzUwNzIwMCwiZXh0ZW5kIjp7ImlkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6InN1cGVyX2FkbWluIiwiY2xpZW50IjoiTU9CSUxFIn19.8Z8Z7X7Y6W5V4U3T2S1R0Q9P8O7N6M5L4K3J2I1H0G9F8E7D6C5B4A3S2D1F0G9H8J7K6L5M4N3B2V1C0X9S8A7Q6W5E4R3T2Y1U0I9O8P7L6K5J4H3G2F1D0S9A8Q7W6E5R4T3Y2U1I0O9P8L7K6J5H4G3F2D1S0A9Q8W7E6R5T4Y3U2I1O0P9L8K7J6H5G4F3D2S1A0Q9W8E7R6T5Y4U3I2O1P0L9K8J7H6G5F4D3S2A1Q0W9E8R7T6Y5U4I3O2P1L0K9J8H7G6F5D4S3A2Q1W0E9R8T7Y6U5I4O3P2L1K0J9H8G7F6D5S4A3Q2W1E0R9T8Y7U6I5O4P3L2K1J0H9G8F7D6S5A4Q3W2E1R0T9Y8U7I6O5P4L3K2J1H0G9F8D7S6A5Q4W3E2R1T0Y9U8I7O6P5L4K3J2H1G0F9D8S7A6Q5W4E3R2T1Y0U9I8O7P6L5K4J3H2G1F0D9S8A7Q6W5E4R3T2Y1U0I9O8P7L6K5J4H3G2F1D0'
curl -X GET \ http://localhost/user_info.php \ -H 'Authorization: Bearer 过期的Token字符串'
curl -X GET \ http://localhost/user_info.php \ -H 'Authorization: Bearer 已退出的Token字符串'
|
正确Token返回示例:
1 2 3 4 5 6 7 8 9 10 11
| { "code": 200, "msg": "获取用户信息成功", "data": { "user_id": 1, "username": "admin", "role": "super_admin", "client_type": "MOBILE", "token_expire_tips": "Access Token将在7200秒后过期" } }
|
五、Token刷新与安全退出
5.1 Token刷新接口
创建refresh_token.php,实现用Refresh Token获取新的Access Token:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <?php
header('Content-Type: application/json; charset=utf-8'); require_once 'JwtService.php';
$jwtService = new JwtService();
$refreshToken = ''; if (!empty($_POST['refresh_token'])) { $refreshToken = trim($_POST['refresh_token']); } else { $headers = getallheaders(); $refreshTokenHeader = $headers['Refresh-Token'] ?? ''; if (!empty($refreshTokenHeader)) { $refreshToken = $refreshTokenHeader; } }
$verifyResult = $jwtService->verifyToken($refreshToken); if ($verifyResult['code'] !== 200) { echo json_encode($verifyResult, JSON_UNESCAPED_UNICODE); exit; }
$userData = $verifyResult['data']; $newTokens = $jwtService->generateTokens($userData);
$response = [ 'code' => 200, 'msg' => 'Token刷新成功', 'data' => [ 'token_type' => $newTokens['token_type'], 'expires_in' => $newTokens['expires_in'], 'access_token' => $newTokens['access_token'] ] ];
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
5.2 安全退出接口
创建logout.php,实现Token加入黑名单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| <?php
header('Content-Type: application/json; charset=utf-8'); require_once 'JwtService.php';
$jwtService = new JwtService();
$accessToken = $jwtService->extractTokenFromRequest(); $refreshToken = $_POST['refresh_token'] ?? '';
if (empty($accessToken)) { echo json_encode([ 'code' => 400, 'msg' => 'Access Token不能为空', 'data' => [] ], JSON_UNESCAPED_UNICODE); exit; }
$redis = $jwtService->getRedis();
try { $config = include 'jwt.config.php'; $decoded = JWT::decode( $accessToken, new Key($config['secret_key'], $config['algorithm']) ); $expireTime = $decoded->exp - time(); if ($expireTime <= 0) { $expireTime = 3600; }
$accessTokenBlackKey = 'jwt_blacklist:' . md5($accessToken); $redis->setex($accessTokenBlackKey, $expireTime, 1);
if (!empty($refreshToken)) { $refreshTokenDecoded = JWT::decode( $refreshToken, new Key($config['secret_key'], $config['algorithm']) ); $refreshExpireTime = $refreshTokenDecoded->exp - time(); if ($refreshExpireTime <= 0) { $refreshExpireTime = 86400; } $refreshTokenBlackKey = 'jwt_blacklist:' . md5($refreshToken); $redis->setex($refreshTokenBlackKey, $refreshExpireTime, 1); }
$redis->close();
echo json_encode([ 'code' => 200, 'msg' => '退出登录成功,Token已失效', 'data' => [] ], JSON_UNESCAPED_UNICODE);
} catch (Exception $e) { $redis->close(); echo json_encode([ 'code' => 500, 'msg' => '退出登录失败:' . $e->getMessage(), 'data' => [] ], JSON_UNESCAPED_UNICODE); }
|
测试退出接口:
1 2 3 4
| curl -X POST \ http://localhost/logout.php \ -H 'Authorization: Bearer AccessToken字符串' \ -d 'refresh_token=RefreshToken字符串'
|
返回示例:
1 2 3 4 5
| { "code": 200, "msg": "退出登录成功,Token已失效", "data": [] }
|
六、JWT使用的安全最佳实践
6.1 核心安全原则
- 密钥安全:
- 密钥必须足够长(建议32位以上随机字符串),避免使用简单字符串;
- 密钥不要硬编码在代码中,建议通过环境变量/配置文件加载;
- 不同环境(开发/测试/生产)使用不同的密钥。
- Token有效期:
- Access Token有效期不宜过长(建议30分钟-2小时),降低泄露风险;
- Refresh Token有效期可适当延长(7-30天),但需严格限制使用场景(仅用于刷新Token)。
- 传输安全:
- 所有接口必须使用HTTPS协议,防止Token被中间人劫持;
- 禁止将Token放在URL参数中传递(会记录在日志、浏览器历史中);
- 优先使用
Authorization: Bearer {Token}请求头传递。
- 黑名单机制:
- 必须实现Token黑名单,解决JWT无状态导致的“退出后Token仍有效”问题;
- 推荐使用Redis存储黑名单,利用其过期机制自动清理无效数据。
6.2 常见问题解决方案
- Token被盗用:
- 增加设备绑定(在Payload中存储设备ID/IP),验证时校验设备信息;
- 实现Token高频访问监控,异常访问自动拉黑。
- 用户信息变更:
- 用户修改密码/权限后,立即拉黑该用户所有Token;
- 可在Redis中存储
user_blacklist:{user_id},验证时先检查用户是否被拉黑。
- 性能优化:
- Redis黑名单Key使用
md5(token)缩短长度,提升存储和查询效率;
- 对验证逻辑增加缓存,避免重复解析Token。
七、总结
本文完整讲解了PHP中JWT的落地实现,从基础概念、环境准备,到Token生成、验证、刷新、退出的全流程代码实现,核心要点如下:
- JWT是无状态认证方案,适合分布式、跨域、多端场景,核心由Header、Payload、Signature三部分组成;
- PHP中推荐使用
firebase/php-jwt库处理JWT,避免手动解析带来的安全风险;
- 纯JWT存在“退出后Token仍有效”的问题,需通过Redis黑名单机制解决;
- 生产环境中需遵循安全最佳实践:短有效期、HTTPS传输、密钥保密、设备绑定等。
通过本文的代码案例,你可以快速在PHP项目中实现完整的JWT认证体系,兼顾安全性和易用性,适配前后端分离、分布式部署等主流架构场景。