在 WordPress 中实现刷新令牌(Refresh Token)和访问令牌(Access Token)并存的 JWT 认证系统

您希望在 WordPress 中实现刷新令牌(Refresh Token)和访问令牌(Access Token)并存的 JWT 认证系统。这是一种常见的认证模式,可以提高安全性并改善用户体验。下面我将详细介绍如何在 WordPress 中实现这种机制。

刷新令牌和访问令牌的基本概念

在实现之前,让我们先了解这两种令牌的区别:

  1. 访问令牌(Access Token)
    • 短期有效(通常几分钟到几小时)
    • 用于访问受保护的资源
    • 存储在 HTTP-only cookie 中以防止 XSS 攻击
  2. 刷新令牌(Refresh Token)
    • 长期有效(通常几天到几周)
    • 用于获取新的访问令牌
    • 同样存储在 HTTP-only cookie 中,但有不同的过期时间和路径

实现方案

1. 设置必要的库和常量

首先,我们需要安装 JWT 库并设置必要的常量:

// 在 wp-config.php 中添加  
define('JWT_AUTH_ACCESS_SECRET_KEY', 'your-access-token-secret-key');  
define('JWT_AUTH_REFRESH_SECRET_KEY', 'your-refresh-token-secret-key');  
define('JWT_AUTH_ACCESS_TOKEN_EXPIRATION', 3600); // 1小时  
define('JWT_AUTH_REFRESH_TOKEN_EXPIRATION', 1209600); // 14天

2. 登录时生成两种令牌

当用户登录成功后,我们需要生成两种令牌并设置为 cookie:

add_action('wp_login', 'set_jwt_auth_cookies', 10, 2);  
function set_jwt_auth_cookies($user_login, $user) {  
    require_once __DIR__ . '/vendor/autoload.php';  
    use Firebase\JWT\JWT;  

    // 当前时间  
    $issued_at = time();  

    // 生成访问令牌  
    $access_expiration = $issued_at + JWT_AUTH_ACCESS_TOKEN_EXPIRATION;  
    $access_token_data = array(  
        'iss' => get_site_url(),  
        'iat' => $issued_at,  
        'exp' => $access_expiration,  
        'user' => array(  
            'id' => $user->ID,  
            'email' => $user->user_email,  
            'roles' => $user->roles  
        ),  
        'token_type' => 'access'  
    );  

    $access_token = JWT::encode(  
        $access_token_data,   
        JWT_AUTH_ACCESS_SECRET_KEY,   
        'HS256'  
    );  

    // 生成刷新令牌  
    $refresh_expiration = $issued_at + JWT_AUTH_REFRESH_TOKEN_EXPIRATION;  
    $refresh_token_data = array(  
        'iss' => get_site_url(),  
        'iat' => $issued_at,  
        'exp' => $refresh_expiration,  
        'user_id' => $user->ID,  
        'token_type' => 'refresh'  
    );  

    $refresh_token = JWT::encode(  
        $refresh_token_data,   
        JWT_AUTH_REFRESH_SECRET_KEY,   
        'HS256'  
    );  

    // 设置访问令牌 cookie  
    setcookie(  
        'wp_jwt_access_token',  
        $access_token,  
        [  
            'expires' => $access_expiration,  
            'path' => '/',  
            'domain' => parse_url(get_site_url(), PHP_URL_HOST),  
            'secure' => is_ssl(),  
            'httponly' => true,  
            'samesite' => 'Strict'  
        ]  
    );  

    // 设置刷新令牌 cookie - 注意路径限制为 /token-refresh  
    setcookie(  
        'wp_jwt_refresh_token',  
        $refresh_token,  
        [  
            'expires' => $refresh_expiration,  
            'path' => '/token-refresh', // 限制路径,提高安全性  
            'domain' => parse_url(get_site_url(), PHP_URL_HOST),  
            'secure' => is_ssl(),  
            'httponly' => true,  
            'samesite' => 'Strict'  
        ]  
    );  

    // 可选:在数据库中存储刷新令牌的哈希值,用于验证和撤销  
    $token_hash = wp_hash($refresh_token);  
    update_user_meta($user->ID, 'jwt_refresh_token_hash', $token_hash);  
    update_user_meta($user->ID, 'jwt_refresh_token_expiry', $refresh_expiration);  
}

3. 创建刷新令牌端点

我们需要创建一个专门的端点来处理刷新令牌请求:

