本文目录导读:

我来详细讲解如何在PHP中安全地实现JWT(JSON Web Token)。
使用成熟的JWT库
推荐使用经过验证的JWT库,而不是自己实现:
composer require firebase/php-jwt
或者使用更完整的库:
composer require lcobucci/jwt
基础安全的JWT实现示例
使用firebase/php-jwt(简单示例)
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JwtService {
private string $key;
private string $issuer;
private string $audience;
public function __construct() {
// 从环境变量获取密钥,不要硬编码
$this->key = getenv('JWT_SECRET_KEY');
$this->issuer = 'your-app-name';
$this->audience = 'your-audience';
if (empty($this->key) || strlen($this->key) < 32) {
throw new Exception('JWT密钥必须至少32字节');
}
}
public function generateToken(array $userData): string {
$now = time();
$payload = [
'iss' => $this->issuer, // 签发者
'aud' => $this->audience, // 受众
'iat' => $now, // 签发时间
'nbf' => $now, // 生效时间
'exp' => $now + 3600, // 过期时间(1小时)
'jti' => bin2hex(random_bytes(16)), // 唯一标识
'data' => [
'user_id' => $userData['id'],
'role' => $userData['role'] ?? 'user'
]
];
return JWT::encode($payload, $this->key, 'HS256');
}
public function validateToken(string $token): array {
try {
// 设置解码后的验证参数
$decoded = JWT::decode($token, new Key($this->key, 'HS256'));
// 额外验证
if ($decoded->iss !== $this->issuer) {
throw new Exception('无效的签发者');
}
if ($decoded->aud !== $this->audience) {
throw new Exception('无效的受众');
}
if ($decoded->exp < time()) {
throw new Exception('Token已过期');
}
return (array)$decoded->data;
} catch (\Firebase\JWT\ExpiredException $e) {
throw new AuthException('Token已过期', 401);
} catch (\Firebase\JWT\SignatureInvalidException $e) {
throw new AuthException('签名无效', 401);
} catch (\Exception $e) {
throw new AuthException('无效的Token', 401);
}
}
}
使用lcobucci/jwt(更安全更完整)
<?php
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\ValidAt;
use Lcobucci\Clock\SystemClock;
class SecureJwtService {
private Configuration $config;
public function __construct() {
$key = getenv('JWT_SECRET_KEY');
$this->config = Configuration::forSymmetricSigner(
new Sha256(),
InMemory::plainText($key)
);
// 添加验证约束
$this->config->setValidationConstraints(
new SignedWith($this->config->signer(), $this->config->verificationKey()),
new ValidAt(SystemClock::fromUTC())
);
}
public function generateToken(string $userId, string $role): string {
$now = new DateTimeImmutable();
$token = $this->config->builder()
// 设置header
->issuedBy('your-app-server')
->permittedFor('your-frontend-app')
->identifiedBy(bin2hex(random_bytes(16)), true)
->issuedAt($now)
->canOnlyBeUsedAfter($now)
->expiresAt($now->modify('+1 hour'))
// 自定义Claims
->withClaim('uid', $userId)
->withClaim('role', $role)
->getToken($this->config->signer(), $this->config->signingKey());
return $token->toString();
}
public function validateToken(string $tokenString): array {
try {
$token = $this->config->parser()->parse($tokenString);
// 验证Token
$constraints = $this->config->validationConstraints();
$this->config->validator()->assert($token, ...$constraints);
$claims = $token->claims();
return [
'user_id' => $claims->get('uid'),
'role' => $claims->get('role'),
'expires_at' => $claims->get('exp')
];
} catch (\Exception $e) {
throw new AuthException('Token验证失败: ' . $e->getMessage());
}
}
}
安全最佳实践
密钥管理
// .env 文件中存储(不要提交到版本控制)
JWT_SECRET_KEY=${2:your-very-secure-secret-key-at-least-32-chars}
// 密钥生成
// Linux/Mac: openssl rand -base64 32
// PHP: bin2hex(random_bytes(32))
JWT认证中间件示例(用于框架)
class JwtAuthMiddleware {
private JwtService $jwtService;
public function __construct() {
$this->jwtService = new JwtService();
}
public function handle(Request $request): void {
// 1. 从请求头获取Token
$authorization = $request->getHeader('Authorization');
if (preg_match('/Bearer\s+(.*)/', $authorization, $matches)) {
$token = $matches[1];
try {
// 2. 验证Token
$claims = $this->jwtService->validateToken($token);
// 3. 将用户信息存入请求
$request->setAttribute('user_id', $claims['user_id']);
$request->setAttribute('role', $claims['role']);
} catch (AuthException $e) {
// 4. Token无效,返回401
http_response_code(401);
echo json_encode(['error' => $e->getMessage()]);
exit;
}
} else {
http_response_code(401);
echo json_encode(['error' => '缺少Authorization头']);
exit;
}
}
}
高级安全特性
刷新Token机制
class RefreshTokenService {
public function generateRefreshToken(string $userId): string {
return bin2hex(random_bytes(64)); // 生成随机刷新令牌
}
public function refreshAccessToken(string $refreshToken): array {
// 验证刷新令牌(存储在数据库/Redis)
$storedToken = $this->database->getRefreshToken($refreshToken);
if (!$storedToken || $storedToken['expires_at'] < time()) {
throw new AuthException('刷新令牌无效或已过期');
}
// 生成新的访问令牌
$accessToken = $this->jwtService->generateToken(
$storedToken['user_id'],
$storedToken['role']
);
// 轮换刷新令牌(安全实践)
$newRefreshToken = $this->generateRefreshToken($storedToken['user_id']);
return [
'access_token' => $accessToken,
'refresh_token' => $newRefreshToken
];
}
}
Token黑名单机制
class TokenBlackList {
public function addToBlacklist(string $jti, int $expires_at): void {
// 使用Redis存储
Redis::set("blacklist:{$jti}", true, 'EX', $expires_at - time());
}
public function isBlacklisted(string $jti): bool {
return Redis::exists("blacklist:{$jti}");
}
}
重要安全提醒
- 永远不要将JWT存储在localStorage(易受XSS攻击),推荐使用HttpOnly Cookie
- 使用HTTPS传输,防止中间人攻击
- 设置合理的过期时间(访问令牌通常15分钟-1小时)
- 实现令牌轮换和黑名单机制
- 避免在JWT中存储敏感信息(可解密)
- 使用强加密算法(推荐HS256或RS256)
- 验证HS256签名时防止算法混淆攻击
完整的安全配置示例
final class JwtConfig {
public const ALGORITHM = 'HS256';
public const TOKEN_TTL = 3600; // 1小时
public const REFRESH_TTL = 2592000; // 30天
public const MAX_REFRESH_USE = 10; // 刷新令牌最大使用次数
public static function getSecretKey(): string {
$key = getenv('JWT_SECRET_KEY');
if (!$key || strlen($key) < 32) {
throw new RuntimeException('JWT密钥配置错误');
}
return $key;
}
}
通过遵循这些安全实践,你可以构建一个安全可靠的JWT认证系统,记得定期更新密钥,并监控异常使用模式。