PHP实现JWT(JSON Web Token)认证:签发、校验与Refresh Token机制设计

PHP实现JWT(JSON Web Token)认证:签发、校验与Refresh Token机制设计

大家好!今天我们来深入探讨如何在PHP中实现JWT(JSON Web Token)认证机制,包括签发、校验以及Refresh Token的实现。JWT是一种紧凑、自包含的方式,用于在各方之间安全地传输信息作为JSON对象。它特别适用于API认证,因为它可以减少对服务器数据库的查询次数。

一、JWT的基础概念

在深入代码之前,我们先来了解一下JWT的基本结构和工作原理。JWT主要由三部分组成,并通过点号(.)分隔:

  1. Header (头部):通常包含两个部分:token类型(通常是JWT)和所使用的签名算法(例如HMAC SHA256或RSA)。

    {
      "alg": "HS256",
      "typ": "JWT"
    }
  2. 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
    }
  3. 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;

?>

代码解释:

  1. 引入依赖: 使用require_once 'vendor/autoload.php';引入Composer自动加载器。
  2. JwtHandler类: 创建一个类来封装JWT相关操作。
  3. 构造函数: 在构造函数中设置JWT密钥,最好从环境变量中读取,增加安全性。
  4. generateToken()方法:
    • 设置issuedAt(签发时间)和expire(过期时间)。
    • 构建Payload数组,包含标准声明(iat, iss, nbf, exp)和自定义数据(data)。
    • 使用JWT::encode()方法生成JWT。它接收Payload、密钥和算法作为参数。
  5. 示例用法: 创建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;
}

?>

代码解释:

  1. 引入依赖: 同样引入Composer自动加载器。
  2. validateToken()方法:
    • 使用JWT::decode()方法解码JWT。它接收JWT、密钥和算法作为参数。
    • 如果Token有效,返回解码后的Payload。
    • 如果Token无效(例如,过期或签名不匹配),JWT::decode()会抛出异常。我们使用try...catch块来捕获异常并返回null
  3. 错误处理: 捕获Exception异常,处理Token验证失败的情况,例如Token过期,签名不匹配等。
  4. 示例用法: 创建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;

?>

代码解释:

  1. 引入依赖: 需要安装ramsey/uuid包,用于生成唯一的Refresh Token。
  2. 数据库连接: 添加数据库连接配置。
  3. 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;
}
?>

代码解释:

  1. 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中的所有声明,例如issaudexp等,以确保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。记住,安全是第一位的,一定要遵循安全最佳实践,才能构建可靠的认证系统。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注