add_action('rest_api_init', 'register_token_refresh_route');  
function register_token_refresh_route() {  
    register_rest_route('jwt-auth/v1', '/token-refresh', array(  
        'methods' => 'POST',  
        'callback' => 'refresh_access_token',  
        'permission_callback' => '__return_true'  
    ));  
}  

function refresh_access_token(WP_REST_Request $request) {  
    require_once __DIR__ . '/vendor/autoload.php';  
    use Firebase\JWT\JWT;  
    use Firebase\JWT\Key;  

    // 检查刷新令牌是否存在  
    if (!isset($_COOKIE['wp_jwt_refresh_token'])) {  
        return new WP_Error(  
            'jwt_auth_no_refresh_token',  
            __('刷新令牌不存在'),  
            array('status' => 401)  
        );  
    }  

    $refresh_token = $_COOKIE['wp_jwt_refresh_token'];  

    try {  
        // 解码并验证刷新令牌  
        $decoded = JWT::decode(  
            $refresh_token,   
            new Key(JWT_AUTH_REFRESH_SECRET_KEY, 'HS256')  
        );  

        // 检查令牌类型  
        if (!isset($decoded->token_type) || $decoded->token_type !== 'refresh') {  
            return new WP_Error(  
                'jwt_auth_invalid_token_type',  
                __('无效的令牌类型'),  
                array('status' => 401)  
            );  
        }  

        // 检查令牌是否过期  
        if ($decoded->exp < time()) {  
            return new WP_Error(  
                'jwt_auth_token_expired',  
                __('刷新令牌已过期'),  
                array('status' => 401)  
            );  
        }  

        // 获取用户  
        $user_id = $decoded->user_id;  
        $user = get_user_by('ID', $user_id);  

        if (!$user) {  
            return new WP_Error(  
                'jwt_auth_user_not_found',  
                __('用户不存在'),  
                array('status' => 401)  
            );  
        }  

        // 可选:验证存储的令牌哈希  
        $stored_token_hash = get_user_meta($user_id, 'jwt_refresh_token_hash', true);  
        $current_token_hash = wp_hash($refresh_token);  

        if ($stored_token_hash !== $current_token_hash) {  
            return new WP_Error(  
                'jwt_auth_token_revoked',  
                __('刷新令牌已被撤销'),  
                array('status' => 401)  
            );  
        }  

        // 生成新的访问令牌  
        $issued_at = time();  
        $access_expiration = $issued_at + JWT_AUTH_ACCESS_TOKEN_EXPIRATION;  

        $access_token_data = array(  
            'iss' => get_site_url(),  
            'iat' => $issued_at,  
            'exp' => $access_expiration,  
            'user' => array(  
                'id' => $user->ID,  
                'email' => $user->user_email,  
                'roles' => $user->roles  
            ),  
            'token_type' => 'access'  
        );  

        $access_token = JWT::encode(  
            $access_token_data,   
            JWT_AUTH_ACCESS_SECRET_KEY,   
            'HS256'  
        );  

        // 设置新的访问令牌 cookie  
        setcookie(  
            'wp_jwt_access_token',  
            $access_token,  
            [  
                'expires' => $access_expiration,  
                'path' => '/',  
                'domain' => parse_url(get_site_url(), PHP_URL_HOST),  
                'secure' => is_ssl(),  
                'httponly' => true,  
                'samesite' => 'Strict'  
            ]  
        );  

        // 返回成功响应  
        return array(  
            'success' => true,  
            'message' => __('访问令牌已刷新')  
        );  

    } catch (Exception $e) {  
        return new WP_Error(  
            'jwt_auth_invalid_token',  
            $e->getMessage(),  
            array('status' => 401)  
        );  
    }  
}

4. 验证访问令牌

我们需要创建一个函数来验证访问令牌:

function verify_jwt_access_token() {  
    // 如果用户已经通过 WordPress 标准方式登录,不需要验证 JWT  
    if (is_user_logged_in()) {  
        return true;  
    }  

    // 检查访问令牌是否存在  
    if (!isset($_COOKIE['wp_jwt_access_token'])) {  
        return false;  
    }  

    require_once __DIR__ . '/vendor/autoload.php';  
    use Firebase\JWT\JWT;  
    use Firebase\JWT\Key;  

    $token = $_COOKIE['wp_jwt_access_token'];  

    try {  
        // 解码并验证访问令牌  
        $decoded = JWT::decode(  
            $token,   
            new Key(JWT_AUTH_ACCESS_SECRET_KEY, 'HS256')  
        );  

        // 检查令牌类型  
        if (!isset($decoded->token_type) || $decoded->token_type !== 'access') {  
            return false;  
        }  

        // 检查令牌是否过期  
        if ($decoded->exp < time()) {  
            return false;  
        }  

        // 设置当前用户  
        $user_id = $decoded->user->id;  
        wp_set_current_user($user_id);  

        return true;  
    } catch (Exception $e) {  
        return false;  
    }  
}

