您希望在 WordPress 中实现一个自定义数据表来管理 JWT token,并且希望这些 token 能与设备相关联。这是一个很好的安全实践,可以让您更好地控制用户的会话,并在需要时撤销特定设备的访问权限。
实现方案概述
我们需要完成以下几个步骤:
- 创建自定义数据表
- 实现 token 生成和存储逻辑
- 添加设备信息收集
- 实现 token 验证和刷新
- 创建管理界面
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:
wp_jwt_access_token
- 存储访问令牌,可在整个网站中使用(path='/')wp_jwt_refresh_token
- 存储刷新令牌,仅限于刷新端点使用(path='/token-refresh')
两个 cookie 都设置了以下安全选项:
httponly
设为true
,防止 JavaScript 访问 cookie,保护令牌免受 XSS 攻击secure
设为is_ssl()
,确保在 HTTPS 连接中才发送 cookiesamesite
设为'Strict'
,防止跨站请求伪造攻击domain
设为网站的主域名expires
分别设置为访问令牌和刷新令牌的过期时间
这种实现方式确保了 JWT 令牌的安全存储和传输,同时通过限制刷新令牌的路径范围来减少被盗用的风险。
Notes
这段代码是 WordPress 自定义 JWT 认证插件实现的一部分,用于在用户登录后设置包含 JWT 令牌的 cookie。虽然 WordPress 核心没有内置 JWT 支持,但通过这种方式可以为 WordPress 添加基于 JWT 的认证机制。