Files
wordpress-theme-aksrt/inc/classes/auth/class-aksrt-auth-email-verification.php
T

530 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* AKSRT Auth Email Verification
*
* 注册邮箱验证子系统:从 AKSRT_Auth_Service 拆出。
* 负责验证状态读写、管理员操作、历史用户回填、验证流程(链接/验证码)与相关通知邮件。
*
* 所有方法均为 public static。常量与若干门面级 helper 仍由 AKSRT_Auth_Service 持有,
* 本类通过 AKSRT_Auth_Service:: 引用以保持单一数据源。
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Auth_Email_Verification {
public static function user_requires_email_verification($user): bool {
if (!$user instanceof WP_User) {
return false;
}
if (!AKSRT_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, AKSRT_Auth_Service::EMAIL_VERIFIED_META, true) === 1;
}
public static function mark_email_verified(int $user_id): void {
$was_verified = (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, true) === 1;
update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 1);
self::clear_email_verification($user_id);
// 仅在状态由"未验证 → 已验证"跳变时触发,避免历史 backfill / 重复调用造成奖励重发。
// 监听方负责自己幂等(aksrt_invite 表的 status 检查已经做到这一点)。
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, AKSRT_Auth_Service::EMAIL_METHOD_META);
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TOKEN_HASH_META);
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_HASH_META);
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_EXPIRES_META);
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META);
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TARGET_META);
delete_user_meta($user_id, AKSRT_Auth_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, AKSRT_Auth_Service::EMAIL_METHOD_META, true),
'target_email' => (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TARGET_META, true),
'expires_at' => (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_EXPIRES_META, true),
'requested_at' => (int) get_user_meta($user_id, AKSRT_Auth_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);
AKSRT_Auth_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, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 0);
self::clear_email_verification($user_id);
self::record_admin_action($user_id, 'unmark_verified', $by_admin_id);
AKSRT_Auth_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 = AKSRT_Auth_Service::current_registration_verification_method();
$email = (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TARGET_META, true);
if (!in_array($method, ['link', 'code'], true)) {
$method = AKSRT_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, AKSRT_Auth_Service::EMAIL_OBK_META, true),
'bypass_throttle' => true,
'force' => true,
]);
// 标记为待验证(确保 admin 主动发邮件后用户被视为未验证)
if (!empty($result['success'])) {
update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 0);
self::record_admin_action($user_id, 'send_verification', $by_admin_id);
AKSRT_Auth_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' => AKSRT_Auth_Service::EMAIL_VERIFIED_META,
'compare' => 'NOT EXISTS',
],
],
]);
if (empty($users)) {
break;
}
foreach ($users as $u) {
update_user_meta((int) $u->ID, AKSRT_Auth_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(),
]);
AKSRT_Auth_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' => AKSRT_Auth_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);
AKSRT_Auth_Service::log_event('admin.backfill.finished', []);
return;
}
foreach ($users as $u) {
update_user_meta((int) $u->ID, AKSRT_Auth_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, AKSRT_Auth_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, AKSRT_Auth_Service::EMAIL_CODE_ATTEMPTS_META, true);
$attempts++;
update_user_meta($user_id, AKSRT_Auth_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, AKSRT_Auth_Service::EMAIL_CODE_IP_TTL);
}
if ($attempts >= AKSRT_Auth_Service::EMAIL_CODE_MAX_ATTEMPTS || $ip_attempts >= AKSRT_Auth_Service::EMAIL_CODE_MAX_ATTEMPTS) {
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_HASH_META);
update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META, 0);
AKSRT_Auth_Service::log_event('verify.code.locked', ['uid' => $user_id, 'attempts' => $attempts]);
return ['success' => false, 'error' => __('验证码错误次数过多,当前验证码已失效,请重新发送。', 'aksrt')];
}
AKSRT_Auth_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, AKSRT_Auth_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::registration_verification_ttl_minutes() * MINUTE_IN_SECONDS);
update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 0);
update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_METHOD_META, $method);
update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_TARGET_META, $email);
update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_EXPIRES_META, $expires_at);
update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META, time());
if ($obk !== '') {
update_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_OBK_META, $obk);
}
delete_user_meta($user->ID, AKSRT_Auth_Service::EMAIL_TOKEN_HASH_META);
delete_user_meta($user->ID, AKSRT_Auth_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, AKSRT_Auth_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, AKSRT_Auth_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);
AKSRT_Auth_Service::log_event('email.send.fail', ['uid' => (int) $user->ID, 'method' => $method]);
return ['success' => false, 'error' => __('验证邮件发送失败,请稍后重试', 'aksrt')];
}
AKSRT_Auth_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, AKSRT_Auth_Service::EMAIL_TOKEN_HASH_META, true);
$expires_at = (int) get_user_meta($user_id, AKSRT_Auth_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 >= AKSRT_Auth_Service::EMAIL_CODE_MAX_ATTEMPTS) {
AKSRT_Auth_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, AKSRT_Auth_Service::EMAIL_CODE_ATTEMPTS_META, true);
if ($user_attempts >= AKSRT_Auth_Service::EMAIL_CODE_MAX_ATTEMPTS) {
delete_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_HASH_META);
update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_REQUESTED_AT_META, 0);
AKSRT_Auth_Service::log_event('verify.code.locked.read', ['uid' => $user_id]);
return ['success' => false, 'error' => __('验证码错误次数过多,当前验证码已失效,请重新发送。', 'aksrt')];
}
$stored_hash = (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_CODE_HASH_META, true);
$expires_at = (int) get_user_meta($user_id, AKSRT_Auth_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, AKSRT_Auth_Service::EMAIL_OBK_META, true);
$oauth = $obk !== '' ? AKSRT_Auth_Service::get_oauth_bind_context($obk) : null;
$email = (string) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_TARGET_META, true);
$was_verified = (int) get_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, true) === 1;
update_user_meta($user_id, AKSRT_Auth_Service::EMAIL_VERIFIED_META, 1);
self::clear_email_verification($user_id);
AKSRT_Auth_Service::log_event('verify.success', ['uid' => $user_id]);
// 状态跳变时触发 hook,邀请系统等模块据此发奖
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)) {
AKSRT_Auth_Service::finalize_oauth_registration($user_id, $oauth, $obk);
} elseif (AKSRT_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::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 = AKSRT_Auth_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::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::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::send_event_email(
'reg_success', $email, $default_subject, $default_content,
['{username}' => $user->user_login, '{site_name}' => $site_name]
);
}
}