5. 与 REST API 集成

我们可以使用 WordPress 的 rest_authentication_errors 过滤器来集成我们的 JWT 认证系统:

add_filter('rest_authentication_errors', 'jwt_authenticate_rest_api', 10, 1);  
function jwt_authenticate_rest_api($errors) {  
    // 如果已经通过其他方式认证,不干扰  
    if ($errors !== null) {  
        return $errors;  
    }  

    // 验证访问令牌  
    if (verify_jwt_access_token()) {  
        return true;  
    }  

    // 访问令牌验证失败,返回错误  
    return new WP_Error(  
        'jwt_auth_failed',  
        __('JWT 认证失败'),  
        array('status' => 401)  
    );  
}

6. 前端实现自动刷新

在前端,我们需要实现一个机制来检测访问令牌是否过期,并在需要时自动刷新:

// 检查访问令牌是否即将过期,如果是,则刷新  
async function checkAndRefreshToken() {  
    try {  
        // 发送请求到受保护的端点  
        const response = await fetch('/wp-json/wp/v2/posts', {  
            credentials: 'same-origin' // 包含 cookies  
        });  

        // 如果返回 401 错误,尝试刷新令牌  
        if (response.status === 401) {  
            await refreshToken();  

            // 重试原始请求  
            return fetch('/wp-json/wp/v2/posts', {  
                credentials: 'same-origin'  
            });  
        }  

        return response;  
    } catch (error) {  
        console.error('Token check failed:', error);  
        throw error;  
    }  
}  

// 刷新访问令牌  
async function refreshToken() {  
    try {  
        const response = await fetch('/wp-json/jwt-auth/v1/token-refresh', {  
            method: 'POST',  
            credentials: 'same-origin' // 包含 cookies  
        });  

        if (!response.ok) {  
            // 如果刷新失败,可能需要重定向到登录页面  
            window.location.href = '/wp-login.php';  
            throw new Error('Token refresh failed');  
        }  

        return await response.json();  
    } catch (error) {  
        console.error('Token refresh failed:', error);  
        throw error;  
    }  
}  

// 使用示例  
async function fetchProtectedData() {  
    try {  
        const response = await checkAndRefreshToken();  
        if (response.ok) {  
            const data = await response.json();  
            console.log('Protected data:', data);  
        }  
    } catch (error) {  
        console.error('Failed to fetch protected data:', error);  
    }  
}

7. 处理登出

当用户登出时,我们需要清除所有 JWT cookie 并撤销刷新令牌:

add_action('wp_logout', 'clear_jwt_auth_cookies');  
function clear_jwt_auth_cookies() {  
    // 获取当前用户  
    $user_id = get_current_user_id();  

    // 清除存储的刷新令牌信息  
    if ($user_id) {  
        delete_user_meta($user_id, 'jwt_refresh_token_hash');  
        delete_user_meta($user_id, 'jwt_refresh_token_expiry');  
    }  

    // 清除访问令牌 cookie  
    setcookie(  
        'wp_jwt_access_token',  
        '',  
        [  
            'expires' => time() - 3600,  
            'path' => '/',  
            'domain' => parse_url(get_site_url(), PHP_URL_HOST),  
            'secure' => is_ssl(),  
            'httponly' => true,  
            'samesite' => 'Strict'  
        ]  
    );  

    // 清除刷新令牌 cookie  
    setcookie(  
        'wp_jwt_refresh_token',  
        '',  
        [  
            'expires' => time() - 3600,  
            'path' => '/token-refresh',  
            'domain' => parse_url(get_site_url(), PHP_URL_HOST),  
            'secure' => is_ssl(),  
            'httponly' => true,  
            'samesite' => 'Strict'  
        ]  
    );  
}

8. 令牌轮换和安全性增强

为了进一步提高安全性,您可以实现令牌轮换机制,即每次刷新访问令牌时也更新刷新令牌:

// 在 refresh_access_token 函数中添加以下代码  
// 生成新的刷新令牌  
$new_refresh_expiration = time() + JWT_AUTH_REFRESH_TOKEN_EXPIRATION;  
$new_refresh_token_data = array(  
    'iss' => get_site_url(),  
    'iat' => time(),  
    'exp' => $new_refresh_expiration,  
    'user_id' => $user_id,  
    'token_type' => 'refresh'  
);  

