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

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

+1 -8
View File
@@ -167,14 +167,7 @@ class EmailRollback {
if (count($log) > 10) $log = array_slice($log, -10);
update_user_meta($user_id, 'aksrt_email_verification_admin_log', $log);
if (class_exists('AKSRT\\Auth\\Service')) {
Service::log_event('admin.email_rollback', [
'uid' => $user_id,
'by' => $by_admin_id ?: get_current_user_id(),
]);
}
return [
return [
'success' => true,
'message' => sprintf(__('已将用户邮箱回滚为 %s', 'aksrt'), $previous),
];
-523
View File
@@ -1,523 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Auth;
defined('ABSPATH') || exit;
class EmailVerification {
public static function user_requires_email_verification($user): bool {
if (!$user instanceof \WP_User) {
return false;
}
if (!\AKSRT\Settings\Settings::registration_requires_email_verification()) {
return false;
}
return !self::is_email_verified((int) $user->ID);
}
public static function is_email_verified(int $user_id): bool {
return (int) get_user_meta($user_id, Service::EMAIL_VERIFIED_META, true) === 1;
}
public static function mark_email_verified(int $user_id): void {
$was_verified = (int) get_user_meta($user_id, Service::EMAIL_VERIFIED_META, true) === 1;
update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 1);
self::clear_email_verification($user_id);
// 仅在状态由"未验证 → 已验证"跳变时触发,避免历史 backfill / 重复调用造成奖励重发。
// 监听方负责自己幂等(Ref 关系使用原子奖励状态处理重复事件)。
if (!$was_verified) {
do_action('aksrt_email_verified', $user_id);
}
}
public static function clear_email_verification(int $user_id): void {
delete_user_meta($user_id, Service::EMAIL_METHOD_META);
delete_user_meta($user_id, Service::EMAIL_TOKEN_HASH_META);
delete_user_meta($user_id, Service::EMAIL_CODE_HASH_META);
delete_user_meta($user_id, Service::EMAIL_EXPIRES_META);
delete_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META);
delete_user_meta($user_id, Service::EMAIL_TARGET_META);
delete_user_meta($user_id, Service::EMAIL_OBK_META);
self::reset_email_code_attempt_trackers($user_id);
}
// ============================================================
// 公开 API:管理员后台 / 内部业务调用
// ============================================================
/**
* 是否已验证邮箱(公开版本,给后台 UI / 用户列表渲染用)。
*/
public static function user_is_email_verified(int $user_id): bool {
return self::is_email_verified($user_id);
}
/**
* 取一个用户当前的验证状态摘要,用于后台资料页展示。
*
* @return array{verified:bool, method:string, target_email:string, expires_at:int, requested_at:int, last_admin_log:array}
*/
public static function get_user_verification_state(int $user_id): array {
return [
'verified' => self::is_email_verified($user_id),
'method' => (string) get_user_meta($user_id, Service::EMAIL_METHOD_META, true),
'target_email' => (string) get_user_meta($user_id, Service::EMAIL_TARGET_META, true),
'expires_at' => (int) get_user_meta($user_id, Service::EMAIL_EXPIRES_META, true),
'requested_at' => (int) get_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META, true),
'last_admin_log' => (array) (get_user_meta($user_id, 'aksrt_email_verification_admin_log', true) ?: []),
];
}
/**
* 管理员:把指定用户标记为已验证(清除所有进行中的验证 meta)。
*/
public static function admin_mark_verified(int $user_id, int $by_admin_id = 0): array {
if ($user_id <= 0) {
return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
}
if (!current_user_can('edit_user', $user_id)) {
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
}
self::mark_email_verified($user_id);
self::record_admin_action($user_id, 'mark_verified', $by_admin_id);
Service::log_event('admin.action', ['action' => 'mark_verified', 'uid' => $user_id, 'by' => $by_admin_id]);
return ['success' => true, 'message' => __('已标记为已验证', 'aksrt')];
}
/**
* 管理员:撤销已验证状态(用于测试 / 回滚 / 应对邮箱泄漏)。
*/
public static function admin_unmark_verified(int $user_id, int $by_admin_id = 0): array {
if ($user_id <= 0) {
return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
}
if (!current_user_can('edit_user', $user_id)) {
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
}
update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 0);
self::clear_email_verification($user_id);
self::record_admin_action($user_id, 'unmark_verified', $by_admin_id);
Service::log_event('admin.action', ['action' => 'unmark_verified', 'uid' => $user_id, 'by' => $by_admin_id]);
return ['success' => true, 'message' => __('已撤销验证状态', 'aksrt')];
}
/**
* 管理员:立即发送 / 重发验证邮件(绕过 60s 节流)。
*/
public static function admin_send_verification(int $user_id, int $by_admin_id = 0): array {
if ($user_id <= 0) {
return ['success' => false, 'error' => __('无效的用户 ID', 'aksrt')];
}
if (!current_user_can('edit_user', $user_id)) {
return ['success' => false, 'error' => __('权限不足', 'aksrt')];
}
$user = get_userdata($user_id);
if (!$user || is_wp_error($user)) {
return ['success' => false, 'error' => __('用户不存在', 'aksrt')];
}
$method = Service::current_registration_verification_method();
$email = (string) get_user_meta($user_id, Service::EMAIL_TARGET_META, true);
if (!in_array($method, ['link', 'code'], true)) {
$method = \AKSRT\Settings\Settings::registration_verification_method();
}
if ($email === '' || !is_email($email)) {
$email = (string) $user->user_email;
}
$result = self::begin_email_verification($user, $method, [
'email' => $email,
'obk' => (string) get_user_meta($user_id, Service::EMAIL_OBK_META, true),
'bypass_throttle' => true,
'force' => true,
]);
// 标记为待验证(确保 admin 主动发邮件后用户被视为未验证)
if (!empty($result['success'])) {
update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 0);
self::record_admin_action($user_id, 'send_verification', $by_admin_id);
Service::log_event('admin.action', ['action' => 'send_verification', 'uid' => $user_id, 'by' => $by_admin_id]);
}
return $result;
}
/**
* 把已存在的用户视为受信任(grandfather):写入 aksrt_email_verified=1。
*
* 仅写入 meta 不存在的用户;已经显式为 0 的(正在走验证流程的新注册)保持不动。
*
* 用户量 > 50000 时改为异步分批;否则内联完成。返回处理总数。
*
* @param bool $force 强制再次执行(即便 backfill_done 标记已存在),调试用
*/
public static function backfill_legacy_users_as_verified(bool $force = false): int {
if (!$force && get_option('aksrt_email_verification_backfill_done')) {
return 0;
}
$total = 0;
if (function_exists('count_users')) {
$counts = count_users();
$total = (int) ($counts['total_users'] ?? 0);
}
// 大表保护:超过 5w 用户改为后台 cron 分批
if ($total > 50000) {
if (!wp_next_scheduled('aksrt_email_verification_backfill_batch')) {
wp_schedule_single_event(time() + 30, 'aksrt_email_verification_backfill_batch', [0]);
}
update_option('aksrt_email_verification_backfill_log', [
'mode' => 'async',
'total' => $total,
'scheduled' => time(),
'by' => get_current_user_id(),
]);
return 0;
}
// 内联分批,每批 500
$updated = 0;
$paged = 1;
do {
$users = get_users([
'number' => 500,
'paged' => $paged,
'fields' => ['ID'],
'meta_query' => [
[
'key' => Service::EMAIL_VERIFIED_META,
'compare' => 'NOT EXISTS',
],
],
]);
if (empty($users)) {
break;
}
foreach ($users as $u) {
update_user_meta((int) $u->ID, Service::EMAIL_VERIFIED_META, 1);
$updated++;
}
$paged++;
// 让 MySQL 喘口气
if (function_exists('usleep')) {
usleep(50000);
}
} while (count($users) === 500 && $paged < 200); // 上限 10w 兜底防呆
update_option('aksrt_email_verification_backfill_done', time());
update_option('aksrt_email_verification_backfill_log', [
'mode' => 'inline',
'count' => $updated,
'ran_at' => time(),
'by' => get_current_user_id(),
]);
Service::log_event('admin.backfill', ['count' => $updated, 'by' => get_current_user_id()]);
return $updated;
}
/**
* 异步分批回填:cron 钩子。每批 500,自动调度下一批直到完成。
*/
public static function run_async_backfill_batch(int $offset = 0): void {
$users = get_users([
'number' => 500,
'offset' => max(0, $offset),
'fields' => ['ID'],
'orderby' => 'ID',
'order' => 'ASC',
'meta_query' => [
[
'key' => Service::EMAIL_VERIFIED_META,
'compare' => 'NOT EXISTS',
],
],
]);
if (empty($users)) {
update_option('aksrt_email_verification_backfill_done', time());
$log = (array) get_option('aksrt_email_verification_backfill_log', []);
$log['finished_at'] = time();
update_option('aksrt_email_verification_backfill_log', $log);
Service::log_event('admin.backfill.finished', []);
return;
}
foreach ($users as $u) {
update_user_meta((int) $u->ID, Service::EMAIL_VERIFIED_META, 1);
}
// 调度下一批
wp_schedule_single_event(time() + 30, 'aksrt_email_verification_backfill_batch', [$offset + 500]);
}
/**
* 记录管理员操作日志(每个用户最多 10 条 ring buffer)。
*/
public static function record_admin_action(int $user_id, string $action, int $by_admin_id): void {
$log = get_user_meta($user_id, 'aksrt_email_verification_admin_log', true);
if (!is_array($log)) {
$log = [];
}
$log[] = [
't' => time(),
'action' => $action,
'by' => $by_admin_id ?: get_current_user_id(),
];
if (count($log) > 10) {
$log = array_slice($log, -10);
}
update_user_meta($user_id, 'aksrt_email_verification_admin_log', $log);
}
// ============================================================
// 验证码失败计数 / IP 节流
// ============================================================
public static function email_code_ip_throttle_key(int $user_id): string {
$ip = sanitize_text_field((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
if ($user_id <= 0 || $ip === '') {
return '';
}
return 'aksrt_email_code_try_' . $user_id . '_' . md5($ip);
}
public static function reset_email_code_attempt_trackers(int $user_id): void {
delete_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META);
$ip_key = self::email_code_ip_throttle_key($user_id);
if ($ip_key !== '') {
delete_transient($ip_key);
}
}
public static function fail_email_code_attempt(int $user_id): array {
$attempts = (int) get_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META, true);
$attempts++;
update_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META, $attempts);
$ip_attempts = 0;
$ip_key = self::email_code_ip_throttle_key($user_id);
if ($ip_key !== '') {
$ip_attempts = (int) get_transient($ip_key);
$ip_attempts++;
set_transient($ip_key, $ip_attempts, Service::EMAIL_CODE_IP_TTL);
}
if ($attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS || $ip_attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS) {
delete_user_meta($user_id, Service::EMAIL_CODE_HASH_META);
update_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META, 0);
Service::log_event('verify.code.locked', ['uid' => $user_id, 'attempts' => $attempts]);
return ['success' => false, 'error' => __('验证码错误次数过多,当前验证码已失效,请重新发送。', 'aksrt')];
}
Service::log_event('verify.code.fail', ['uid' => $user_id, 'attempts' => $attempts]);
return ['success' => false, 'error' => __('邮箱验证码错误或已过期,请重新输入。', 'aksrt')];
}
// ============================================================
// 验证流程:发起 / 校验 / 完成
// ============================================================
public static function begin_email_verification(\WP_User $user, string $method, array $args = []): array {
$method = in_array($method, ['link', 'code'], true) ? $method : 'link';
$email = sanitize_email((string) ($args['email'] ?? $user->user_email));
$obk = sanitize_text_field((string) ($args['obk'] ?? ''));
$bypass_throttle = !empty($args['bypass_throttle']);
if ($email === '' || !is_email($email)) {
return ['success' => false, 'error' => __('当前账号缺少有效邮箱,无法发送验证邮件', 'aksrt')];
}
// 节流:仅在非管理员路径生效(admin_send_verification 会传 bypass_throttle=true)
if (!$bypass_throttle) {
$last_requested = (int) get_user_meta($user->ID, Service::EMAIL_REQUESTED_AT_META, true);
if ($last_requested > 0 && (time() - $last_requested) < 60 && empty($args['force'])) {
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
}
}
$expires_at = time() + (\AKSRT\Settings\Settings::registration_verification_ttl_minutes() * MINUTE_IN_SECONDS);
update_user_meta($user->ID, Service::EMAIL_VERIFIED_META, 0);
update_user_meta($user->ID, Service::EMAIL_METHOD_META, $method);
update_user_meta($user->ID, Service::EMAIL_TARGET_META, $email);
update_user_meta($user->ID, Service::EMAIL_EXPIRES_META, $expires_at);
update_user_meta($user->ID, Service::EMAIL_REQUESTED_AT_META, time());
if ($obk !== '') {
update_user_meta($user->ID, Service::EMAIL_OBK_META, $obk);
}
delete_user_meta($user->ID, Service::EMAIL_TOKEN_HASH_META);
delete_user_meta($user->ID, Service::EMAIL_CODE_HASH_META);
if ($method === 'code') {
$code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
update_user_meta($user->ID, Service::EMAIL_CODE_HASH_META, wp_hash_password($code));
// 新验证码 = 重置失败计数 / IP 节流,避免被旧计数立即锁死
self::reset_email_code_attempt_trackers((int) $user->ID);
$sent = self::send_registration_verification_code_email($user, $email, $code);
} else {
$token = wp_generate_password(48, false, false);
update_user_meta($user->ID, Service::EMAIL_TOKEN_HASH_META, hash('sha256', $token));
$sent = self::send_registration_verification_link_email($user, $email, $token);
}
if (!$sent) {
self::clear_email_verification((int) $user->ID);
Service::log_event('email.send.fail', ['uid' => (int) $user->ID, 'method' => $method]);
return ['success' => false, 'error' => __('验证邮件发送失败,请稍后重试', 'aksrt')];
}
Service::log_event('email.send.ok', ['uid' => (int) $user->ID, 'method' => $method]);
$message = $method === 'code'
? __('注册成功,验证码已发送到你的邮箱,请完成验证后继续。', 'aksrt')
: __('注册成功,验证邮件已发送到你的邮箱,请点击邮件中的链接完成验证。', 'aksrt');
return [
'success' => true,
'message' => $message,
];
}
public static function verify_email_token(int $user_id, string $token): array {
$user = get_userdata($user_id);
if (!$user || is_wp_error($user)) {
return ['success' => false, 'error' => __('验证账号不存在或已失效', 'aksrt')];
}
$stored_hash = (string) get_user_meta($user_id, Service::EMAIL_TOKEN_HASH_META, true);
$expires_at = (int) get_user_meta($user_id, Service::EMAIL_EXPIRES_META, true);
if ($stored_hash === '' || $expires_at <= time() || !hash_equals($stored_hash, hash('sha256', $token))) {
return ['success' => false, 'error' => __('验证链接无效或已过期,请重新发送验证邮件。', 'aksrt')];
}
return self::complete_email_verification($user);
}
public static function verify_email_code(int $user_id, string $code): array {
$user = get_userdata($user_id);
if (!$user || is_wp_error($user)) {
return ['success' => false, 'error' => __('验证账号不存在或已失效', 'aksrt')];
}
// 先做 IP 节流:相同 IP 在 EMAIL_CODE_IP_TTL 内累计失败到上限即直接拒绝
$ip_key = self::email_code_ip_throttle_key($user_id);
if ($ip_key !== '') {
$ip_attempts = (int) get_transient($ip_key);
if ($ip_attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS) {
Service::log_event('verify.code.ip_throttled', ['uid' => $user_id, 'attempts' => $ip_attempts]);
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
}
}
// 用户级失败计数:达到上限后该验证码立即作废
$user_attempts = (int) get_user_meta($user_id, Service::EMAIL_CODE_ATTEMPTS_META, true);
if ($user_attempts >= Service::EMAIL_CODE_MAX_ATTEMPTS) {
delete_user_meta($user_id, Service::EMAIL_CODE_HASH_META);
update_user_meta($user_id, Service::EMAIL_REQUESTED_AT_META, 0);
Service::log_event('verify.code.locked.read', ['uid' => $user_id]);
return ['success' => false, 'error' => __('验证码错误次数过多,当前验证码已失效,请重新发送。', 'aksrt')];
}
$stored_hash = (string) get_user_meta($user_id, Service::EMAIL_CODE_HASH_META, true);
$expires_at = (int) get_user_meta($user_id, Service::EMAIL_EXPIRES_META, true);
if ($stored_hash === '' || $expires_at <= time() || !wp_check_password($code, $stored_hash)) {
return self::fail_email_code_attempt($user_id);
}
self::reset_email_code_attempt_trackers($user_id);
return self::complete_email_verification($user);
}
public static function complete_email_verification(\WP_User $user): array {
$user_id = (int) $user->ID;
$obk = (string) get_user_meta($user_id, Service::EMAIL_OBK_META, true);
$oauth = $obk !== '' ? Service::get_oauth_bind_context($obk) : null;
$email = (string) get_user_meta($user_id, Service::EMAIL_TARGET_META, true);
$was_verified = (int) get_user_meta($user_id, Service::EMAIL_VERIFIED_META, true) === 1;
update_user_meta($user_id, Service::EMAIL_VERIFIED_META, 1);
self::clear_email_verification($user_id);
Service::log_event('verify.success', ['uid' => $user_id]);
// 状态跳变时触发 hook,Ref 推广奖励等模块据此发奖
if (!$was_verified) {
do_action('aksrt_email_verified', $user_id);
}
if ($email !== '' && is_email($email)) {
self::send_registration_verified_notice_email($user, $email);
}
if ($oauth && is_array($oauth)) {
Service::finalize_oauth_registration($user_id, $oauth, $obk);
} elseif (\AKSRT\Settings\Settings::registration_auto_login_after_verify()) {
wp_set_current_user($user_id);
wp_set_auth_cookie($user_id, true);
}
$redirect = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/');
if (!\AKSRT\Settings\Settings::registration_auto_login_after_verify()) {
$redirect = aksrt_get_login_url();
}
return [
'success' => true,
'message' => __('邮箱验证成功,即将进入你的账号。', 'aksrt'),
'redirect' => $redirect,
];
}
// ============================================================
// 通知邮件
// ============================================================
public static function send_registration_verification_link_email(\WP_User $user, string $email, string $token): bool {
$url = Service::get_email_verification_url((int) $user->ID, ['token' => $token]);
$site_name = get_bloginfo('name');
$default_subject = '[' . $site_name . '] ' . __('验证你的邮箱地址', 'aksrt');
$default_content = sprintf(
__("您好 %s,\n\n欢迎注册 %s。\n\n请点击以下链接完成邮箱验证:\n\n%s\n\n验证成功后系统将自动为你登录。\n\n如果这不是你的操作,请忽略此邮件。", 'aksrt'),
$user->user_login, $site_name, $url
);
return \AKSRT\Settings\Settings::send_event_email(
'reg_verify_link', $email, $default_subject, $default_content,
['{username}' => $user->user_login, '{site_name}' => $site_name, '{url}' => $url]
);
}
public static function send_registration_verification_code_email(\WP_User $user, string $email, string $code): bool {
$site_name = get_bloginfo('name');
$default_subject = '[' . $site_name . '] ' . __('邮箱验证码', 'aksrt');
$default_content = sprintf(
__("您好 %s,\n\n欢迎注册 %s。\n\n你的邮箱验证码为:\n\n【%s】\n\n验证码验证成功后系统将自动为你登录。\n\n如果这不是你的操作,请忽略此邮件。", 'aksrt'),
$user->user_login, $site_name, $code
);
return \AKSRT\Settings\Settings::send_event_email(
'reg_verify_code', $email, $default_subject, $default_content,
['{username}' => $user->user_login, '{site_name}' => $site_name, '{code}' => $code]
);
}
public static function send_registration_verified_notice_email(\WP_User $user, string $email): bool {
$site_name = get_bloginfo('name');
$default_subject = '[' . $site_name . '] ' . __('邮箱验证成功', 'aksrt');
$default_content = sprintf(
__("您好 %s,\n\n你在 %s 的账号邮箱已经验证成功。\n\n如果这不是你的操作,请立即联系站点管理员。", 'aksrt'),
$user->user_login, $site_name
);
return \AKSRT\Settings\Settings::send_event_email(
'reg_success', $email, $default_subject, $default_content,
['{username}' => $user->user_login, '{site_name}' => $site_name]
);
}
}
+13 -34
View File
@@ -37,19 +37,14 @@ final class Router {
public function register_query_vars(array $vars): array {
$vars[] = 'aksrt_sign';
$vars[] = 'aksrt_auth_tab';
$vars[] = 'aksrt_logout';
return $vars;
}
private function auth_route_map(): array {
return [
'login' => 'index.php?aksrt_sign=1&aksrt_auth_tab=login',
'register' => 'index.php?aksrt_sign=1&aksrt_auth_tab=register',
'bind-account' => 'index.php?aksrt_sign=1&aksrt_auth_tab=bind',
'sign' => 'index.php?aksrt_sign=1',
'logout' => 'index.php?aksrt_logout=1',
'reset-password' => 'index.php?aksrt_sign=1&aksrt_auth_tab=reset-password',
'verify-email' => 'index.php?aksrt_sign=1&aksrt_auth_tab=verify-email',
];
}
@@ -63,8 +58,7 @@ final class Router {
if (get_query_var('aksrt_sign')) {
// §4.1 Global sign gate: when close_sign is active, block non-logged-in access to sign page
if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::close_sign() && !is_user_logged_in()) {
$tab = (string) get_query_var('aksrt_auth_tab');
if (!in_array($tab, ['reset-password', 'verify-email', 'bind-account'], true) && empty($_REQUEST['interim-login'])) {
if (empty($_REQUEST['interim-login'])) {
if (!headers_sent()) {
wp_safe_redirect(home_url('/'), 302);
exit;
@@ -72,10 +66,7 @@ final class Router {
}
}
$auth_template = function_exists('aksrt_get_auth_template_file')
? aksrt_get_auth_template_file((string) get_query_var('aksrt_auth_tab'), true)
: 'templates/pages/page-sign.php';
$tpl = aksrt_locate_template_file($auth_template);
$tpl = aksrt_locate_template_file('templates/pages/page-sign.php');
if ($tpl) {
return $tpl;
}
@@ -85,7 +76,7 @@ final class Router {
}
public function maybe_flush_rewrite_rules(): void {
$rewrite_version = AKSRT_VERSION . ':sign-router-v2-logout';
$rewrite_version = AKSRT_VERSION . ':sign-router-v3-single-route';
if (get_option('aksrt_auth_routes_flushed') !== $rewrite_version) {
flush_rewrite_rules(false);
update_option('aksrt_auth_routes_flushed', $rewrite_version);
@@ -125,7 +116,7 @@ final class Router {
}
public function redirect_native_login(): void {
if (wp_doing_ajax()) {
if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::replace_wp_login()) {
return;
}
@@ -139,31 +130,26 @@ final class Router {
$action = isset($_REQUEST['action']) ? sanitize_key((string) $_REQUEST['action']) : 'login';
$redirect_to = isset($_REQUEST['redirect_to']) ? wp_unslash((string) $_REQUEST['redirect_to']) : '';
$interim = !empty($_REQUEST['interim-login']);
$reauth = !empty($_REQUEST['reauth']);
$is_admin_target = $redirect_to !== '' && strpos($redirect_to, admin_url()) === 0;
if (is_user_logged_in() && current_user_can('manage_options')) {
return;
}
if ($is_admin_target || $interim || $reauth || $action === 'logout') {
return;
if ($action === 'logout') {
wp_safe_redirect(aksrt_get_logout_url($redirect_to ?: home_url('/')), 302);
exit;
}
switch ($action) {
case 'register':
$target = aksrt_get_register_url();
$target = aksrt_get_sign_page_url('signup');
break;
case 'lostpassword':
$target = aksrt_get_reset_password_url();
$target = aksrt_get_sign_page_url('resetpassword');
break;
case 'rp':
case 'resetpass':
$target = add_query_arg('legacy', '1', aksrt_get_reset_password_url());
$target = aksrt_get_sign_page_url('resetpassword');
break;
default:
$target = aksrt_get_login_url();
$target = aksrt_get_sign_page_url('signin');
break;
}
@@ -215,16 +201,9 @@ final class Router {
}
/**
* Detect logout via rewrite query var, GET flag, or pretty path.
* Detect the unique public logout path.
*/
private function is_logout_request(): bool {
if ((string) get_query_var('aksrt_logout') !== '' && (string) get_query_var('aksrt_logout') !== '0') {
return true;
}
if (isset($_GET['aksrt_logout']) && (string) $_GET['aksrt_logout'] !== '' && (string) $_GET['aksrt_logout'] !== '0') {
return true;
}
$request_uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
if ($request_uri === '') {
return false;
+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,
+63 -343
View File
@@ -10,17 +10,24 @@ defined('ABSPATH') || exit;
trait ServicePart1a {
public static function register_user(array $post, array $context = []): array {
public static function register_user(array $post): array {
$user_login = sanitize_user(wp_unslash($post['user_login'] ?? ''), true);
$user_email = sanitize_email(wp_unslash($post['user_email'] ?? ''));
$password = (string) ($post['user_pass'] ?? '');
$confirm = (string) ($post['user_pass_confirm'] ?? '');
if (!wp_verify_nonce($post['_register_nonce'] ?? '', 'aksrt_register')) {
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
if (
class_exists('\AKSRT\Settings\Settings')
&& \AKSRT\Settings\Settings::signup_verification_enabled()
&& empty($post['_auth_code_verified'])
) {
return ['success' => false, 'error' => __('请先完成邮箱或手机验证', 'aksrt')];
}
if (empty($post['agree_terms'])) {
$agreement = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::user_agreement_settings()
: ['enabled' => false, 'privacy' => false, 'required' => false];
if ((!empty($agreement['enabled']) || !empty($agreement['privacy'])) && !empty($agreement['required']) && empty($post['agree_terms'])) {
return ['success' => false, 'error' => __('请先同意服务条款', 'aksrt')];
}
@@ -31,6 +38,13 @@ trait ServicePart1a {
if (mb_strlen($user_login) < 3 || mb_strlen($user_login) > 60) {
return ['success' => false, 'error' => __('用户名长度需在 3-60 个字符之间', 'aksrt')];
}
if (class_exists('\AKSRT\Settings\Settings')) {
foreach (\AKSRT\Settings\Settings::nickname_blacklist() as $keyword) {
if ($keyword !== '' && mb_stripos($user_login, $keyword) !== false) {
return ['success' => false, 'error' => __('用户名包含禁止使用的关键词', 'aksrt')];
}
}
}
if (username_exists($user_login)) {
return ['success' => false, 'error' => __('该用户名已被使用', 'aksrt')];
@@ -39,17 +53,37 @@ trait ServicePart1a {
if (empty($user_email) || !is_email($user_email)) {
return ['success' => false, 'error' => __('请输入有效的邮箱地址', 'aksrt')];
}
if (class_exists('\AKSRT\Settings\Settings')) {
$email_limit = \AKSRT\Settings\Settings::email_limit();
$matches = false;
foreach ((array) ($email_limit['domains'] ?? []) as $keyword) {
if ($keyword !== '' && stripos($user_email, (string) $keyword) !== false) {
$matches = true;
break;
}
}
if ((($email_limit['type'] ?? 'not') === 'not' && $matches) || (($email_limit['type'] ?? 'not') === 'allow' && !empty($email_limit['domains']) && !$matches)) {
return ['success' => false, 'error' => __('该邮箱不符合本站注册规则', 'aksrt')];
}
}
if (email_exists($user_email)) {
return ['success' => false, 'error' => __('该邮箱已被其他账号使用', 'aksrt')];
}
if (
class_exists('\AKSRT\Settings\Settings')
&& \AKSRT\Settings\Settings::signup_verification_enabled()
&& \AKSRT\Settings\Settings::signup_omit_password_confirmation()
) {
$confirm = $password;
}
$password_check = self::validate_password($password, $confirm);
if ($password_check !== true) {
return ['success' => false, 'error' => $password_check];
}
if (function_exists('aksrt_require_captcha')) {
if (empty($post['_auth_code_verified']) && function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('register', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
@@ -88,6 +122,12 @@ trait ServicePart1a {
return ['success' => false, 'error' => __('推广码无效,请检查后重试', 'aksrt')];
}
}
if ($submitted_ref !== '' && empty($ref_context['valid']) && !empty($registration['reject_invalid'])) {
if ($ref_service) {
$ref_service->clear_registration_context();
}
return ['success' => false, 'error' => __('推广码无效,请检查后重试', 'aksrt')];
}
$user_id = wp_create_user($user_login, $password, $user_email);
if (is_wp_error($user_id)) {
@@ -103,115 +143,20 @@ trait ServicePart1a {
return ['success' => false, 'error' => __('注册成功,但读取用户信息失败,请联系管理员', 'aksrt')];
}
$oauth_context = isset($context['oauth_bind_data']) && is_array($context['oauth_bind_data']) ? $context['oauth_bind_data'] : [];
$oauth_verified = self::oauth_email_is_verified($oauth_context);
$requires_verify = \AKSRT\Settings\Settings::registration_requires_email_verification() && !$oauth_verified;
if (!$requires_verify) {
self::mark_email_verified($user_id);
return [
'success' => true,
'user_id' => (int) $user_id,
'user' => $user,
'user_login' => $user_login,
'notice' => $ref_notice,
];
}
$method = \AKSRT\Settings\Settings::registration_verification_method();
$begin = self::begin_email_verification(
$user,
$method,
[
'obk' => sanitize_text_field((string) ($context['obk'] ?? '')),
]
);
if (empty($begin['success'])) {
self::delete_user_account($user_id);
return $begin;
}
self::mark_email_verified($user_id);
wp_set_auth_cookie((int) $user_id, true);
do_action('wp_login', $user->user_login, $user);
return [
'success' => true,
'user_id' => (int) $user_id,
'user' => $user,
'user_login' => $user_login,
'verification_required' => true,
'verification_method' => $method,
'verify_url' => self::get_email_verification_url((int) $user_id, ['mode' => $method]),
'message' => $begin['message'] ?? '',
'notice' => $ref_notice,
'success' => true,
'user_id' => (int) $user_id,
'user' => $user,
'user_login' => $user_login,
'redirect' => home_url('/'),
'notice' => $ref_notice,
];
}
public static function bind_oauth_account(array $post, array $oauth_data): array {
if (!wp_verify_nonce($post['_bind_nonce'] ?? '', 'aksrt_bind')) {
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('bind', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
}
}
$creds = [
'user_login' => self::resolve_email_to_username((string) ($post['log'] ?? '')),
'user_password' => (string) ($post['pwd'] ?? ''),
'remember' => true,
];
$user = wp_signon($creds);
if (is_wp_error($user)) {
return ['success' => false, 'error' => self::friendly_login_error($user->get_error_message())];
}
if (self::user_requires_email_verification($user)) {
wp_logout();
return [
'success' => false,
'error' => __('该账号尚未完成邮箱验证,请先完成验证后再绑定第三方登录。', 'aksrt'),
'verify_url' => self::get_email_verification_url((int) $user->ID),
];
}
$provider = sanitize_key((string) ($oauth_data['provider'] ?? ''));
if ($provider === '') {
return ['success' => false, 'error' => __('绑定数据无效,请重新使用第三方登录', 'aksrt')];
}
$uid = (int) $user->ID;
update_user_meta($uid, "aksrt_{$provider}_id", $oauth_data['provider_id'] ?? '');
$bindings = get_user_meta($uid, 'aksrt_oauth_bindings', true) ?: [];
if (!is_array($bindings)) {
$bindings = [];
}
if (!in_array($provider, $bindings, true)) {
$bindings[] = $provider;
}
update_user_meta($uid, 'aksrt_oauth_bindings', $bindings);
if (!empty($oauth_data['user_data']['avatar_url'])) {
update_user_meta($uid, 'aksrt_oauth_avatar', esc_url_raw($oauth_data['user_data']['avatar_url']));
}
if (!empty($post['obk'])) {
delete_transient(sanitize_text_field((string) $post['obk']));
}
wp_set_auth_cookie($uid, true);
return [
'success' => true,
'user' => $user,
'provider' => $provider,
];
}
public static function finalize_oauth_registration(int $user_id, array $oauth_data, string $obk = ''): void {
public static function finalize_oauth_binding(int $user_id, array $oauth_data, string $obk = ''): void {
if ($user_id <= 0) {
return;
}
@@ -222,16 +167,17 @@ trait ServicePart1a {
}
update_user_meta($user_id, "aksrt_{$provider}_id", $oauth_data['provider_id'] ?? '');
update_user_meta($user_id, 'aksrt_oauth_bindings', [$provider]);
$bindings = get_user_meta($user_id, 'aksrt_oauth_bindings', true);
$bindings = is_array($bindings) ? $bindings : [];
if (!in_array($provider, $bindings, true)) {
$bindings[] = $provider;
}
update_user_meta($user_id, 'aksrt_oauth_bindings', array_values($bindings));
if (!empty($oauth_data['user_data']['avatar_url'])) {
update_user_meta($user_id, 'aksrt_oauth_avatar', esc_url_raw($oauth_data['user_data']['avatar_url']));
}
if (self::oauth_email_is_verified($oauth_data)) {
EmailVerification::mark_email_verified($user_id);
}
if ($obk !== '') {
delete_transient(sanitize_text_field($obk));
}
@@ -239,235 +185,9 @@ trait ServicePart1a {
wp_set_auth_cookie($user_id, true);
}
public static function get_email_verification_context(array $query): array {
$invalid_payload = [
'mode' => 'invalid',
'user' => null,
'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt'),
'method' => '',
'email' => '',
'uid' => 0,
'verified' => false,
'access_key' => '',
];
$user_id = isset($query['uid']) ? absint($query['uid']) : 0;
if ($user_id <= 0) {
return $invalid_payload;
}
$user = get_userdata($user_id);
if (!$user || is_wp_error($user)) {
return $invalid_payload;
}
$method = self::current_registration_verification_method();
$email = (string) get_user_meta($user_id, self::EMAIL_TARGET_META, true);
$token = sanitize_text_field((string) ($query['token'] ?? ''));
$access = sanitize_text_field((string) ($query['vk'] ?? ''));
// 容错:meta 缺失时(例如历史账号被强制要求验证),回退到设置默认值与 user_email
if (!in_array($method, ['link', 'code'], true)) {
$method = self::current_registration_verification_method();
}
if ($email === '' || !is_email($email)) {
$email = (string) $user->user_email;
}
$masked_email = self::masked_email($email);
$has_access = self::has_valid_email_access_key($user_id, $access);
// token 路径:链接模式直接走 token 校验;不依赖 vk
if ($token !== '') {
if (EmailVerification::is_email_verified($user_id)) {
return [
'mode' => 'invalid',
'user' => $user,
'error' => __('该验证链接已失效或已使用,请重新发起验证流程', 'aksrt'),
'method' => $has_access ? $method : '',
'email' => $has_access ? $masked_email : '',
'uid' => $has_access ? $user_id : 0,
'verified' => true,
'access_key' => $has_access ? $access : '',
];
}
$result = self::verify_email_token($user_id, $token);
if (!empty($result['success'])) {
return [
'mode' => 'completed',
'user' => $user,
'error' => '',
'method' => $method,
'email' => $masked_email,
'uid' => $user_id,
'verified' => true,
'redirect' => $result['redirect'] ?? home_url('/'),
'message' => $result['message'] ?? '',
'access_key' => $has_access ? $access : '',
];
}
return [
'mode' => 'invalid',
'user' => $user,
'error' => $result['error'] ?? __('验证链接无效或已过期', 'aksrt'),
'method' => $has_access ? $method : '',
'email' => $has_access ? $masked_email : '',
'uid' => $has_access ? $user_id : 0,
'verified' => false,
'access_key' => $has_access ? $access : '',
];
}
// 没有 token 也没有 vk:返回完全脱敏的通用错误,避免根据 uid 枚举账号状态
if (!$has_access) {
return $invalid_payload;
}
if (!self::user_requires_email_verification($user)) {
return [
'mode' => 'verified',
'user' => $user,
'error' => '',
'method' => '',
'email' => self::masked_email((string) $user->user_email),
'uid' => $user_id,
'verified' => true,
'access_key' => $access,
];
}
return [
'mode' => $method === 'code' ? 'code' : 'pending',
'user' => $user,
'error' => '',
'method' => $method,
'email' => $masked_email ?: self::masked_email((string) $user->user_email),
'uid' => $user_id,
'verified' => false,
'access_key' => $access,
];
}
public static function verify_email_code_submission(array $post): array {
if (!wp_verify_nonce($post['_verify_email_nonce'] ?? '', 'aksrt_verify_email')) {
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
}
$user_id = isset($post['verify_uid']) ? absint($post['verify_uid']) : 0;
$access = sanitize_text_field((string) ($post['vk'] ?? ''));
$code = sanitize_text_field((string) ($post['email_code'] ?? ''));
if ($user_id <= 0 || $code === '') {
return ['success' => false, 'error' => __('请输入完整的邮箱验证码', 'aksrt')];
}
if (!self::has_valid_email_access_key($user_id, $access)) {
return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('verify_email', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
}
}
return self::verify_email_code($user_id, $code);
}
public static function resend_email_verification(array $post): array {
if (!wp_verify_nonce($post['_verify_email_nonce'] ?? '', 'aksrt_verify_email')) {
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
}
$user_id = isset($post['verify_uid']) ? absint($post['verify_uid']) : 0;
$access = sanitize_text_field((string) ($post['vk'] ?? ''));
// 必须先持有有效 vk 才允许重发,避免未登录用户用 uid 枚举触发邮件
if ($user_id <= 0 || !self::has_valid_email_access_key($user_id, $access)) {
return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
}
// IP 节流:每 IP 每 RESEND_IP_WINDOW 秒最多 RESEND_IP_MAX 次(跨账号合并计数)
$ip = sanitize_text_field((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
if ($ip !== '') {
$ip_key = 'aksrt_email_resend_ip_' . md5($ip);
$ip_attempts = (int) get_transient($ip_key);
if ($ip_attempts >= self::RESEND_IP_MAX) {
self::log_event('verify.resend.ip_throttled', ['uid' => $user_id, 'ip' => $ip]);
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
}
set_transient($ip_key, $ip_attempts + 1, self::RESEND_IP_WINDOW);
}
$user = get_userdata($user_id);
if (!$user || is_wp_error($user)) {
return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')];
}
if (!self::user_requires_email_verification($user)) {
return ['success' => false, 'error' => __('该账号已完成邮箱验证,无需重复发送', 'aksrt')];
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('verify_email', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
}
}
$method = self::current_registration_verification_method();
$email = (string) get_user_meta($user_id, self::EMAIL_TARGET_META, true);
// 容错:meta 缺失时回退到设置默认值与 user_email(兼容历史账号 / meta 被外部清理的情况)
if (!in_array($method, ['link', 'code'], true)) {
$method = \AKSRT\Settings\Settings::registration_verification_method();
}
if ($email === '' || !is_email($email)) {
$email = (string) $user->user_email;
}
if ($email === '' || !is_email($email)) {
return ['success' => false, 'error' => __('当前账号缺少有效邮箱,请联系管理员', 'aksrt')];
}
$requested_at = (int) get_user_meta($user_id, self::EMAIL_REQUESTED_AT_META, true);
if ($requested_at > 0 && (time() - $requested_at) < 60) {
return ['success' => false, 'error' => __('操作过于频繁,请稍后再试', 'aksrt')];
}
return self::begin_email_verification($user, $method, [
'email' => $email,
'obk' => (string) get_user_meta($user_id, self::EMAIL_OBK_META, true),
'force' => true,
]);
}
private static function user_requires_email_verification($user): bool {
return EmailVerification::user_requires_email_verification($user);
}
private static function mark_email_verified(int $user_id): void {
EmailVerification::mark_email_verified($user_id);
}
// ============================================================
// 公开 API:管理员后台 / 内部业务调用(委托 EmailVerification)
// ============================================================
public static function user_is_email_verified(int $user_id): bool {
return EmailVerification::user_is_email_verified($user_id);
}
public static function get_user_verification_state(int $user_id): array {
return EmailVerification::get_user_verification_state($user_id);
}
public static function admin_mark_verified(int $user_id, int $by_admin_id = 0): array {
return EmailVerification::admin_mark_verified($user_id, $by_admin_id);
}
public static function admin_unmark_verified(int $user_id, int $by_admin_id = 0): array {
return EmailVerification::admin_unmark_verified($user_id, $by_admin_id);
update_user_meta($user_id, self::EMAIL_VERIFIED_META, '1');
do_action('aksrt_email_verified', $user_id);
}
}
+36 -351
View File
@@ -9,227 +9,6 @@ defined('ABSPATH') || exit;
trait ServicePart1b {
public static function admin_send_verification(int $user_id, int $by_admin_id = 0): array {
return EmailVerification::admin_send_verification($user_id, $by_admin_id);
}
public static function backfill_legacy_users_as_verified(bool $force = false): int {
return EmailVerification::backfill_legacy_users_as_verified($force);
}
public static function run_async_backfill_batch(int $offset = 0): void {
EmailVerification::run_async_backfill_batch($offset);
}
private static function delete_user_account(int $user_id): void {
if ($user_id <= 0) {
return;
}
if (!function_exists('wp_delete_user')) {
require_once ABSPATH . 'wp-admin/includes/user.php';
}
if (function_exists('wp_delete_user')) {
wp_delete_user($user_id);
}
}
private static function oauth_email_is_verified(array $oauth_data): bool {
$user_data = isset($oauth_data['user_data']) && is_array($oauth_data['user_data']) ? $oauth_data['user_data'] : $oauth_data;
if (empty($user_data['email'])) {
return false;
}
if (!empty($user_data['email_verified'])) {
return true;
}
$provider = sanitize_key((string) ($user_data['provider'] ?? $oauth_data['provider'] ?? ''));
if ($provider === 'google' && !empty($user_data['email'])) {
return true;
}
if ($provider === 'github' && isset($user_data['email_verified'])) {
return !empty($user_data['email_verified']);
}
return false;
}
private static function begin_email_verification(\WP_User $user, string $method, array $args = []): array {
return EmailVerification::begin_email_verification($user, $method, $args);
}
private static function verify_email_token(int $user_id, string $token): array {
return EmailVerification::verify_email_token($user_id, $token);
}
private static function verify_email_code(int $user_id, string $code): array {
return EmailVerification::verify_email_code($user_id, $code);
}
public static function get_reset_context(array $query): array {
if (!empty($query['key']) || !empty($query['login']) || !empty($query['legacy'])) {
return [
'mode' => 'invalid',
'user' => null,
'error' => __('旧的重置密码链接已失效,请重新申请重置密码。', 'aksrt'),
'token' => '',
'user_id'=> 0,
];
}
$token = sanitize_text_field($query['token'] ?? '');
$user_id = isset($query['uid']) ? absint($query['uid']) : 0;
if ($token === '' || $user_id <= 0) {
return [
'mode' => 'request',
'user' => null,
'error' => '',
'token' => '',
'user_id'=> 0,
];
}
$user = self::validate_reset_token($user_id, $token);
if (is_wp_error($user)) {
return [
'mode' => 'invalid',
'user' => null,
'error' => $user->get_error_message(),
'token' => $token,
'user_id'=> $user_id,
];
}
return [
'mode' => 'reset',
'user' => $user,
'error' => '',
'token' => $token,
'user_id'=> $user_id,
];
}
public static function request_password_reset(array $post): array {
if (!wp_verify_nonce($post['_reset_nonce'] ?? '', 'aksrt_reset_password')) {
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('lostpassword', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
}
}
$raw_login = trim((string) ($post['user_login'] ?? ''));
if ($raw_login === '') {
return ['success' => false, 'error' => __('请输入邮箱地址或用户名', 'aksrt')];
}
$user = null;
if (is_email($raw_login)) {
$user = get_user_by('email', $raw_login);
} else {
$user = get_user_by('login', sanitize_user($raw_login, true));
}
if (!$user || is_wp_error($user)) {
return ['success' => true, 'message' => __('如果该账号存在,重置邮件已发送到注册邮箱', 'aksrt')];
}
$token = self::create_reset_token($user->ID);
$sent = self::send_password_reset_email($user, $token);
if (!$sent) {
return ['success' => false, 'error' => __('重置邮件发送失败,请稍后再试', 'aksrt')];
}
return ['success' => true, 'message' => __('如果该账号存在,重置邮件已发送到注册邮箱', 'aksrt')];
}
public static function reset_password_with_token(array $post): array {
if (!wp_verify_nonce($post['_setpw_nonce'] ?? '', 'aksrt_set_password')) {
return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')];
}
$token = sanitize_text_field($post['reset_token'] ?? '');
$user_id = isset($post['reset_uid']) ? absint($post['reset_uid']) : 0;
$pass1 = (string) ($post['pass1'] ?? '');
$pass2 = (string) ($post['pass2'] ?? '');
$user = self::validate_reset_token($user_id, $token);
if (is_wp_error($user)) {
return ['success' => false, 'error' => $user->get_error_message(), 'mode' => 'invalid'];
}
$password_check = self::validate_password($pass1, $pass2);
if ($password_check !== true) {
return ['success' => false, 'error' => $password_check, 'mode' => 'reset'];
}
wp_set_password($pass1, $user_id);
self::clear_reset_token($user_id);
return ['success' => true, 'message' => __('密码已设置成功!', 'aksrt')];
}
private static function create_reset_token(int $user_id): string {
$token = wp_generate_password(48, false, false);
update_user_meta($user_id, self::RESET_TOKEN_HASH_META, hash('sha256', $token));
update_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META, time() + self::RESET_TOKEN_TTL);
update_user_meta($user_id, self::RESET_TOKEN_REQUESTED_AT, time());
return $token;
}
private static function validate_reset_token(int $user_id, string $token) {
if ($user_id <= 0 || $token === '') {
return new \WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
}
$user = get_userdata($user_id);
if (!$user || is_wp_error($user)) {
return new \WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
}
$stored_hash = (string) get_user_meta($user_id, self::RESET_TOKEN_HASH_META, true);
$expires_at = (int) get_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META, true);
$token_hash = hash('sha256', $token);
if ($stored_hash === '' || $expires_at <= time() || !hash_equals($stored_hash, $token_hash)) {
return new \WP_Error('invalid_reset_token', __('重置链接无效或已过期', 'aksrt'));
}
return $user;
}
private static function clear_reset_token(int $user_id): void {
delete_user_meta($user_id, self::RESET_TOKEN_HASH_META);
delete_user_meta($user_id, self::RESET_TOKEN_EXPIRES_META);
delete_user_meta($user_id, self::RESET_TOKEN_REQUESTED_AT);
}
private static function send_password_reset_email(\WP_User $user, string $token): bool {
$reset_url = aksrt_get_reset_password_url($token, $user->ID);
$site_name = get_bloginfo('name');
$default_subject = '[' . $site_name . '] ' . __('重置密码', 'aksrt');
$default_content = sprintf(
__("您好 %s,\n\n有人请求重置您在 %s 的密码。\n\n请点击以下链接设置新密码:\n\n%s\n\n如果这不是您本人的操作,请忽略此邮件,您的密码不会被更改。", 'aksrt'),
$user->user_login, $site_name, $reset_url
);
return \AKSRT\Settings\Settings::send_event_email(
'password_reset', $user->user_email, $default_subject, $default_content,
['{username}' => $user->user_login, '{site_name}' => $site_name, '{url}' => $reset_url]
);
}
/* =================================================================
* 手机号认证(注册 / 登录 / 绑定)
* 对齐 legacy 的 signup_captcha / signin_captcha / bind_phone_captcha
* ================================================================= */
/**
* 手机号注册
*
@@ -238,34 +17,39 @@ trait ServicePart1b {
*/
public static function register_by_phone(array $post): array {
$phone = sanitize_text_field((string) ($post['phone'] ?? ''));
$code = sanitize_text_field((string) ($post['code'] ?? ''));
$pass = (string) ($post['password'] ?? '');
$confirm = (string) ($post['password_confirm'] ?? '');
$username = sanitize_user(wp_unslash((string) ($post['user_login'] ?? '')), true);
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
}
if (empty($code)) {
return ['success' => false, 'error' => __('请输入验证码', 'aksrt')];
if (empty($post['_auth_code_verified'])) {
return ['success' => false, 'error' => __('请先完成手机验证', 'aksrt')];
}
if (strlen($pass) < 6) {
return ['success' => false, 'error' => __('密码至少 6 位', 'aksrt')];
if ($username === '' || !validate_username($username) || mb_strlen($username) < 3 || mb_strlen($username) > 60) {
return ['success' => false, 'error' => __('请输入 3-60 个字符的有效用户名', 'aksrt')];
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('register', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
if (username_exists($username)) {
return ['success' => false, 'error' => __('该用户名已被使用', 'aksrt')];
}
if (class_exists('\AKSRT\Settings\Settings')) {
foreach (\AKSRT\Settings\Settings::nickname_blacklist() as $keyword) {
if ($keyword !== '' && mb_stripos($username, $keyword) !== false) {
return ['success' => false, 'error' => __('用户名包含禁止使用的关键词', 'aksrt')];
}
}
if (\AKSRT\Settings\Settings::signup_omit_password_confirmation()) {
$confirm = $pass;
}
$agreement = \AKSRT\Settings\Settings::user_agreement_settings();
if ((!empty($agreement['enabled']) || !empty($agreement['privacy'])) && !empty($agreement['required']) && empty($post['agree_terms'])) {
return ['success' => false, 'error' => __('请先同意服务条款', 'aksrt')];
}
}
// 验证 SMS 验证码
if (class_exists('\AKSRT\Sms\SMS')) {
$verify_result = \AKSRT\Sms\SMS::get_instance()->verify_code($phone, $code);
if (empty($verify_result['success'])) {
return ['success' => false, 'error' => $verify_result['message'] ?? __('验证码不正确', 'aksrt')];
}
} else {
return ['success' => false, 'error' => __('短信服务不可用', 'aksrt')];
$password_check = self::validate_password($pass, $confirm);
if ($password_check !== true) {
return ['success' => false, 'error' => $password_check];
}
// 检查手机号是否已注册
@@ -274,15 +58,6 @@ trait ServicePart1b {
return ['success' => false, 'error' => __('该手机号已注册', 'aksrt')];
}
// 生成用户名(phone_ + 后4位)
$username = 'phone_' . substr($phone, -4);
$suffix = 1;
$base = $username;
while (username_exists($username)) {
$username = $base . '_' . $suffix;
$suffix++;
}
$ref_service = class_exists('\AKSRT\Payment\ReferralAttribution')
? \AKSRT\Payment\ReferralAttribution::get_instance()
: null;
@@ -314,9 +89,15 @@ trait ServicePart1b {
return ['success' => false, 'error' => __('推广码无效,请检查后重试', 'aksrt')];
}
}
if ($submitted_ref !== '' && empty($ref_context['valid']) && !empty($registration['reject_invalid'])) {
if ($ref_service) {
$ref_service->clear_registration_context();
}
return ['success' => false, 'error' => __('推广码无效,请检查后重试', 'aksrt')];
}
// 创建用户
$user_id = wp_create_user($username, $pass, $phone . '@sms.aksrt.local');
$user_id = wp_create_user($username, $pass, '');
if (is_wp_error($user_id)) {
if ($ref_service) {
$ref_service->clear_registration_context();
@@ -328,16 +109,15 @@ trait ServicePart1b {
update_user_meta($user_id, 'aksrt_phone', $phone);
update_user_meta($user_id, 'aksrt_phone_verified', '1');
// 邮箱标记为已验证(手机注册等同于已验证)
update_user_meta($user_id, 'aksrt_email_verified', '1');
if ($ref_service) {
$ref_service->reward_registered_user((int) $user_id);
}
self::mark_email_verified((int) $user_id);
// 自动登录
wp_set_current_user($user_id);
wp_set_auth_cookie($user_id, true);
$user = get_userdata($user_id);
if ($user instanceof \WP_User) {
do_action('wp_login', $user->user_login, $user);
}
$redirect = function_exists('aksrt_get_user_center_url')
? aksrt_get_user_center_url()
@@ -352,101 +132,6 @@ trait ServicePart1b {
];
}
/**
* 手机号登录(密码登录)
*
* @param array $post (phone, password)
* @return array
*/
public static function login_by_phone_password(array $post): array {
$phone = sanitize_text_field((string) ($post['phone'] ?? ''));
$pass = (string) ($post['password'] ?? '');
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('login', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
}
}
$user = self::get_user_by_phone($phone);
if (!$user) {
return ['success' => false, 'error' => __('该手机号未注册', 'aksrt')];
}
// 验证密码
if (!wp_check_password($pass, $user->data->user_pass, $user->ID)) {
return ['success' => false, 'error' => __('密码不正确', 'aksrt')];
}
wp_set_current_user($user->ID);
wp_set_auth_cookie($user->ID, true);
$redirect = function_exists('aksrt_get_user_center_url')
? aksrt_get_user_center_url()
: home_url('/');
return [
'success' => true,
'user_id' => $user->ID,
'redirect' => $redirect,
];
}
/**
* 手机号免密登录(SMS 验证码登录)
*
* @param array $post (phone, code)
* @return array
*/
public static function login_by_phone_code(array $post): array {
$phone = sanitize_text_field((string) ($post['phone'] ?? ''));
$code = sanitize_text_field((string) ($post['code'] ?? ''));
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')];
}
if (function_exists('aksrt_require_captcha')) {
$captcha_result = aksrt_require_captcha('login', $post);
if ($captcha_result !== true) {
return ['success' => false, 'error' => $captcha_result];
}
}
// 验证 SMS 验证码
if (class_exists('\AKSRT\Sms\SMS')) {
$verify_result = \AKSRT\Sms\SMS::get_instance()->verify_code($phone, $code);
if (empty($verify_result['success'])) {
return ['success' => false, 'error' => $verify_result['message'] ?? __('验证码不正确', 'aksrt')];
}
} else {
return ['success' => false, 'error' => __('短信服务不可用', 'aksrt')];
}
$user = self::get_user_by_phone($phone);
if (!$user) {
return ['success' => false, 'error' => __('该手机号未注册', 'aksrt')];
}
wp_set_current_user($user->ID);
wp_set_auth_cookie($user->ID, true);
$redirect = function_exists('aksrt_get_user_center_url')
? aksrt_get_user_center_url()
: home_url('/');
return [
'success' => true,
'user_id' => $user->ID,
'redirect' => $redirect,
];
}
/**
* 绑定手机号到当前用户
*