实现自定义数据表来管理 jwt token

您希望在 WordPress 中实现一个自定义数据表来管理 JWT token,并且希望这些 token 能与设备相关联。这是一个很好的安全实践,可以让您更好地控制用户的会话,并在需要时撤销特定设备的访问权限。

实现方案概述

我们需要完成以下几个步骤:

  1. 创建自定义数据表
  2. 实现 token 生成和存储逻辑
  3. 添加设备信息收集
  4. 实现 token 验证和刷新
  5. 创建管理界面

1. 创建自定义数据表

首先,我们需要在 WordPress 中创建一个自定义表来存储 JWT token 和设备信息。这可以在插件激活时完成:

function jwt_device_tokens_create_tables() {  
    global $wpdb;  

    $table_name = $wpdb->prefix . 'jwt_device_tokens';  
    $charset_collate = $wpdb->get_charset_collate();  

    $sql = "CREATE TABLE $table_name (  
        id bigint(20) NOT NULL AUTO_INCREMENT,  
        user_id bigint(20) NOT NULL,  
        refresh_token varchar(255) NOT NULL,  
        refresh_token_hash varchar(255) NOT NULL,  
        access_token_hash varchar(255) DEFAULT NULL,  
        device_name varchar(255) NOT NULL,  
        device_type varchar(50) NOT NULL,  
        device_os varchar(50) NOT NULL,  
        device_browser varchar(50) NOT NULL,  
        device_ip varchar(45) NOT NULL,  
        user_agent text NOT NULL,  
        issued_at datetime NOT NULL,  
        expires_at datetime NOT NULL,  
        last_used datetime DEFAULT NULL,  
        is_revoked tinyint(1) DEFAULT 0,  
        PRIMARY KEY  (id),  
        KEY user_id (user_id),  
        KEY refresh_token_hash (refresh_token_hash),  
        KEY is_revoked (is_revoked)  
    ) $charset_collate;";  

    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');  
    dbDelta($sql);  
}  

register_activation_hook(__FILE__, 'jwt_device_tokens_create_tables');

2. 实现 token 生成和存储逻辑

接下来,我们需要在用户登录时生成 token 并收集设备信息:

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

    // 获取设备信息  
    $device_info = jwt_device_get_device_info();  

    // 生成刷新令牌  
    $issued_at = time();  
    $refresh_expiration = $issued_at + JWT_AUTH_REFRESH_TOKEN_EXPIRATION;  

    // 生成一个随机的刷新令牌  
    $refresh_token = wp_generate_password(64, false);  
    $refresh_token_hash = wp_hash($refresh_token);  

    // 生成访问令牌  
    $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  
        ),  
        'device' => array(  
            'name' => $device_info['device_name'],  
            'type' => $device_info['device_type']  
        ),  
        'token_type' => 'access'  
    );  

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

    // 存储到数据库  
    global $wpdb;  
    $table_name = $wpdb->prefix . 'jwt_device_tokens';  

    $wpdb->insert(  
        $table_name,  
        array(  
            'user_id' => $user->ID,  
            'refresh_token' => $refresh_token,  
            'refresh_token_hash' => $refresh_token_hash,  
            'access_token_hash' => $access_token_hash,  
            'device_name' => $device_info['device_name'],  
            'device_type' => $device_info['device_type'],  
            'device_os' => $device_info['device_os'],  
            'device_browser' => $device_info['device_browser'],  
            'device_ip' => $device_info['device_ip'],  
            'user_agent' => $device_info['user_agent'],  
            'issued_at' => date('Y-m-d H:i:s', $issued_at),  
            'expires_at' => date('Y-m-d H:i:s', $refresh_expiration),  
            'last_used' => date('Y-m-d H:i:s', $issued_at)  
        )  
    );  

    // 设置 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'  
        ]  
    );  

    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'  
        ]  
    );  
}

3. 添加设备信息收集

我们需要一个函数来收集设备信息:

function jwt_device_get_device_info() {  
    // 使用第三方库如 Mobile Detect 或 Device Detector 可以获得更准确的信息  
    // 这里使用简化版本  
    $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';  
    $ip = jwt_device_get_client_ip();  

    // 简单的设备类型检测  
    $device_type = 'desktop';  
    if (preg_match('/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i', $user_agent)) {  
        $device_type = 'mobile';  
    } else if (preg_match('/android|ipad|playbook|silk/i', $user_agent)) {  
        $device_type = 'tablet';  
    }  

    // 操作系统检测  
    $os = 'Unknown';  
    if (preg_match('/windows nt/i', $user_agent)) {  
        $os = 'Windows';  
    } else if (preg_match('/macintosh|mac os x/i', $user_agent)) {  
        $os = 'MacOS';  
    } else if (preg_match('/linux/i', $user_agent)) {  
        $os = 'Linux';  
    } else if (preg_match('/iphone|ipad/i', $user_agent)) {  
        $os = 'iOS';  
    } else if (preg_match('/android/i', $user_agent)) {  
        $os = 'Android';  
    }  

    // 浏览器检测  
    $browser = 'Unknown';  
    if (preg_match('/msie|trident/i', $user_agent)) {  
        $browser = 'Internet Explorer';  
    } else if (preg_match('/firefox/i', $user_agent)) {  
        $browser = 'Firefox';  
    } else if (preg_match('/chrome/i', $user_agent)) {  
        $browser = 'Chrome';  
    } else if (preg_match('/safari/i', $user_agent)) {  
        $browser = 'Safari';  
    } else if (preg_match('/opera|opr/i', $user_agent)) {  
        $browser = 'Opera';  
    } else if (preg_match('/edge/i', $user_agent)) {  
        $browser = 'Edge';  
    }  

    // 设备名称(简化版)  
    $device_name = $os . ' ' . $browser;  

    return array(  
        'device_name' => $device_name,  
        'device_type' => $device_type,  
        'device_os' => $os,  
        'device_browser' => $browser,  
        'device_ip' => $ip,  
        'user_agent' => $user_agent  
    );  
}  

function jwt_device_get_client_ip() {  
    $ip = '';  

    if (!empty($_SERVER['HTTP_CLIENT_IP'])) {  
        $ip = $_SERVER['HTTP_CLIENT_IP'];  
    } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {  
        $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];  
    } else {  
        $ip = $_SERVER['REMOTE_ADDR'];  
    }  

    return $ip;  
}

4. 实现 token 验证和刷新

现在我们需要实现 token 验证和刷新功能:

function jwt_device_verify_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'];  
    $token_hash = wp_hash($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;  
        }  

        // 验证令牌是否在数据库中存在且未被撤销  
        global $wpdb;  
        $table_name = $wpdb->prefix . 'jwt_device_tokens';  

        $token_exists = $wpdb->get_var(  
            $wpdb->prepare(  
                "SELECT id FROM $table_name   
                WHERE access_token_hash = %s   
                AND user_id = %d   
                AND is_revoked = 0   
                AND expires_at > %s",  
                $token_hash,  
                $decoded->user->id,  
                date('Y-m-d H:i:s')  
            )  
        );  

        if (!$token_exists) {  
            return false;  
        }  

        // 更新最后使用时间  
        $wpdb->update(  
            $table_name,  
            array('last_used' => date('Y-m-d H:i:s')),  
            array('access_token_hash' => $token_hash)  
        );  

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

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