$new_refresh_token = JWT::encode(  
    $new_refresh_token_data,   
    JWT_AUTH_REFRESH_SECRET_KEY,   
    'HS256'  
);  

// 设置新的刷新令牌 cookie  
setcookie(  
    'wp_jwt_refresh_token',  
    $new_refresh_token,  
    [  
        'expires' => $new_refresh_expiration,  
        'path' => '/token-refresh',  
        'domain' => parse_url(get_site_url(), PHP_URL_HOST),  
        'secure' => is_ssl(),  
        'httponly' => true,  
        'samesite' => 'Strict'  
    ]  
);  

// 更新数据库中的令牌哈希  
$new_token_hash = wp_hash($new_refresh_token);  
update_user_meta($user_id, 'jwt_refresh_token_hash', $new_token_hash);  
update_user_meta($user_id, 'jwt_refresh_token_expiry', $new_refresh_expiration);

9. 实现令牌撤销机制

您可以添加一个管理界面,允许管理员撤销用户的刷新令牌:

// 添加撤销令牌的功能  
function revoke_user_refresh_token($user_id) {  
    delete_user_meta($user_id, 'jwt_refresh_token_hash');  
    delete_user_meta($user_id, 'jwt_refresh_token_expiry');  

    // 可选:记录撤销事件  
    update_user_meta($user_id, 'jwt_token_revoked_at', time());  
}  

// 在用户编辑页面添加撤销按钮  
add_action('edit_user_profile', 'add_revoke_token_button');  
add_action('show_user_profile', 'add_revoke_token_button');  

function add_revoke_token_button($user) {  
    if (current_user_can('edit_users')) {  
        $token_hash = get_user_meta($user->ID, 'jwt_refresh_token_hash', true);  
        $token_expiry = get_user_meta($user->ID, 'jwt_refresh_token_expiry', true);  

        echo '<h3>JWT 认证令牌</h3>';  
        echo '<table class="form-table">';  
        echo '<tr>';  
        echo '<th><label>刷新令牌状态</label></th>';  
        echo '<td>';  

        if ($token_hash && $token_expiry > time()) {  
            echo '活跃 (过期时间: ' . date('Y-m-d H:i:s', $token_expiry) . ')';  
            echo ' <a href="' . wp_nonce_url(admin_url('user-edit.php?user_id=' . $user->ID . '&action=revoke_jwt_token'), 'revoke_jwt_token_' . $user->ID) . '" class="button">撤销令牌</a>';  
        } else {  
            echo '无活跃令牌';  
        }  

        echo '</td>';  
        echo '</tr>';  
        echo '</table>';  
    }  
}  

// 处理撤销请求  
add_action('admin_init', 'handle_token_revocation');  

function handle_token_revocation() {  
    if (isset($_GET['action']) && $_GET['action'] === 'revoke_jwt_token' && isset($_GET['user_id'])) {  
        $user_id = intval($_GET['user_id']);  

        if (current_user_can('edit_users') && check_admin_referer('revoke_jwt_token_' . $user_id)) {  
            revoke_user_refresh_token($user_id);  

            // 添加通知  
            add_action('admin_notices', function() {  
                echo '<div class="notice notice-success is-dismissible"><p>用户的 JWT 令牌已成功撤销。</p></div>';  
            });  
        }  
    }  
}

10. 完整的插件实现

将所有这些功能整合到一个完整的插件中:

<?php  
/**  
 * Plugin Name: WordPress JWT Authentication with Refresh Tokens  
 * Description: 为 WordPress 添加带刷新令牌的 JWT 认证支持  
 * Version: 1.0  
 * Author: Your Name  
 */  

// 确保直接访问时退出  
if (!defined('ABSPATH')) {  
    exit;  
}  

// 引入 JWT 库  
require_once __DIR__ . '/vendor/autoload.php';  
use Firebase\JWT\JWT;  
use Firebase\JWT\Key;  

// 定义常量(如果没有在 wp-config.php 中定义)  
if (!defined('JWT_AUTH_ACCESS_SECRET_KEY')) {  
    define('JWT_AUTH_ACCESS_SECRET_KEY', 'your-access-token-secret-key');  
}  

if (!defined('JWT_AUTH_REFRESH_SECRET_KEY')) {  
    define('JWT_AUTH_REFRESH_SECRET_KEY', 'your-refresh-token-secret-key');  
}  

