登录注册体系的源码级硬切换

This commit is contained in:
kate522 committed 2026-08-23 20:30:01 +08:00
1 parent 73fcd4ed72
commit af52f9e94d
72 files changed
+1666 -6576

No files matched your search

+18 -250
View File
@@ -12,233 +12,7 @@ final class Service {
use ServicePart1b;
private const RESET_TOKEN_HASH_META = 'aksrt_password_reset_token_hash';
private const RESET_TOKEN_EXPIRES_META = 'aksrt_password_reset_expires';
private const RESET_TOKEN_REQUESTED_AT = 'aksrt_password_reset_requested_at';
private const RESET_TOKEN_TTL = 3600;
public const EMAIL_VERIFIED_META = 'aksrt_email_verified';
/**
* 用户当时被发送的验证方式('link' | 'code')。
*
* 注意:这是 *审计字段*,仅用于后台展示「上一次给该用户发的是哪种验证邮件」。
* 路由分支(验证页应该显示链接提示还是验证码输入框、自助/管理员重发使用哪种方式)
* 一律读取全局配置 self::current_registration_verification_method(),
* 这样管理员把站点从 code 切换到 link 后,所有在途用户都立即跟随新方式。
* 不要根据这个 meta 决定页面模式或重发方式。
*/
public const EMAIL_METHOD_META = 'aksrt_email_verification_method';
public const EMAIL_TOKEN_HASH_META = 'aksrt_email_verification_token_hash';
public const EMAIL_CODE_HASH_META = 'aksrt_email_verification_code_hash';
public const EMAIL_EXPIRES_META = 'aksrt_email_verification_expires';
public const EMAIL_REQUESTED_AT_META = 'aksrt_email_verification_requested_at';
public const EMAIL_TARGET_META = 'aksrt_email_verification_email';
public const EMAIL_OBK_META = 'aksrt_email_verification_obk';
private const EMAIL_ACCESS_KEY_META = 'aksrt_email_verification_access_key';
public const EMAIL_CODE_ATTEMPTS_META = 'aksrt_email_verification_code_attempts';
public const EMAIL_CODE_MAX_ATTEMPTS = 5;
public const EMAIL_CODE_IP_TTL = 900;
// 自助重发的 IP 节流:每 IP 每 RESEND_IP_WINDOW 秒最多 RESEND_IP_MAX 次。
private const RESEND_IP_MAX = 6;
private const RESEND_IP_WINDOW = 600;
/**
* 启动钩子:注册全局 authenticate 拦截器(阶段 4)
* 调用方:bootstrap.php 在所有类加载完成后调用一次。
*/
public static function init(): void {
add_filter('authenticate', [self::class, 'enforce_email_verification_on_authenticate'], 30, 3);
}
/**
* 全局登录拦截器:堵住 wp-login.php / REST / XML-RPC / Application Password 等所有
* 不经过 Service::login_user() 的认证路径。
*
* 多重短路保护,按"成本最低"顺序排列:
* 1. 紧急降级常量 AKSRT_NO_EMAIL_GATE
* 2. $user 不是 \WP_User(认证失败 / 未开始)
* 3. \WP_Error(前置错误,原样返回)
* 4. 功能未启用
* 5. 管理员豁免(manage_options)
* 6. cron / 安装 / 升级豁免
* 7. REST/XML-RPC 阶段化(option aksrt_email_verification_rest_block_at 控制)
* 8. 自定义豁免过滤器
*
* @param mixed $user
* @param string $username
* @param string $password
* @return mixed
*/
public static function enforce_email_verification_on_authenticate($user, $username = '', $password = '') {
if (defined('AKSRT_NO_EMAIL_GATE') && AKSRT_NO_EMAIL_GATE) {
return $user;
}
if (!($user instanceof \WP_User)) {
return $user;
}
if (is_wp_error($user)) {
return $user;
}
if (!class_exists('\AKSRT\Settings\Settings') || !\AKSRT\Settings\Settings::registration_requires_email_verification()) {
return $user;
}
if (user_can($user, 'manage_options')) {
return $user;
}
if (defined('DOING_CRON') && DOING_CRON) {
return $user;
}
if (defined('WP_INSTALLING') && WP_INSTALLING) {
return $user;
}
$is_rest = (defined('REST_REQUEST') && REST_REQUEST);
$is_xmlrpc = (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST);
$rest_block_at = (int) get_option('aksrt_email_verification_rest_block_at', 0);
// 阶段化:option=0 时 REST/XML-RPC 仅记录不拦截(Day 0~3 灰度期)
if (($is_rest || $is_xmlrpc) && $rest_block_at <= 0) {
if (!EmailVerification::is_email_verified((int) $user->ID)) {
self::log_event('auth.rest_unverified', [
'uid' => (int) $user->ID,
'rest' => $is_rest,
'xmlrpc' => $is_xmlrpc,
]);
}
return $user;
}
// 已验证 → 直接放行
if (EmailVerification::is_email_verified((int) $user->ID)) {
return $user;
}
// 自定义豁免:filter 返回 false 即放行
$should_block = apply_filters('aksrt_email_verification_should_block', true, $user, [
'rest' => $is_rest,
'xmlrpc' => $is_xmlrpc,
]);
if (!$should_block) {
return $user;
}
self::log_event('auth.blocked', [
'uid' => (int) $user->ID,
'rest' => $is_rest,
'xmlrpc' => $is_xmlrpc,
]);
return new \WP_Error(
'aksrt_email_unverified',
__('账号尚未完成邮箱验证,请先完成邮箱验证后再登录。', 'aksrt'),
[
'verify_url' => self::get_email_verification_url((int) $user->ID),
]
);
}
/**
* 轻量事件日志(ring buffer,最近 200 条),用于健康检查路由统计。
* 不写入文件,仅写 transient(24h TTL),不影响性能。
*/
public static function log_event(string $code, array $context = []): void {
$key = 'aksrt_email_verify_log';
$logs = get_transient($key);
if (!is_array($logs)) {
$logs = [];
}
$logs[] = [
't' => time(),
'c' => $code,
'd' => $context,
];
// 保留最近 200 条
if (count($logs) > 200) {
$logs = array_slice($logs, -200);
}
set_transient($key, $logs, DAY_IN_SECONDS);
}
/**
* 读取最近 24h 的事件日志聚合。
*/
public static function get_email_verification_url(int $user_id, array $args = []): string {
if ($user_id <= 0 || !function_exists('aksrt_get_verify_email_url')) {
return '';
}
$access_key = self::get_or_issue_email_access_key($user_id);
if ($access_key !== '') {
$args['vk'] = $access_key;
}
return aksrt_get_verify_email_url($user_id, $args);
}
public static function current_registration_verification_method(): string {
if (!class_exists('\AKSRT\Settings\Settings')) {
return 'link';
}
$method = \AKSRT\Settings\Settings::registration_verification_method();
return in_array($method, ['link', 'code'], true) ? $method : 'link';
}
private static function get_or_issue_email_access_key(int $user_id, bool $rotate = false): string {
if ($user_id <= 0) {
return '';
}
$current = (string) get_user_meta($user_id, self::EMAIL_ACCESS_KEY_META, true);
if (!$rotate && $current !== '') {
return $current;
}
$key = wp_generate_password(48, false, false);
update_user_meta($user_id, self::EMAIL_ACCESS_KEY_META, $key);
return $key;
}
private static function has_valid_email_access_key(int $user_id, string $access_key): bool {
if ($user_id <= 0 || $access_key === '') {
return false;
}
$stored = (string) get_user_meta($user_id, self::EMAIL_ACCESS_KEY_META, true);
return $stored !== '' && hash_equals($stored, $access_key);
}
private static function masked_email(string $email): string {
if ($email === '' || !is_email($email) || strpos($email, '@') === false) {
return '';
}
[$local, $domain] = explode('@', $email, 2);
$local_visible = function_exists('mb_substr') ? mb_substr($local, 0, 2, 'UTF-8') : substr($local, 0, 2);
$local_masked = $local_visible . str_repeat('*', max(1, strlen($local) - strlen($local_visible)));
$domain_parts = explode('.', $domain);
$domain_head = $domain_parts[0] ?? '';
$domain_visible = substr($domain_head, 0, 1);
$domain_parts[0] = $domain_visible . str_repeat('*', max(1, strlen($domain_head) - strlen($domain_visible)));
return $local_masked . '@' . implode('.', $domain_parts);
}
public static function get_recent_log_summary(): array {
$logs = get_transient('aksrt_email_verify_log');
if (!is_array($logs)) {
return [];
}
$cutoff = time() - DAY_IN_SECONDS;
$summary = [];
foreach ($logs as $entry) {
if (!is_array($entry) || !isset($entry['t'], $entry['c'])) continue;
if ((int) $entry['t'] < $cutoff) continue;
$code = (string) $entry['c'];
$summary[$code] = ($summary[$code] ?? 0) + 1;
}
return $summary;
}
public static function get_oauth_bind_context(string $obk): ?array {
$obk = sanitize_text_field($obk);
@@ -264,6 +38,20 @@ final class Service {
public static function resolve_email_to_username(string $login): string {
$login = trim($login);
if (
preg_match('/^1[3-9]\d{9}$/', $login)
&& class_exists('\AKSRT\Settings\Settings')
&& \AKSRT\Settings\Settings::signin_phone_enabled()
) {
$users = get_users([
'meta_key' => 'aksrt_phone',
'meta_value' => $login,
'number' => 1,
]);
if (!empty($users[0]) && $users[0] instanceof \WP_User) {
return $users[0]->user_login;
}
}
if (is_email($login)) {
$user = get_user_by('email', $login);
if ($user && !is_wp_error($user)) {
@@ -319,23 +107,11 @@ final class Service {
return true;
}
public static function login_user(array $post, bool $is_ajax = false, bool $skip_nonce = false): array {
public static function login_user(array $post, string $captcha_scene = 'login'): array {
$resolved_login = self::resolve_email_to_username((string) ($post['log'] ?? ''));
if (!$skip_nonce) {
if ($is_ajax) {
if (!wp_verify_nonce($post['_ajax_nonce'] ?? '', 'aksrt_ajax_login')) {
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
}
} else {
if (!wp_verify_nonce($post['_login_nonce'] ?? '', 'aksrt_login')) {
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
}
}
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('login', $post);
$captcha_result = aksrt_require_captcha($captcha_scene, $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
}
@@ -362,20 +138,12 @@ final class Service {
// REST API 上下文中 wp_signon 可能无法正确设置 Cookie
// 手动调用 wp_set_auth_cookie 确保 Cookie 被设置
if (defined('REST_REQUEST') && REST_REQUEST) {
wp_set_auth_cookie($user->ID, true);
wp_set_auth_cookie($user->ID, !empty($post['rememberme']));
wp_set_current_user($user->ID);
}
if (self::user_requires_email_verification($user)) {
wp_logout();
return [
'success' => false,
'error' => __('账号尚未完成邮箱验证,请先验证邮箱后再登录', 'aksrt'),
'verify_url' => self::get_email_verification_url((int) $user->ID),
];
}
$redirect = !empty($post['redirect_to']) ? wp_unslash((string) $post['redirect_to']) : home_url('/');
$redirect = wp_validate_redirect($redirect, home_url('/'));
return [
'success' => true,