// 刷新令牌端点  
add_action('rest_api_init', 'jwt_device_register_token_refresh_route');  
function jwt_device_register_token_refresh_route() {  
    register_rest_route('jwt-auth/v1', '/token-refresh', array(  
        'methods' => 'POST',  
        'callback' => 'jwt_device_refresh_access_token',  
        'permission_callback' => '__return_true'  
    ));  
}  

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

    // 检查刷新令牌是否存在  
    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'];  
    $refresh_token_hash = wp_hash($refresh_token);  

    // 从数据库中获取令牌信息  
    global $wpdb;  
    $table_name = $wpdb->prefix . 'jwt_device_tokens';  

    $token_data = $wpdb->get_row(  
        $wpdb->prepare(  
            "SELECT * FROM $table_name   
            WHERE refresh_token_hash = %s   
            AND is_revoked = 0   
            AND expires_at > %s",  
            $refresh_token_hash,  
            date('Y-m-d H:i:s')  
        )  
    );  

    if (!$token_data) {  
        return new WP_Error(  
            'jwt_auth_invalid_token',  
            __('无效的刷新令牌或已过期'),  
            array('status' => 401)  
        );  
    }  

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

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

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

    // 获取设备信息  
    $device_info = array(  
        'device_name' => $token_data->device_name,  
        'device_type' => $token_data->device_type  
    );  

    $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  
        ),  
        'device' => $device_info,  
        'token_type' => 'access'  
    );  

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

    $access_token_hash = wp_hash($access_token);  

    // 更新数据库中的访问令牌哈希  
    $wpdb->update(  
        $table_name,  
        array(  
            'access_token_hash' => $access_token_hash,  
            'last_used' => date('Y-m-d H:i:s')  
        ),  
        array('id' => $token_data->id)  
    );  

    // 设置新的访问令牌 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'  
        ]  
    );  

    // 可选:实现令牌轮换 - 每次刷新访问令牌时也更新刷新令牌  
    if (defined('JWT_AUTH_ROTATE_REFRESH_TOKEN') && JWT_AUTH_ROTATE_REFRESH_TOKEN) {  
        $refresh_expiration = $issued_at + JWT_AUTH_REFRESH_TOKEN_EXPIRATION;  
        $new_refresh_token = wp_generate_password(64, false);  
        $new_refresh_token_hash = wp_hash($new_refresh_token);  

        // 更新数据库中的刷新令牌  
        $wpdb->update(  
            $table_name,  
            array(  
                'refresh_token' => $new_refresh_token,  
                'refresh_token_hash' => $new_refresh_token_hash,  
                'expires_at' => date('Y-m-d H:i:s', $refresh_expiration)  
            ),  
            array('id' => $token_data->id)  
        );  

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

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

5. 创建管理界面

现在我们需要创建一个管理界面,让管理员可以查看和管理用户的设备和令牌:

// 添加管理菜单  
add_action('admin_menu', 'jwt_device_add_admin_menu');  
function jwt_device_add_admin_menu() {  
    add_users_page(  
        __('设备登录管理'),  
        __('设备登录'),  
        'edit_users',  
        'jwt-device-tokens',  
        'jwt_device_tokens_page'  
    );  
}  

// 设备令牌管理页面  
function jwt_device_tokens_page() {  
    global $wpdb;  
    $table_name = $wpdb->prefix . 'jwt_device_tokens';  

    // 处理撤销令牌请求  
    if (isset($_GET['action']) && $_GET['action'] === 'revoke' && isset($_GET['token_id'])) {  
        $token_id = intval($_GET['token_id']);  

        if (current_user_can('edit_users') && check_admin_referer('revoke_token_' . $token_id)) {  
            $wpdb->update(  
                $table_name,  
                array('is_revoked' => 1),  
                array('id' => $token_id)  
            );  

            echo '<div class="notice notice-success is-dismissible"><p>' . __('令牌已成功撤销。') . '</p></div>';  
        }  
    }  

    // 获取所有活跃的令牌  
    $tokens = $wpdb->get_results(  
        "SELECT t.*, u.user_login, u.user_email   
        FROM $table_name t   
        JOIN {$wpdb->users} u ON t.user_id = u.ID   
        WHERE t.is_revoked = 0   
        AND t.expires_at > NOW()   
        ORDER BY t.last_used DESC"  
    );  

    ?>  
    <div class="wrap">  
        <h1><?php _e('设备登录管理'); ?></h1>  

        <table class="wp-list-table widefat fixed striped">  
            <thead>  
                <tr>  
                    <th><?php _e('用户'); ?></th>  
                    <th><?php _e('设备'); ?></th>  
                    <th><?php _e('操作系统'); ?></th>  
                    <th><?php _e('浏览器'); ?></th>  
                    <th><?php _e('IP 地址'); ?></th>  
                    <th><?php _e('最后使用'); ?></th>  
                    <th><?php _e('过期时间'); ?></th>  
                    <th><?php _e('操作'); ?></th>  
                </tr>  
            </thead>  
            <tbody>  
                <?php if (empty($tokens)) : ?>  
                    <tr>  
                        <td colspan="8"><?php _e('没有活跃的设备登录。'); ?></td>  
                    </tr>  
                <?php else : ?>  
                    <?php foreach ($tokens as $token) : ?>  
                        <tr>  
                            <td>  
                                <?php echo esc_html($token->user_login); ?><br>  
                                <small><?php echo esc_html($token->user_email); ?></small>  
                            </td>  
                            <td><?php echo esc_html($token->device_name); ?></td>  
                            <td><?php echo esc_html($token->device_os); ?></td>  
                            <td><?php echo esc_html($token->device_browser); ?></td>  
                            <td><?php echo esc_html($token->device_ip); ?></td>  
                            <td>  
                                <?php   
                                if ($token->last_used) {  
                                    echo esc_html(human_time_diff(strtotime($token->last_used), current_time('timestamp'))) . __(' 前');  
                                } else {  
                                    _e('从未');  
                                }  
                                ?>  
                            </td>  
                            <td>  
                                <?php echo esc_html(human_time_diff(current_time('timestamp'), strtotime($token->expires_at))) . __(' 后'); ?>  
                            </td>  
                            <td>  
                                <a href="<?php echo wp_nonce_url(admin_url('users.php?page=jwt-device-tokens&action=revoke&token_id=' . $token->id), 'revoke_token_' . $token->id); ?>" class="button button-small" onclick="return confirm('<?php _e('确定要撤销此设备的访问权限吗?'); ?>');">  
                                    <?php _e('撤销'); ?>  
                                </a>  
                            </td>  
                        </tr>  
                    <?php endforeach; ?>  
                <?php endif; ?>  
            </tbody>  
        </table>  
    </div>  
    <?php  
}

6. 用户个人资料页面集成

让用户可以在自己的个人资料页面查看和管理自己的设备登录:

// 添加到用户个人资料页面  
add_action('show_user_profile', 'jwt_device_show_user_tokens');  
add_action('edit_user_profile', 'jwt_device_show_user_tokens');  

function jwt_device_show_user_tokens($user) {  
    // 只有当前用户或管理员可以查看  
    if (!current_user_can('edit_user', $user->ID)) {  
        return;  
    }  

    global $wpdb;  
    $table_name = $wpdb->prefix . 'jwt_device_tokens';  

    // 获取用户的活跃令牌  
    $tokens = $wpdb->get_results(  
        $wpdb->prepare(  
            "SELECT * FROM $table_name   
            WHERE user_id = %d   
            AND is_revoked = 0   
            AND expires_at > NOW()   
            ORDER BY last_used DESC",  
            $user->ID  
        )  
    );  

    ?>  
    <h2><?php _e('设备登录'); ?></h2>  

    <table class="form-table">  
        <tr>  
            <th><?php _e('当前活跃的设备'); ?></th>  
            <td>  
                <?php if (empty($tokens)) : ?>  
                    <p><?php _e('没有活跃的设备登录。'); ?></p>  
                <?php else : ?>  
                    <table class="widefat striped" style="max-width: 800px;">  
                        <thead>  
                            <tr>  
                                <th><?php _e('设备'); ?></th>  
                                <th><?php _e('操作系统'); ?></th>  
                                <th><?php _e('浏览器'); ?></th>  
                                <th><?php _e('最后使用'); ?></th>  
                                <th><?php _e('操作'); ?></th>  
                            </tr>  
                        </thead>  
                        <tbody>  
                            <?php foreach ($tokens as $token) : ?>  
                                <tr>  
                                    <td><?php echo esc_html($token->device_name); ?></td>  
                                    <td><?php echo esc_html($token->device_os); ?></td>  
                                    <td><?php echo esc_html($token->device_browser); ?></td>  
                                    <td>  
                                        <?php   
                                        if ($token->last_used) {  
                                            echo esc_html(human_time_diff(strtotime($token->last_used), current_time('timestamp'))) . __(' 前');  
                                        } else {  
                                            _e('从未');  
                                        }  
                                        ?>  
                                    </td>  
                                    <td>  
                                        <?php if (current_user_can('edit_user', $user->ID)) : ?>  
                                            <a href="<?php echo wp_nonce_url(add_query_arg(array('action' => 'revoke_device', 'token_id' => $token->id), admin_url('profile.php')), 'revoke_device_' . $token->id); ?>" class="button button-small" onclick="return confirm('<?php _e('确定要撤销此设备的访问权限吗?'); ?>');">  
                                                <?php _e('撤销'); ?>  
                                            </a>  
                                        <?php endif; ?>  
                                    </td>  
                                </tr>  
                            <?php endforeach; ?>  
                        </tbody>  
                    </table>  
                <?php endif; ?>  
            </td>  
        </tr>  
    </table>  
    <?php  
}  

// 处理个人资料页面的撤销请求  
add_action('admin_init', 'jwt_device_handle_profile_revoke');  
function jwt_device_handle_profile_revoke() {  
    if (isset($_GET['action']) && $_GET['action'] === 'revoke_device' && isset($_GET['token_id'])) {  
        $token_id = intval($_GET['token_id']);  
        $user_id = get_current_user_id();  

        // 验证 nonce  
        if (!wp_verify_nonce($_GET['_wpnonce'], 'revoke_device_' . $token_id)) {  
            wp_die(__('安全检查失败。'));  
        }  

        global $wpdb;  
        $table_name = $wpdb->prefix . 'jwt_device_tokens';  

        // 确保用户只能撤销自己的设备(管理员可以撤销任何设备)  
        $token = $wpdb->get_row(  
            $wpdb->prepare(  
                "SELECT * FROM $table_name WHERE id = %d",  
                $token_id  
            )  
        );  

        if (!$token || ($token->user_id != $user_id && !current_user_can('edit_users'))) {  
            wp_die(__('您没有权限执行此操作。'));  
        }  

        // 撤销令牌  
        $wpdb->update(  
            $table_name,  
            array('is_revoked' => 1),  
            array('id' => $token_id)  
        );  

        // 重定向回个人资料页面  
        wp_redirect(add_query_arg(array('revoked' => '1'), admin_url('profile.php')));  
        exit;  
    }  

    // 显示撤销成功消息  
    if (isset($_GET['revoked']) && $_GET['revoked'] === '1') {  
        add_action('admin_notices', function() {  
            echo '<div class="notice notice-success is-dismissible"><p>' . __('设备访问已成功撤销。') . '</p></div>';  
        });  
    }  
}

7. 处理登出

当用户登出时,我们需要撤销当前设备的令牌:

add_action('wp_logout', 'jwt_device_handle_logout');  
function jwt_device_handle_logout() {  
    // 检查刷新令牌是否存在  
    if (!isset($_COOKIE['wp_jwt_refresh_token'])) {  
        return;  
    }  

    $refresh_token = $_COOKIE['wp_jwt_refresh_token'];  
    $refresh_token_hash = wp_hash($refresh_token);  

    global $wpdb;  
    $table_name = $wpdb->prefix . 'jwt_device_tokens';  

    // 撤销当前设备的令牌  
    $wpdb->update(  
        $table_name,  
        array('is_revoked' => 1),  
        array('refresh_token_hash' => $refresh_token_hash)  
    );  

    // 清除 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'  
        ]  
    );  

    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. 与 REST API 集成