if (!defined('JWT_AUTH_ACCESS_TOKEN_EXPIRATION')) {  
    define('JWT_AUTH_ACCESS_TOKEN_EXPIRATION', 3600); // 1小时  
}  

if (!defined('JWT_AUTH_REFRESH_TOKEN_EXPIRATION')) {  
    define('JWT_AUTH_REFRESH_TOKEN_EXPIRATION', 1209600); // 14天  
}  

// 包含所有之前定义的函数  
// ...  

// 添加管理页面  
add_action('admin_menu', 'jwt_auth_add_admin_menu');  

function jwt_auth_add_admin_menu() {  
    add_options_page(  
        'JWT 认证设置',  
        'JWT 认证',  
        'manage_options',  
        'jwt-auth-settings',  
        'jwt_auth_settings_page'  
    );  
}  

function jwt_auth_settings_page() {  
    ?>  
    <div class="wrap">  
        <h1>JWT 认证设置</h1>  
        <form method="post" action="options.php">  
            <?php  
            settings_fields('jwt_auth_settings');  
            do_settings_sections('jwt-auth-settings');  
            submit_button();  
            ?>  
        </form>  
    </div>  
    <?php  
}  

add_action('admin_init', 'jwt_auth_settings_init');  

function jwt_auth_settings_init() {  
    register_setting('jwt_auth_settings', 'jwt_auth_settings');  

    add_settings_section(  
        'jwt_auth_settings_section',  
        '令牌设置',  
        'jwt_auth_settings_section_callback',  
        'jwt-auth-settings'  
    );  

    add_settings_field(  
        'access_token_expiration',  
        '访问令牌有效期(秒)',  
        'jwt_auth_access_token_expiration_render',  
        'jwt-auth-settings',  
        'jwt_auth_settings_section'  
    );  

    add_settings_field(  
        'refresh_token_expiration',  
        '刷新令牌有效期(秒)',  
        'jwt_auth_refresh_token_expiration_render',  
        'jwt-auth-settings',  
        'jwt_auth_settings_section'  
    );  
}  

function jwt_auth_settings_section_callback() {  
    echo '配置 JWT 认证令牌的设置。';  
}  

function jwt_auth_access_token_expiration_render() {  
    $options = get_option('jwt_auth_settings');  
    ?>  
    <input type="number" name="jwt_auth_settings[access_token_expiration]" value="<?php echo isset($options['access_token_expiration']) ? esc_attr($options['access_token_expiration']) : JWT_AUTH_ACCESS_TOKEN_EXPIRATION; ?>">  
    <p class="description">访问令牌的有效期(以秒为单位)。默认为 3600(1小时)。</p>  
    <?php  
}  

function jwt_auth_refresh_token_expiration_render() {  
    $options = get_option('jwt_auth_settings');  
    ?>  
    <input type="number" name="jwt_auth_settings[refresh_token_expiration]" value="<?php echo isset($options['refresh_token_expiration']) ? esc_attr($options['refresh_token_expiration']) : JWT_AUTH_REFRESH_TOKEN_EXPIRATION; ?>">  
    <p class="description">刷新令牌的有效期(以秒为单位)。默认为 1209600(14天)。</p>  
    <?php  
}

安全最佳实践

  1. 使用不同的密钥:为访问令牌和刷新令牌使用不同的密钥,这样即使一个密钥泄露,也不会影响另一个令牌的安全性。
  2. 限制刷新令牌的路径:将刷新令牌的 cookie 路径限制为只能在刷新端点使用,这样可以减少被盗用的风险。
  3. 实现令牌轮换:每次使用刷新令牌时,都生成一个新的刷新令牌,这样可以限制被盗用的刷新令牌的使用时间。
  4. 存储令牌哈希:在数据库中只存储刷新令牌的哈希值,而不是令牌本身,这样即使数据库被泄露,攻击者也无法获取有效的令牌。
  5. 实现令牌撤销:提供一种机制来撤销用户的刷新令牌,以应对令牌泄露的情况。
  6. 监控异常活动:记录令牌使用情况,检测异常活动,例如从不同 IP 地址使用同一个刷新令牌。

Notes

WordPress 核心没有内置的 JWT 支持,所以上述解决方案是基于 WordPress 的钩子系统和 PHP 的 cookie 功能,结合第三方 JWT 库来实现的。这种双令牌认证系统可以显著提高安全性,同时改善用户体验,因为用户不需要频繁登录。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