您希望在 WordPress 中实现刷新令牌(Refresh Token)和访问令牌(Access Token)并存的 JWT 认证系统。这是一种常见的认证模式,可以提高安全性并改善用户体验。下面我将详细介绍如何在 WordPress 中实现这种机制。
刷新令牌和访问令牌的基本概念
在实现之前,让我们先了解这两种令牌的区别:
- 访问令牌(Access Token):
- 短期有效(通常几分钟到几小时)
- 用于访问受保护的资源
- 存储在 HTTP-only cookie 中以防止 XSS 攻击
- 刷新令牌(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
}
安全最佳实践
- 使用不同的密钥:为访问令牌和刷新令牌使用不同的密钥,这样即使一个密钥泄露,也不会影响另一个令牌的安全性。
- 限制刷新令牌的路径:将刷新令牌的 cookie 路径限制为只能在刷新端点使用,这样可以减少被盗用的风险。
- 实现令牌轮换:每次使用刷新令牌时,都生成一个新的刷新令牌,这样可以限制被盗用的刷新令牌的使用时间。
- 存储令牌哈希:在数据库中只存储刷新令牌的哈希值,而不是令牌本身,这样即使数据库被泄露,攻击者也无法获取有效的令牌。
- 实现令牌撤销:提供一种机制来撤销用户的刷新令牌,以应对令牌泄露的情况。
- 监控异常活动:记录令牌使用情况,检测异常活动,例如从不同 IP 地址使用同一个刷新令牌。
Notes
WordPress 核心没有内置的 JWT 支持,所以上述解决方案是基于 WordPress 的钩子系统和 PHP 的 cookie 功能,结合第三方 JWT 库来实现的。这种双令牌认证系统可以显著提高安全性,同时改善用户体验,因为用户不需要频繁登录。