将我们的 JWT 认证系统与 WordPress REST API 集成:

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

    // 验证 JWT  
    if (jwt_device_verify_access_token()) {  
        return true;  
    }  

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

9. 清理过期令牌

定期清理过期的令牌以保持数据库整洁:

// 注册每日清理过期令牌的计划任务  
register_activation_hook(__FILE__, 'jwt_device_schedule_cleanup');  
function jwt_device_schedule_cleanup() {  
    if (!wp_next_scheduled('jwt_device_cleanup_expired_tokens')) {  
        wp_schedule_event(time(), 'daily', 'jwt_device_cleanup_expired_tokens');  
    }  
}  

// 清理过期令牌的钩子  
add_action('jwt_device_cleanup_expired_tokens', 'jwt_device_cleanup_expired_tokens');  
function jwt_device_cleanup_expired_tokens() {  
    global $wpdb;  
    $table_name = $wpdb->prefix . 'jwt_device_tokens';  

    // 删除过期超过 30 天的令牌  
    $wpdb->query(  
        $wpdb->prepare(  
            "DELETE FROM $table_name WHERE expires_at < %s",  
            date('Y-m-d H:i:s', strtotime('-30 days'))  
        )  
    );  
}  

// 在插件停用时清除计划任务  
register_deactivation_hook(__FILE__, 'jwt_device_clear_scheduled_cleanup');  
function jwt_device_clear_scheduled_cleanup() {  
    wp_clear_scheduled_hook('jwt_device_cleanup_expired_tokens');  
}

