PHP实现JWT(JSON Web Token)认证:签发、校验与Refresh Token机制设计
大家好!今天我们来深入探讨如何在PHP中实现JWT(JSON Web Token)认证机制,包括签发、校验以及Refresh Token的实现。JWT是一种紧凑、自包含的方式,用于在各方之间安全地传输信息作为JSON对象。它特别适用于API认证,因为它可以减少对服务器数据库的查询次数。
一、JWT的基础概念
在深入代码之前,我们先来了解一下JWT的基本结构和工作原理。JWT主要由三部分组成,并通过点号(.)分隔:
-
Header (头部):通常包含两个部分:token类型(通常是JWT)和所使用的签名算法(例如HMAC SHA256或RSA)。
{ "alg": "HS256", "typ": "JWT" } -
Payload (载荷):包含声明(claims)。声明是关于实体(通常是用户)和其他数据的声明。有三种类型的声明:
- Registered Claims (注册声明):这是一组预定义的声明,推荐使用,但不是强制性的。例如:
iss(issuer)、sub(subject)、aud(audience)、exp(expiration time)、nbf(not before)、iat(issued at)、jti(JWT ID)。 - Public Claims (公开声明):可以由使用者随意定义,但为了避免冲突,建议使用IANA注册的命名空间。
- Private Claims (私有声明):用于在应用程序之间共享信息。
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 } - Registered Claims (注册声明):这是一组预定义的声明,推荐使用,但不是强制性的。例如:
-
Signature (签名):通过Header中指定的算法,使用Header、Payload和一个密钥(secret)进行签名。这个签名用于验证消息的发送者,并确保消息在此过程中没有被篡改。
签名算法的计算方式如下:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
二、环境准备
在开始编码之前,需要确保PHP环境满足以下条件:
- PHP 7.0或更高版本(推荐使用最新版本)
- OpenSSL扩展(用于加密和签名)
- Composer(用于依赖管理)
使用Composer安装一个流行的JWT库,例如firebase/php-jwt:
composer require firebase/php-jwt
三、JWT签发(生成Token)
下面是如何在PHP中生成JWT的示例代码:
<?php
require_once 'vendor/autoload.php';
use FirebaseJWTJWT;
class JwtHandler {
protected $jwt_secret;
public function __construct() {
// Replace with your actual secret key. Keep this secure!
$this->jwt_secret = getenv('JWT_SECRET') ?: 'your-secret-key'; // 从环境变量中获取,或者使用默认值
}
public function generateToken($payload) {
$issuedAt = time();
$expire = $issuedAt + (60 * 60); // Token expires after 1 hour
$serverName = $_SERVER["SERVER_NAME"];
$data = [
'iat' => $issuedAt, // Issued at: time when the token was generated
'iss' => $serverName, // Issuer
'nbf' => $issuedAt, // Not before
'exp' => $expire, // Expire
'data' => $payload // User data
];
// Important:
// You must specify supported algorithms for your application.
$jwt = JWT::encode(
$data, //Data to be encoded in the JWT
$this->jwt_secret, // The signing key
'HS256' // Algorithm used to sign the token, use HS256
);
return $jwt;
}
}
// 示例用法
$jwtHandler = new JwtHandler();
$user_data = [
'user_id' => 123,
'username' => 'johndoe'
];
$jwt_token = $jwtHandler->generateToken($user_data);
echo "JWT Token: " . $jwt_token . PHP_EOL;
?>
代码解释:
- 引入依赖: 使用
require_once 'vendor/autoload.php';引入Composer自动加载器。 - JwtHandler类: 创建一个类来封装JWT相关操作。
- 构造函数: 在构造函数中设置JWT密钥,最好从环境变量中读取,增加安全性。
generateToken()方法:- 设置
issuedAt(签发时间)和expire(过期时间)。 - 构建Payload数组,包含标准声明(
iat,iss,nbf,exp)和自定义数据(data)。 - 使用
JWT::encode()方法生成JWT。它接收Payload、密钥和算法作为参数。
- 设置
- 示例用法: 创建
JwtHandler实例,定义用户数据,调用generateToken()生成JWT,并输出。
四、JWT校验(验证Token)
验证JWT的有效性至关重要,以确保接收到的Token是真实且未被篡改的。
<?php
require_once 'vendor/autoload.php';
use FirebaseJWTJWT;
use FirebaseJWTKey;
use Exception;
class JwtHandler {
protected $jwt_secret;
public function __construct() {
$this->jwt_secret = getenv('JWT_SECRET') ?: 'your-secret-key';
}
public function validateToken($jwt) {
try {
$decoded = JWT::decode($jwt, new Key($this->jwt_secret, 'HS256'));
return $decoded;
} catch (Exception $e) {
// Handle token validation errors
return null;
}
}
}
// 示例用法
$jwtHandler = new JwtHandler();
$jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2OTc2NzE1MzUsImlzcyI6ImxvY2FsaG9zdCIsIm5iZiI6MTY5NzY3MTUzNSwiZXhwIjoxNjk3NjczMzM1LCJkYXRhIjp7InVzZXJfaWQiOjEyMywidXNlcm5hbWUiOiJqb2huZG9lIn19.kFj_F01l4G2X6jFf1oD9a9V6X7nK8zH0aW9gJ2b2i9k"; // Replace with your actual JWT
$decoded_data = $jwtHandler->validateToken($jwt_token);
if ($decoded_data) {
echo "Token is valid!" . PHP_EOL;
print_r($decoded_data);
} else {
echo "Token is invalid!" . PHP_EOL;
}
?>
代码解释:
- 引入依赖: 同样引入Composer自动加载器。
validateToken()方法:- 使用
JWT::decode()方法解码JWT。它接收JWT、密钥和算法作为参数。 - 如果Token有效,返回解码后的Payload。
- 如果Token无效(例如,过期或签名不匹配),
JWT::decode()会抛出异常。我们使用try...catch块来捕获异常并返回null。
- 使用
- 错误处理: 捕获
Exception异常,处理Token验证失败的情况,例如Token过期,签名不匹配等。 - 示例用法: 创建
JwtHandler实例,提供JWT,调用validateToken()验证Token,并输出结果。
五、Refresh Token机制
JWT的有效期通常较短,以提高安全性。但是,频繁地要求用户重新登录会降低用户体验。Refresh Token机制允许我们在不需要用户重新登录的情况下,获取新的JWT。
数据库结构:
需要创建一个数据库表来存储Refresh Token。
CREATE TABLE refresh_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
refresh_token VARCHAR(255) NOT NULL,
expiry_date DATETIME NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Refresh Token签发:
<?php
require_once 'vendor/autoload.php';
use FirebaseJWTJWT;
use RamseyUuidUuid; // 需要安装 ramsey/uuid 包
class JwtHandler {
protected $jwt_secret;
protected $refresh_token_secret; // 新增 Refresh Token Secret
protected $db; // 数据库连接
public function __construct() {
$this->jwt_secret = getenv('JWT_SECRET') ?: 'your-secret-key';
$this->refresh_token_secret = getenv('REFRESH_TOKEN_SECRET') ?: 'your-refresh-token-secret'; // 从环境变量中获取
$this->db = new PDO('mysql:host=localhost;dbname=your_database', 'your_user', 'your_password'); // 替换为你的数据库连接信息
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
public function generateToken($payload) {
$issuedAt = time();
$expire = $issuedAt + (60 * 60); // JWT expires after 1 hour
$serverName = $_SERVER["SERVER_NAME"];
$data = [
'iat' => $issuedAt, // Issued at: time when the token was generated
'iss' => $serverName, // Issuer
'nbf' => $issuedAt, // Not before
'exp' => $expire, // Expire
'data' => $payload // User data
];
$jwt = JWT::encode(
$data, //Data to be encoded in the JWT
$this->jwt_secret, // The signing key
'HS256' // Algorithm used to sign the token, use HS256
);
return $jwt;
}
public function generateRefreshToken($user_id) {
$refreshToken = Uuid::uuid4()->toString(); // Generate a UUID
$expiryDate = date('Y-m-d H:i:s', time() + (60 * 60 * 24 * 30)); // Expires in 30 days
// Store the refresh token in the database
$stmt = $this->db->prepare("INSERT INTO refresh_tokens (user_id, refresh_token, expiry_date) VALUES (?, ?, ?)");
$stmt->execute([$user_id, $refreshToken, $expiryDate]);
return $refreshToken;
}
}
// 示例用法
$jwtHandler = new JwtHandler();
$user_data = [
'user_id' => 123,
'username' => 'johndoe'
];
$jwt_token = $jwtHandler->generateToken($user_data);
$refresh_token = $jwtHandler->generateRefreshToken($user_data['user_id']);
echo "JWT Token: " . $jwt_token . PHP_EOL;
echo "Refresh Token: " . $refresh_token . PHP_EOL;
?>
代码解释:
- 引入依赖: 需要安装
ramsey/uuid包,用于生成唯一的Refresh Token。 - 数据库连接: 添加数据库连接配置。
generateRefreshToken()方法:- 使用
Uuid::uuid4()生成唯一的Refresh Token。 - 设置Refresh Token的过期时间(例如,30天)。
- 将Refresh Token、用户ID和过期时间存储到数据库中。
- 使用
Refresh Token验证和JWT更新:
<?php
require_once 'vendor/autoload.php';
use FirebaseJWTJWT;
use FirebaseJWTKey;
use Exception;
class JwtHandler {
protected $jwt_secret;
protected $refresh_token_secret; // 新增 Refresh Token Secret
protected $db; // 数据库连接
public function __construct() {
$this->jwt_secret = getenv('JWT_SECRET') ?: 'your-secret-key';
$this->refresh_token_secret = getenv('REFRESH_TOKEN_SECRET') ?: 'your-refresh-token-secret'; // 从环境变量中获取
$this->db = new PDO('mysql:host=localhost;dbname=your_database', 'your_user', 'your_password'); // 替换为你的数据库连接信息
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
public function generateToken($payload) {
$issuedAt = time();
$expire = $issuedAt + (60 * 60); // JWT expires after 1 hour
$serverName = $_SERVER["SERVER_NAME"];
$data = [
'iat' => $issuedAt, // Issued at: time when the token was generated
'iss' => $serverName, // Issuer
'nbf' => $issuedAt, // Not before
'exp' => $expire, // Expire
'data' => $payload // User data
];
$jwt = JWT::encode(
$data, //Data to be encoded in the JWT
$this->jwt_secret, // The signing key
'HS256' // Algorithm used to sign the token, use HS256
);
return $jwt;
}
public function generateRefreshToken($user_id) {
$refreshToken = Uuid::uuid4()->toString(); // Generate a UUID
$expiryDate = date('Y-m-d H:i:s', time() + (60 * 60 * 24 * 30)); // Expires in 30 days
// Store the refresh token in the database
$stmt = $this->db->prepare("INSERT INTO refresh_tokens (user_id, refresh_token, expiry_date) VALUES (?, ?, ?)");
$stmt->execute([$user_id, $refreshToken, $expiryDate]);
return $refreshToken;
}
public function refreshAccessToken($refreshToken) {
// 1. Validate the refresh token in the database
$stmt = $this->db->prepare("SELECT user_id, expiry_date FROM refresh_tokens WHERE refresh_token = ?");
$stmt->execute([$refreshToken]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return ['status' => 'error', 'message' => 'Invalid refresh token.'];
}
$expiryDate = new DateTime($row['expiry_date']);
$now = new DateTime();
if ($expiryDate < $now) {
return ['status' => 'error', 'message' => 'Refresh token has expired.'];
}
$user_id = $row['user_id'];
// 2. Generate a new JWT
$user_data = ['user_id' => $user_id]; // Fetch user data from database if needed
$newJwt = $this->generateToken($user_data);
// 3. Optionally, generate a new refresh token and update the database
$newRefreshToken = $this->generateRefreshToken($user_id);
// 4. Return the new JWT and refresh token
return [
'status' => 'success',
'jwt' => $newJwt,
'refresh_token' => $newRefreshToken
];
}
}
// 示例用法
$jwtHandler = new JwtHandler();
$refresh_token = "your_refresh_token_here"; // Replace with the actual refresh token
$result = $jwtHandler->refreshAccessToken($refresh_token);
if ($result['status'] === 'success') {
echo "New JWT: " . $result['jwt'] . PHP_EOL;
echo "New Refresh Token: " . $result['refresh_token'] . PHP_EOL;
} else {
echo "Error: " . $result['message'] . PHP_EOL;
}
?>
代码解释:
refreshAccessToken()方法:- 从数据库中验证Refresh Token是否存在,并且尚未过期。
- 如果Refresh Token有效,则根据用户ID生成新的JWT。
- (可选)可以生成新的Refresh Token并更新数据库。
- 返回新的JWT和Refresh Token。
Refresh Token撤销:
为了安全起见,需要提供撤销Refresh Token的功能。这通常在用户注销或账号被盗时使用。
<?php
require_once 'vendor/autoload.php';
// ... (previous code) ...
public function revokeRefreshToken($refreshToken) {
// Delete the refresh token from the database
$stmt = $this->db->prepare("DELETE FROM refresh_tokens WHERE refresh_token = ?");
$stmt->execute([$refreshToken]);
return ['status' => 'success', 'message' => 'Refresh token revoked.'];
}
// ... (previous code) ...
// 示例用法
$jwtHandler = new JwtHandler();
$refresh_token = "your_refresh_token_here"; // Replace with the actual refresh token
$result = $jwtHandler->revokeRefreshToken($refresh_token);
echo $result['message'] . PHP_EOL;
?>
代码解释:
revokeRefreshToken()方法:- 从数据库中删除指定的Refresh Token。
六、安全最佳实践
- 使用强密钥: 使用足够长的随机字符串作为JWT密钥。密钥应该存储在安全的地方,例如环境变量或配置文件中,避免硬编码在代码中。
- 定期轮换密钥: 定期更换JWT密钥,以防止密钥泄露造成的风险。
- 设置合理的过期时间: JWT的过期时间不宜过长,建议设置为几分钟到几小时。Refresh Token的过期时间可以相对较长,但也要适度。
- 使用HTTPS: 始终使用HTTPS协议来保护JWT在传输过程中的安全。
- 验证所有声明: 验证JWT中的所有声明,例如
iss、aud、exp等,以确保Token的有效性。 - 防止重放攻击: 可以使用
jti(JWT ID)声明来防止重放攻击。jti是一个唯一的Token标识符,可以存储在数据库中,并在每次验证Token时检查是否已经使用过。 - 存储Refresh Token: 安全地存储Refresh Token,例如使用加密存储在数据库中。
- 实施速率限制: 限制Refresh Token的请求频率,以防止滥用。
- 使用白名单或黑名单: 可以使用白名单或黑名单来控制哪些用户或应用程序可以使用JWT认证。
七、JWT的优点和缺点
优点:
- 无状态: JWT是自包含的,不需要服务器存储会话信息。
- 可扩展性: 易于扩展,适用于分布式系统。
- 跨域认证: 可以在不同的域之间进行认证。
- 性能: 减少了对服务器数据库的查询次数。
缺点:
- Token长度: JWT的长度可能较长,增加了网络传输的负担。
- Token撤销困难: 一旦JWT被签发,就无法撤销,除非等到过期。Refresh Token机制可以缓解这个问题。
- 安全性: 如果密钥泄露,所有使用该密钥签发的JWT都将失效。
八、常见错误和解决方法
| 错误类型 | 描述 | 解决方法 |
|---|---|---|
| Invalid Signature | JWT的签名无效,表示Token被篡改或使用了错误的密钥。 | 确保使用的密钥与签发Token时使用的密钥一致。检查Header中的alg参数是否与使用的签名算法匹配。 |
| Token Expired | JWT已过期。 | 检查Payload中的exp声明,确保当前时间小于过期时间。如果Token已过期,使用Refresh Token获取新的JWT。 |
| Invalid Issuer | JWT的签发者(iss)与预期的签发者不匹配。 |
检查Payload中的iss声明,确保其值与预期的签发者一致。 |
| Invalid Audience | JWT的受众(aud)与预期的受众不匹配。 |
检查Payload中的aud声明,确保其值与预期的受众一致。 |
| Not Before | JWT尚未生效(nbf声明指定Token生效的时间)。 |
检查Payload中的nbf声明,确保当前时间晚于生效时间。 |
| Missing or Invalid Claims | JWT缺少必需的声明或声明的值无效。 | 检查Payload中是否包含所有必需的声明,并确保声明的值符合预期。例如,确保sub声明包含有效的用户ID。 |
| Database Issues | Refresh Token机制依赖于数据库存储,数据库连接问题、查询错误或数据不一致可能导致认证失败。 | 检查数据库连接配置是否正确。确保数据库表结构与代码中的查询语句匹配。使用事务来保证数据的一致性。 |
| Security Vulnerabilities | 如果密钥泄露、Token存储不安全或缺乏适当的验证机制,可能导致安全漏洞,例如身份伪造、重放攻击等。 | 遵循安全最佳实践,例如使用强密钥、定期轮换密钥、使用HTTPS、验证所有声明、防止重放攻击、安全地存储Refresh Token、实施速率限制等。 |
九、总结:
今天我们深入了解了PHP中JWT认证的实现,包括签发、校验和Refresh Token机制。希望通过今天的学习,大家能够更好地理解JWT认证的原理和应用,并在实际项目中安全地使用JWT。记住,安全是第一位的,一定要遵循安全最佳实践,才能构建可靠的认证系统。