10. 完整的插件实现

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

<?php  
/**  
 * Plugin Name: WordPress JWT Device Authentication  
 * Description: 为 WordPress 添加基于设备的 JWT 认证支持  
 * Version: 1.0  
 * Author: Your Name  
 */  

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

// 定义常量(如果没有在 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天  
}  

// 定义是否启用刷新令牌轮换  
if (!defined('JWT_AUTH_ROTATE_REFRESH_TOKEN')) {  
    define('JWT_AUTH_ROTATE_REFRESH_TOKEN', true);  
}  

// 创建数据表  
function jwt_device_tokens_create_tables() {  
    global $wpdb;  

    $table_name = $wpdb->prefix . 'jwt_device_tokens';  
    $charset_collate = $wpdb->get_charset_collate();  

    $sql = "CREATE TABLE $table_name (  
        id bigint(20) NOT NULL AUTO_INCREMENT,  
        user_id bigint(20) NOT NULL,  
        refresh_token varchar(255) NOT NULL,  
        refresh_token_hash varchar(255) NOT NULL,  
        access_token_hash varchar(255) DEFAULT NULL,  
        device_name varchar(255) NOT NULL,  
        device_type varchar(50) NOT NULL,  
        device_os varchar(50) NOT NULL,  
        device_browser varchar(50) NOT NULL,  
        device_ip varchar(45) NOT NULL,  
        user_agent text NOT NULL,  
        issued_at datetime NOT NULL,  
        expires_at datetime NOT NULL,  
        last_used datetime DEFAULT NULL,  
        is_revoked tinyint(1) DEFAULT 0,  
        PRIMARY KEY  (id),  
        KEY user_id (user_id),  
        KEY refresh_token_hash (refresh_token_hash),  
        KEY is_revoked (is_revoked)  
    ) $charset_collate;";  

    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');  
    dbDelta($sql);  
}  

register_activation_hook(__FILE__, 'jwt_device_tokens_create_tables');  

// 添加插件设置页面  
add_action('admin_menu', 'jwt_device_add_settings_page');  
function jwt_device_add_settings_page() {  
    add_options_page(  
        __('JWT 设备认证设置'),  
        __('JWT 设备认证'),  
        'manage_options',  
        'jwt-device-auth-settings',  
        'jwt_device_settings_page'  
    );  
}  

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

        <h2><?php _e('安全提示'); ?></h2>  
        <p><?php _e('为了最大限度地提高安全性,建议在 wp-config.php 中定义以下常量:'); ?></p>  
        <pre>  
define('JWT_AUTH_ACCESS_SECRET_KEY', '<?php echo esc_html(wp_generate_password(64, true, true)); ?>');  
define('JWT_AUTH_REFRESH_SECRET_KEY', '<?php echo esc_html(wp_generate_password(64, true, true)); ?>');  
define('JWT_AUTH_ACCESS_TOKEN_EXPIRATION', 3600); // 1小时  
define('JWT_AUTH_REFRESH_TOKEN_EXPIRATION', 1209600); // 14天  
define('JWT_AUTH_ROTATE_REFRESH_TOKEN', true);  
        </pre>  
    </div>  
    <?php  
}  

// 初始化插件设置  
add_action('admin_init', 'jwt_device_settings_init');  
function jwt_device_settings_init() {  
    register_setting('jwt_device_auth_settings', 'jwt_device_auth_settings');  

    add_settings_section(  
        'jwt_device_auth_settings_section',  
        '令牌设置',  
        'jwt_device_settings_section_callback',  
        'jwt-device-auth-settings'  
    );  

    add_settings_field(  
        'access_token_expiration',  
        '访问令牌有效期(秒)',  
        'jwt_device_access_token_expiration_render',  
        'jwt-device-auth-settings',  
        'jwt_device_auth_settings_section'  
    );  

    add_settings_field(  
        'refresh_token_expiration',  
        '刷新令牌有效期(秒)',  
        'jwt_device_refresh_token_expiration_render',  
        'jwt-device-auth-settings',  
        'jwt_device_auth_settings_section'  
    );  

    add_settings_field(  
        'rotate_refresh_token',  
        '令牌轮换',  
        'jwt_device_rotate_refresh_token_render',  
        'jwt-device-auth-settings',  
        'jwt_device_auth_settings_section'  
    );  
}  

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

function jwt_device_access_token_expiration_render() {  
    $options = get_option('jwt_device_auth_settings');  
    ?>  
    <input type="number" name="jwt_device_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_device_refresh_token_expiration_render() {  
    $options = get_option('jwt_device_auth_settings');  
    ?>  
    <input type="number" name="jwt_device_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  
}  

function jwt_device_rotate_refresh_token_render() {  
    $options = get_option('jwt_device_auth_settings');  
    ?>  
    <input type="checkbox" name="jwt_device_auth_settings[rotate_refresh_token]" <?php checked(isset($options['rotate_refresh_token']) ? $options['rotate_refresh_token'] : JWT_AUTH_ROTATE_REFRESH_TOKEN); ?>>  
    <p class="description">每次刷新访问令牌时也更新刷新令牌,提高安全性。</p>  
    <?php  
}  

// 获取设备信息  
function jwt_device_get_device_info() {  
    // 使用第三方库如 Mobile Detect 或 Device Detector 可以获得更准确的信息  
    // 这里使用简化版本  
    $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';  
    $ip = jwt_device_get_client_ip();  

    // 简单的设备类型检测  
    $device_type = 'desktop';  
    if (preg_match('/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i', $user_agent)) {  
        $device_type = 'mobile';  
    } else if (preg_match('/android|ipad|playbook|silk/i', $user_agent)) {  
        $device_type = 'tablet';  
    }  

    // 操作系统检测  
    $os = 'Unknown';  
    if (preg_match('/windows nt/i', $user_agent)) {  
        $os = 'Windows';  
    } else if (preg_match('/macintosh|mac os x/i', $user_agent)) {  
        $os = 'MacOS';  
    } else if (preg_match('/linux/i', $user_agent)) {  
        $os = 'Linux';  
    } else if (preg_match('/iphone|ipad/i', $user_agent)) {  
        $os = 'iOS';  
    } else if (preg_match('/android/i', $user_agent)) {  
        $os = 'Android';  
    }  

    // 浏览器检测  
    $browser = 'Unknown';  
    if (preg_match('/msie|trident/i', $user_agent)) {  
        $browser = 'Internet Explorer';  
    } else if (preg_match('/firefox/i', $user_agent)) {  
        $browser = 'Firefox';  
    } else if (preg_match('/chrome/i', $user_agent)) {  
        $browser = 'Chrome';  
    } else if (preg_match('/safari/i', $user_agent)) {  
        $browser = 'Safari';  
    } else if (preg_match('/opera|opr/i', $user_agent)) {  
        $browser = 'Opera';  
    } else if (preg_match('/edge/i', $user_agent)) {  
        $browser = 'Edge';  
    }  

    // 设备名称(简化版)  
    $device_name = $os . ' ' . $browser;  

    return array(  
        'device_name' => $device_name,  
        'device_type' => $device_type,  
        'device_os' => $os,  
        'device_browser' => $browser,  
        'device_ip' => $ip,  
        'user_agent' => $user_agent  
    );  
}  

function jwt_device_get_client_ip() {  
    $ip = '';  

    if (!empty($_SERVER['HTTP_CLIENT_IP'])) {  
        $ip = $_SERVER['HTTP_CLIENT_IP'];  
    } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {  
        $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];  
    } else {  
        $ip = $_SERVER['REMOTE_ADDR'];  
    }  

    return $ip;  
}  

// 登录时生成令牌  
add_action('wp_login', 'jwt_device_generate_tokens', 10, 2);  
function jwt_device_generate_tokens($user_login, $user) {  
    require_once __DIR__ . '/vendor/autoload.php';  
    use Firebase\JWT\JWT;  

    // 获取设备信息  
    $device_info = jwt_device_get_device_info();  

    // 生成刷新令牌  
    $issued_at = time();  
    $refresh_expiration = $issued_at + JWT_AUTH_REFRESH_TOKEN_EXPIRATION;  

    // 生成一个随机的刷新令牌  
    $refresh_token = wp_generate_password(64, false);  
    $refresh_token_hash = wp_hash($refresh_token);  

    // 生成访问令牌  
    $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  
        ),  
        'device' => array(  
            'name' => $device_info['device_name'],  
            'type' => $device_info['device_type']  
        ),  
        'token_type' => 'access'  
    );  

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

    // 存储到数据库  
    global $wpdb;  
    $table_name = $wpdb->prefix . 'jwt_device_tokens';  

    $wpdb->insert(  
        $table_name,  
        array(  
            'user_id' => $user->ID,  
            'refresh_token' => $refresh_token,  
            'refresh_token_hash' => $refresh_token_hash,  
            'access_token_hash' => $access_token_hash,  
            'device_name' => $device_info['device_name'],  
            'device_type' => $device_info['device_type'],  
            'device_os' => $device_info['device_os'],  
            'device_browser' => $device_info['device_browser'],  
            'device_ip' => $device_info['device_ip'],  
            'user_agent' => $device_info['user_agent'],  
            'issued_at' => date('Y-m-d H:i:s', $issued_at),  
            'expires_at' => date('Y-m-d H:i:s', $refresh_expiration),  
            'last_used' => date('Y-m-d H:i:s', $issued_at)  
        )  
    );  

    // 设置 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  
    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'  
        ]  
    );

这段代码设置了两个 HTTP-only cookie:

  1. wp_jwt_access_token - 存储访问令牌,可在整个网站中使用(path='/')
  2. wp_jwt_refresh_token - 存储刷新令牌,仅限于刷新端点使用(path='/token-refresh')

两个 cookie 都设置了以下安全选项:

  • httponly 设为 true,防止 JavaScript 访问 cookie,保护令牌免受 XSS 攻击
  • secure 设为 is_ssl(),确保在 HTTPS 连接中才发送 cookie
  • samesite 设为 'Strict',防止跨站请求伪造攻击
  • domain 设为网站的主域名
  • expires 分别设置为访问令牌和刷新令牌的过期时间

这种实现方式确保了 JWT 令牌的安全存储和传输,同时通过限制刷新令牌的路径范围来减少被盗用的风险。

Notes

这段代码是 WordPress 自定义 JWT 认证插件实现的一部分,用于在用户登录后设置包含 JWT 令牌的 cookie。虽然 WordPress 核心没有内置 JWT 支持,但通过这种方式可以为 WordPress 添加基于 JWT 的认证机制。

暂无评论

发送评论 编辑评论


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