get_json_params($request); return is_array($params) ? $params : []; } private function auth_verify_rest_nonce(\WP_REST_Request $request, array $payload): bool { $nonce = sanitize_text_field((string) ($request->get_header('X-WP-Nonce') ?: ($payload['_wpnonce'] ?? ''))); return $nonce !== '' && (bool) wp_verify_nonce($nonce, 'wp_rest'); } private function auth_require_captcha(string $scene, array $payload): ?\WP_REST_Response { $result = aksrt_require_captcha($scene, $payload); if ($result === true) { return null; } if (is_wp_error($result)) { $data = $result->get_error_data(); $status = is_array($data) ? (int) ($data['status'] ?? 400) : 400; $message = $result->get_error_message() ?: __('人机验证失败', 'aksrt'); return new \WP_REST_Response([ 'success' => false, 'code' => $result->get_error_code() ?: 'aksrt_captcha_failed', 'message' => $message, ], $status > 0 ? $status : 400); } return new \WP_REST_Response([ 'success' => false, 'code' => 'aksrt_captcha_failed', 'message' => __('人机验证失败', 'aksrt'), ], 400); } private function auth_rate_key(string $action, string $subject = ''): string { $ip = sanitize_text_field((string) ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0')); return 'aksrt_auth_rate_' . md5($action . '|' . strtolower($subject) . '|' . $ip); } private function auth_rate_limited(string $action, string $subject = '', int $limit = 8, int $ttl = 600): bool { $key = $this->auth_rate_key($action, $subject); $count = (int) get_transient($key); if ($count >= $limit) { return true; } set_transient($key, $count + 1, $ttl); return false; } private function auth_code_key(string $scene, string $target): string { return 'aksrt_auth_code_' . md5($scene . '|' . strtolower(trim($target))); } private function auth_store_code(string $scene, string $target, string $code, int $ttl = 10 * MINUTE_IN_SECONDS): void { set_transient($this->auth_code_key($scene, $target), [ 'hash' => wp_hash_password($code), 'attempts' => 0, 'expires' => time() + $ttl, ], $ttl); } private function auth_consume_code(string $scene, string $target, string $code, bool $consume = true): bool { $key = $this->auth_code_key($scene, $target); $stored = get_transient($key); if (!is_array($stored) || empty($stored['hash'])) { return false; } $remaining = (int) ($stored['expires'] ?? 0) - time(); if ($remaining <= 0) { delete_transient($key); return false; } $attempts = (int) ($stored['attempts'] ?? 0) + 1; if ($attempts > 6) { delete_transient($key); return false; } if (!wp_check_password($code, (string) $stored['hash'])) { $stored['attempts'] = $attempts; set_transient($key, $stored, $remaining); return false; } if ($consume) { delete_transient($key); } return true; } private function auth_channel(string $target, string $requested = ''): string { if ($requested === 'phone' || (class_exists('\\AKSRT\\Sms\\SMS') && \AKSRT\Sms\SMS::is_phonenumber($target))) { return 'phone'; } return 'email'; } private function auth_channel_allowed(string $configured, string $channel): bool { return $configured === 'email_phone' || $configured === $channel; } private function auth_safe_redirect(string $requested, string $fallback = ''): string { $fallback = $fallback !== '' ? $fallback : home_url('/'); return wp_validate_redirect($requested, $fallback); } private function auth_registration_error(string $error): string { if (str_contains($error, '已被') || str_contains($error, '已注册') || str_contains($error, '已使用')) { return __('注册信息不可用,请检查后重试', 'aksrt'); } return $error; } private function auth_find_user(string $target, string $channel) { if ($channel === 'phone') { $users = get_users([ 'meta_key' => 'phone_number', 'meta_value' => $target, 'number' => 1, ]); return $users[0] ?? null; } return is_email($target) ? get_user_by('email', sanitize_email($target)) : get_user_by('login', sanitize_user($target, true)); } public function auth_code(\WP_REST_Request $request): \WP_REST_Response { $payload = $this->auth_payload($request); if (!$this->auth_verify_rest_nonce($request, $payload)) { return $this->error(__('安全验证失败,请刷新页面重试', 'aksrt'), 403); } $scene = sanitize_key((string) ($payload['scene'] ?? '')); $target = trim(sanitize_text_field((string) ($payload['target'] ?? ''))); $channel = $this->auth_channel($target, sanitize_key((string) ($payload['channel'] ?? ''))); $obk = sanitize_text_field((string) ($payload['obk'] ?? '')); $oauth_bind = $obk !== '' && class_exists('\AKSRT\Auth\Service') && is_array(\AKSRT\Auth\Service::get_oauth_bind_context($obk)); if (!in_array($scene, ['login', 'register', 'resetpassword'], true)) { return $this->error(__('请求类型无效', 'aksrt')); } if (class_exists('\AKSRT\Settings\Settings')) { if (\AKSRT\Settings\Settings::close_sign()) return $this->error(__('登录注册已关闭', 'aksrt'), 403); if (($scene === 'login' || $scene === 'resetpassword') && \AKSRT\Settings\Settings::close_signin() && !$oauth_bind) return $this->error(__('当前登录方式已关闭', 'aksrt'), 403); if ($scene === 'register' && !\AKSRT\Settings\Settings::registration_enabled() && !$oauth_bind) return $this->error(__('当前已关闭新用户注册', 'aksrt'), 403); $type = $scene === 'login' ? \AKSRT\Settings\Settings::passwordless_login_type() : ($scene === 'register' ? \AKSRT\Settings\Settings::signup_verification_type() : \AKSRT\Settings\Settings::reset_password_verification_type()); $enabled = $scene === 'login' ? \AKSRT\Settings\Settings::passwordless_login_enabled() : ($scene === 'register' ? \AKSRT\Settings\Settings::signup_verification_enabled() : true); if (!$enabled || ($type !== 'email_phone' && $type !== $channel)) return $this->error(__('当前验证方式不可用', 'aksrt'), 403); } if (($channel === 'email' && !is_email($target)) || ($channel === 'phone' && !\AKSRT\Sms\SMS::is_phonenumber($target))) { return $this->error(__('请输入有效的邮箱或手机号', 'aksrt')); } if ($this->auth_rate_limited('code:' . $scene, $target, 5, HOUR_IN_SECONDS)) { return $this->error(__('请求过于频繁,请稍后再试', 'aksrt'), 429); } $captcha_scene = $scene === 'resetpassword' ? 'lostpassword' : $scene; $captcha_error = $this->auth_require_captcha($captcha_scene, $payload); if ($captcha_error) { return $captcha_error; } $generic = __('如果信息有效,验证码将发送到该账号', 'aksrt'); if ($scene !== 'register' && !$this->auth_find_user($target, $channel)) { return $this->success(['message' => $generic, 'countdown' => 60]); } if ($channel === 'phone') { if (!class_exists('\AKSRT\Sms\SMS')) { return $this->error(__('短信服务不可用', 'aksrt'), 503); } $code = (string) random_int(100000, 999999); $result = \AKSRT\Sms\SMS::get_instance()->send_code($target, $code); if (empty($result['success'])) { return $this->error((string) ($result['message'] ?? __('验证码发送失败', 'aksrt'))); } $this->auth_store_code($scene, $target, $code, 30 * MINUTE_IN_SECONDS); return $this->success(['message' => $generic, 'countdown' => (int) ($result['remaining_time'] ?? 60)]); } // [防刷机制]验证码发送限制(短信通道由 SMS::send_code 内部接入)。 if (class_exists('\AKSRT\Extensions\SendLimit')) { $limit = \AKSRT\Extensions\SendLimit::check($target); if (empty($limit['ok'])) { return $this->error((string) $limit['message'], 429); } } $code = (string) random_int(100000, 999999); $scene_event = [ 'login' => 'login_code', 'register' => 'reg_verify_code', 'resetpassword' => 'password_reset', ][$scene] ?? ''; $user = $this->auth_find_user($target, $channel); $display_name = $user instanceof \WP_User ? $user->display_name : $target; $site_name = get_bloginfo('name'); $placeholders = [ '{username}' => $display_name, '{site_name}' => $site_name, '{code}' => $code, ]; $sent = $scene_event !== '' && class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::send_event_email($scene_event, $target, $placeholders) : false; if (!$sent) { delete_transient($this->auth_code_key($scene, $target)); if ($scene !== 'register') { return $this->success(['message' => $generic, 'countdown' => 60]); } return $this->error(__('验证码发送失败,请稍后再试', 'aksrt'), 500); } $cooldown = class_exists('\AKSRT\Extensions\SendLimit') ? (int) \AKSRT\Extensions\SendLimit::record($target) : 0; $this->auth_store_code($scene, $target, $code); return $this->success(['message' => $generic, 'countdown' => $cooldown > 0 ? $cooldown : 60]); } public function auth_login(\WP_REST_Request $request): \WP_REST_Response { $payload = $this->auth_payload($request); if (!$this->auth_verify_rest_nonce($request, $payload)) { return $this->error(__('安全验证失败,请刷新页面重试', 'aksrt'), 403); } $target = trim(sanitize_text_field((string) ($payload['target'] ?? $payload['log'] ?? ''))); $obk = sanitize_text_field((string) ($payload['obk'] ?? '')); $oauth_data = $obk !== '' && class_exists('\AKSRT\Auth\Service') ? \AKSRT\Auth\Service::get_oauth_bind_context($obk) : null; if ($obk !== '' && !$oauth_data) { return $this->error(__('绑定会话已过期,请重新使用第三方登录', 'aksrt'), 410); } if ( class_exists('\AKSRT\Settings\Settings') && (\AKSRT\Settings\Settings::close_sign() || (!$oauth_data && \AKSRT\Settings\Settings::close_signin())) ) { return $this->error(__('当前登录方式已关闭', 'aksrt'), 403); } if ($target === '' || (\AKSRT\Sms\SMS::is_phonenumber($target) && empty($payload['code']) && class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::signin_phone_enabled())) { return $this->error(__('账号或密码错误', 'aksrt'), 401); } if ($this->auth_rate_limited('login', $target)) { return $this->error(__('请求过于频繁,请稍后再试', 'aksrt'), 429); } $captcha_error = $this->auth_require_captcha($oauth_data ? 'bind' : 'login', $payload); if ($captcha_error) { return $captcha_error; } $redirect = $this->auth_safe_redirect((string) ($payload['redirect_to'] ?? ''), home_url('/')); if (!empty($payload['code'])) { if (!class_exists('\AKSRT\Settings\Settings') || !\AKSRT\Settings\Settings::passwordless_login_enabled()) { return $this->error(__('当前未开启免密登录', 'aksrt')); } $channel = $this->auth_channel($target, sanitize_key((string) ($payload['channel'] ?? ''))); $configured = \AKSRT\Settings\Settings::passwordless_login_type(); if (!$this->auth_channel_allowed($configured, $channel)) { return $this->error(__('当前验证方式不可用', 'aksrt'), 403); } $user = $this->auth_find_user($target, $channel); if (!$user || !$this->auth_consume_code('login', $target, sanitize_text_field((string) $payload['code']))) { return $this->error(__('账号或验证码错误', 'aksrt'), 401); } if ($oauth_data) { $oauth_data = \AKSRT\Auth\Service::claim_oauth_bind_context($obk); if (!$oauth_data) { return $this->error(__('绑定会话已被使用,请重新授权', 'aksrt'), 410); } $binding = \AKSRT\Auth\Service::finalize_oauth_binding((int) $user->ID, $oauth_data, $obk); if (is_wp_error($binding)) { return $this->error($binding->get_error_message(), 409); } \AKSRT\Auth\OAuthAccountService::complete_login($user, !empty($payload['rememberme'])); $redirect = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : $redirect; return $this->success(['message' => __('账号绑定成功', 'aksrt'), 'redirect' => $redirect]); } wp_set_current_user((int) $user->ID); wp_set_auth_cookie((int) $user->ID, !empty($payload['rememberme'])); do_action('wp_login', $user->user_login, $user); return $this->success(['message' => __('登录成功', 'aksrt'), 'redirect' => $redirect]); } if (!class_exists('\AKSRT\Auth\Service')) { return $this->error(__('认证服务不可用', 'aksrt'), 500); } $payload['log'] = $target; $payload['pwd'] = (string) ($payload['password'] ?? $payload['pwd'] ?? ''); $payload['redirect_to'] = $redirect; if ($oauth_data) { $resolved_login = \AKSRT\Auth\Service::resolve_email_to_username($target); $user = wp_authenticate($resolved_login, (string) ($payload['password'] ?? $payload['pwd'] ?? '')); if (is_wp_error($user) || !$user instanceof \WP_User) { // 透传具体失败原因,不得泛化为“账号或密码错误”。 $bind_error = $user instanceof \WP_Error ? \AKSRT\Auth\Service::friendly_login_error($user->get_error_message()) : ''; return $this->error($bind_error !== '' ? $bind_error : __('账号或密码错误', 'aksrt'), 401); } $oauth_data = \AKSRT\Auth\Service::claim_oauth_bind_context($obk); if (!$oauth_data) { return $this->error(__('绑定会话已被使用,请重新授权', 'aksrt'), 410); } $binding = \AKSRT\Auth\Service::finalize_oauth_binding((int) $user->ID, $oauth_data, $obk); if (is_wp_error($binding)) { return $this->error($binding->get_error_message(), 409); } \AKSRT\Auth\OAuthAccountService::complete_login($user, !empty($payload['rememberme'])); $redirect = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : $redirect; return $this->success(['message' => __('账号绑定成功', 'aksrt'), 'redirect' => $redirect]); } $result = \AKSRT\Auth\Service::login_user($payload); if (empty($result['success'])) { // 透传具体失败原因:禁封等场景不得泛化为“账号或密码错误”。 $login_error = trim((string) ($result['error'] ?? '')); return $this->error($login_error !== '' ? $login_error : __('账号或密码错误', 'aksrt'), 401); } return $this->success(['message' => $oauth_data ? __('账号绑定成功', 'aksrt') : __('登录成功', 'aksrt'), 'redirect' => $redirect]); } public function auth_register(\WP_REST_Request $request): \WP_REST_Response { $payload = $this->auth_payload($request); if (!$this->auth_verify_rest_nonce($request, $payload)) { return $this->error(__('安全验证失败,请刷新页面重试', 'aksrt'), 403); } $obk = sanitize_text_field((string) ($payload['obk'] ?? '')); $oauth_data = $obk !== '' && class_exists('\AKSRT\Auth\Service') ? \AKSRT\Auth\Service::get_oauth_bind_context($obk) : null; if ($obk !== '' && !$oauth_data) { return $this->error(__('绑定会话已过期,请重新使用第三方登录', 'aksrt'), 410); } if (class_exists('\AKSRT\Settings\Settings') && ( \AKSRT\Settings\Settings::close_sign() || ($oauth_data ? !\AKSRT\Settings\Settings::oauth_registration_enabled() : !\AKSRT\Settings\Settings::registration_enabled()) )) { return $this->error(__('当前已关闭新用户注册', 'aksrt'), 403); } if ($this->auth_rate_limited('register')) { return $this->error(__('请求过于频繁,请稍后再试', 'aksrt'), 429); } $target = trim(sanitize_text_field((string) ($payload['target'] ?? $payload['user_email'] ?? $payload['phone'] ?? ''))); $channel = $this->auth_channel($target, sanitize_key((string) ($payload['channel'] ?? ''))); $signup_verification = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::signup_verification_enabled(); $configured = $signup_verification ? \AKSRT\Settings\Settings::signup_verification_type() : 'email'; if (!$this->auth_channel_allowed($configured, $channel)) { return $this->error(__('当前注册方式不可用', 'aksrt'), 403); } if (($channel === 'email' && !is_email($target)) || ($channel === 'phone' && !\AKSRT\Sms\SMS::is_phonenumber($target))) { return $this->error(__('请输入有效的邮箱或手机号', 'aksrt')); } $captcha_error = $this->auth_require_captcha('register', $payload); if ($captcha_error) { return $captcha_error; } $verification_completed = false; if ($signup_verification) { if (!$this->auth_consume_code('register', $target, sanitize_text_field((string) ($payload['code'] ?? '')))) { return $this->error(__('验证码错误或已过期', 'aksrt')); } $verification_completed = true; } if (!class_exists('\AKSRT\Auth\Service')) { return $this->error(__('认证服务不可用', 'aksrt'), 500); } if ($channel === 'phone') { $payload['phone'] = $target; $result = \AKSRT\Auth\Service::register_by_phone($payload, $verification_completed, !empty($oauth_data)); if (empty($result['success'])) { return $this->error($this->auth_registration_error((string) ($result['error'] ?? __('注册失败', 'aksrt')))); } $user_id = (int) ($result['user_id'] ?? 0); if ($oauth_data && $user_id > 0) { $binding = \AKSRT\Auth\Service::finalize_oauth_binding($user_id, $oauth_data, $obk); if (is_wp_error($binding)) { $this->delete_oauth_registration_user($user_id); return $this->error($binding->get_error_message(), 409); } $user = get_user_by('id', $user_id); if ($user instanceof \WP_User) { \AKSRT\Auth\OAuthAccountService::complete_login($user, true); } } return $this->success(['message' => $oauth_data ? __('注册并绑定成功', 'aksrt') : __('注册成功', 'aksrt'), 'redirect' => $result['redirect'] ?? home_url('/'), 'notice' => (string) ($result['notice'] ?? '')]); } $payload['user_email'] = sanitize_email($target); $payload['user_pass'] = (string) ($payload['password'] ?? ''); $payload['user_pass_confirm'] = (string) ($payload['password_confirm'] ?? $payload['password'] ?? ''); $result = \AKSRT\Auth\Service::register_user($payload, $verification_completed, !empty($oauth_data)); if (empty($result['success'])) { return $this->error($this->auth_registration_error((string) ($result['error'] ?? __('注册失败', 'aksrt')))); } $user_id = (int) ($result['user_id'] ?? 0); if ($oauth_data && $user_id > 0) { $binding = \AKSRT\Auth\Service::finalize_oauth_binding($user_id, $oauth_data, $obk); if (is_wp_error($binding)) { $this->delete_oauth_registration_user($user_id); return $this->error($binding->get_error_message(), 409); } $user = get_user_by('id', $user_id); if ($user instanceof \WP_User) { if ($channel === 'email' && $verification_completed) { \AKSRT\Auth\Service::mark_verified_email($user_id); } \AKSRT\Auth\OAuthAccountService::complete_login($user, true); } } return $this->success([ 'message' => $oauth_data ? __('注册并绑定成功', 'aksrt') : __('注册成功', 'aksrt'), 'redirect' => $result['redirect'] ?? home_url('/'), 'notice' => (string) ($result['notice'] ?? ''), ]); } public function auth_reset_password(\WP_REST_Request $request): \WP_REST_Response { $payload = $this->auth_payload($request); if (!$this->auth_verify_rest_nonce($request, $payload)) { return $this->error(__('安全验证失败,请刷新页面重试', 'aksrt'), 403); } $target = trim(sanitize_text_field((string) ($payload['target'] ?? ''))); $channel = $this->auth_channel($target, sanitize_key((string) ($payload['channel'] ?? ''))); if (class_exists('\AKSRT\Settings\Settings')) { if (\AKSRT\Settings\Settings::close_sign() || \AKSRT\Settings\Settings::close_signin() || !$this->auth_channel_allowed(\AKSRT\Settings\Settings::reset_password_verification_type(), $channel)) { return $this->error(__('当前验证方式不可用', 'aksrt'), 403); } } if (($channel === 'email' && !is_email($target)) || ($channel === 'phone' && !\AKSRT\Sms\SMS::is_phonenumber($target))) { return $this->error(__('请输入有效的邮箱或手机号', 'aksrt')); } if ($this->auth_rate_limited('reset-password', $target, 8, HOUR_IN_SECONDS)) { return $this->error(__('请求过于频繁,请稍后再试', 'aksrt'), 429); } $captcha_error = $this->auth_require_captcha('lostpassword', $payload); if ($captcha_error) { return $captcha_error; } $user = $this->auth_find_user($target, $channel); $password = (string) ($payload['password'] ?? ''); $confirm = (string) ($payload['password_confirm'] ?? ''); if (!class_exists('\AKSRT\Auth\Service')) { return $this->error(__('认证服务不可用', 'aksrt'), 500); } $password_check = \AKSRT\Auth\Service::validate_password($password, $confirm); if ($password_check !== true) { return $this->error((string) $password_check); } if (!$user || !$this->auth_consume_code('resetpassword', $target, sanitize_text_field((string) ($payload['code'] ?? '')))) { return $this->error(__('账号或验证码错误', 'aksrt')); } wp_set_password($password, (int) $user->ID); return $this->success(['message' => __('密码已重置,请重新登录', 'aksrt'), 'redirect' => function_exists('aksrt_get_sign_page_url') ? aksrt_get_sign_page_url('signin') : wp_login_url()]); } public function user_phone_code(\WP_REST_Request $request): \WP_REST_Response { $payload = $this->auth_payload($request); if (!$this->auth_verify_rest_nonce($request, $payload)) return $this->error(__('安全验证失败', 'aksrt'), 403); $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : []; if (empty($settings['bind_phone'])) return $this->error(__('手机号绑定功能暂未开启', 'aksrt'), 403); $stage = sanitize_key((string) ($payload['stage'] ?? 'bind_new_phone')); $user_id = get_current_user_id(); $current_phone = (string) get_user_meta($user_id, 'phone_number', true); if ($stage === 'reset_old_phone') { // 前端在修改已通过校验的中间位后调用:销毁已发放的老号验证码状态。 if ($current_phone !== '') { delete_transient($this->auth_code_key('account_verify_old_phone_' . $user_id, $current_phone)); } return $this->success(['message' => __('已取消本次验证', 'aksrt')]); } if ($stage === 'verify_old_phone') { // 老号验证只接收脱敏中间位,发送目标恒为服务端绑定号, // 避免客户端提交任意号码导致向第三方手机号发码。 if ($current_phone === '') return $this->error(__('当前绑定手机号已变化,请刷新页面重试', 'aksrt'), 409); $parts = function_exists('aksrt_phone_mask_parts') ? aksrt_phone_mask_parts($current_phone) : null; if ($parts !== null) { $middle = sanitize_text_field((string) ($payload['middle'] ?? '')); if (!$this->auth_middle_matches($parts, $middle)) { if (class_exists('\AKSRT\Extensions\SecurityHardening')) { \AKSRT\Extensions\SecurityHardening::error_frequency_record(); } return $this->error(__('填写的号码与绑定手机号不一致', 'aksrt'), 400); } } $phone = $current_phone; } elseif ($stage === 'bind_new_phone') { $phone = sanitize_text_field((string) ($payload['phone'] ?? '')); if (!\AKSRT\Sms\SMS::is_phonenumber($phone)) return $this->error(__('手机号格式不正确', 'aksrt')); if ($current_phone !== '' && !hash_equals($current_phone, $phone)) { $proof = get_transient('aksrt_account_phone_old_' . $user_id); if (!is_array($proof) || !hash_equals($current_phone, (string) ($proof['phone'] ?? ''))) return $this->error(__('请先验证老手机号', 'aksrt'), 403); } $users = get_users(['meta_key' => 'phone_number', 'meta_value' => $phone, 'number' => 1, 'fields' => 'ID']); if (!empty($users) && (int) $users[0] !== $user_id) return $this->error(__('该手机号已被其他账号绑定', 'aksrt'), 409); if ($current_phone !== '' && hash_equals($current_phone, $phone)) return $this->error(__('手机号不能与当前绑定号码相同', 'aksrt'), 409); } else { return $this->error(__('无效的验证步骤', 'aksrt')); } $captcha_error = $this->auth_require_captcha('bind_phone', $payload); if ($captcha_error) return $captcha_error; if (class_exists('\AKSRT\Extensions\SecurityHardening')) { $wait = \AKSRT\Extensions\SecurityHardening::error_frequency_wait(); if ($wait > 0) return $this->error(sprintf(__('验证错误次数过多,请 %d 秒后再试', 'aksrt'), $wait), 429); } if (!class_exists('\AKSRT\Sms\SMS')) return $this->error(__('短信服务不可用', 'aksrt'), 503); $code = (string) random_int(100000, 999999); $result = \AKSRT\Sms\SMS::get_instance()->send_code($phone, $code); if (empty($result['success'])) return $this->error((string) ($result['message'] ?? __('发送失败', 'aksrt'))); $this->auth_store_code('account_' . $stage . '_' . $user_id, $phone, $code, 30 * MINUTE_IN_SECONDS); return $this->success(['message' => __('验证码短信已发送', 'aksrt'), 'countdown' => (int) ($result['remaining_time'] ?? 60)]); } /** * 校验客户端提交的脱敏中间位是否与绑定号一致(不发短信)。 */ public function user_phone_match(\WP_REST_Request $request): \WP_REST_Response { $payload = $this->auth_payload($request); if (!$this->auth_verify_rest_nonce($request, $payload)) return $this->error(__('安全验证失败', 'aksrt'), 403); $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : []; if (empty($settings['bind_phone'])) return $this->error(__('手机号绑定功能暂未开启', 'aksrt'), 403); $user_id = get_current_user_id(); $current_phone = (string) get_user_meta($user_id, 'phone_number', true); $parts = function_exists('aksrt_phone_mask_parts') ? aksrt_phone_mask_parts($current_phone) : null; if ($parts === null) return $this->error(__('当前绑定手机号不支持此验证方式', 'aksrt'), 409); // 中间位是可枚举面:错误频率限制 + 每用户/IP 每小时尝试上限双保险。 if (class_exists('\AKSRT\Extensions\SecurityHardening')) { $wait = \AKSRT\Extensions\SecurityHardening::error_frequency_wait(); if ($wait > 0) return $this->error(sprintf(__('验证错误次数过多,请 %d 秒后再试', 'aksrt'), $wait), 429); } $ip = function_exists('aksrt_get_client_ip') ? aksrt_get_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''); if ($this->auth_rate_limited('phone-match', (string) $user_id . '|' . $ip, 20, HOUR_IN_SECONDS)) { return $this->error(__('尝试次数过多,请稍后再试', 'aksrt'), 429); } $middle = sanitize_text_field((string) ($payload['middle'] ?? '')); if (!$this->auth_middle_matches($parts, $middle)) { if (class_exists('\AKSRT\Extensions\SecurityHardening')) { \AKSRT\Extensions\SecurityHardening::error_frequency_record(); } return $this->error(__('填写的号码与绑定手机号不一致', 'aksrt'), 400); } if (class_exists('\AKSRT\Extensions\SecurityHardening')) { \AKSRT\Extensions\SecurityHardening::error_frequency_clear(); } return $this->success(['match' => true]); } /** * 比对脱敏中间位:长度必须与隐藏位数一致,按位恒时比较。 */ private function auth_middle_matches(array $parts, string $middle): bool { $middle = preg_replace('/\D+/', '', $middle); if ($middle === '' || strlen($middle) !== (int) $parts['hidden_length']) { return false; } return hash_equals((string) $parts['hidden'], $middle); } public function user_verify_phone(\WP_REST_Request $request): \WP_REST_Response { $payload = $this->auth_payload($request); if (!$this->auth_verify_rest_nonce($request, $payload)) return $this->error(__('安全验证失败', 'aksrt'), 403); $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : []; if (empty($settings['bind_phone'])) return $this->error(__('手机号绑定功能暂未开启', 'aksrt'), 403); $captcha_error = $this->auth_require_captcha('bind_phone', $payload); if ($captcha_error) return $captcha_error; $user_id = get_current_user_id(); $current_phone = (string) get_user_meta($user_id, 'phone_number', true); $code = sanitize_text_field((string) ($payload['code'] ?? '')); if ($current_phone === '') return $this->error(__('当前绑定手机号已变化,请刷新页面重试', 'aksrt'), 409); if (!$this->auth_consume_code('account_verify_old_phone_' . $user_id, $current_phone, $code)) { if (class_exists('\AKSRT\Extensions\SecurityHardening')) { \AKSRT\Extensions\SecurityHardening::error_frequency_record(); } return $this->error(__('验证码错误或已过期', 'aksrt')); } if (class_exists('\AKSRT\Extensions\SecurityHardening')) { \AKSRT\Extensions\SecurityHardening::error_frequency_clear(); } set_transient('aksrt_account_phone_old_' . $user_id, ['phone' => $current_phone], 30 * MINUTE_IN_SECONDS); return $this->success(['message' => __('手机号验证成功', 'aksrt')]); } public function user_bind_phone(\WP_REST_Request $request): \WP_REST_Response { $payload = $this->auth_payload($request); if (!$this->auth_verify_rest_nonce($request, $payload)) return $this->error(__('安全验证失败', 'aksrt'), 403); $captcha_error = $this->auth_require_captcha('bind_phone', $payload); if ($captcha_error) return $captcha_error; $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : []; if (empty($settings['bind_phone'])) return $this->error(__('手机号绑定功能暂未开启', 'aksrt'), 403); $user_id = get_current_user_id(); $current_phone = (string) get_user_meta($user_id, 'phone_number', true); $phone = sanitize_text_field((string) ($payload['phone'] ?? '')); $code = sanitize_text_field((string) ($payload['code'] ?? '')); if (!\AKSRT\Sms\SMS::is_phonenumber($phone)) return $this->error(__('手机号格式不正确', 'aksrt')); if ($current_phone !== '') { $proof = get_transient('aksrt_account_phone_old_' . $user_id); if (!is_array($proof) || !hash_equals($current_phone, (string) ($proof['phone'] ?? ''))) return $this->error(__('请先验证老手机号', 'aksrt'), 403); if (hash_equals($current_phone, $phone)) return $this->error(__('手机号不能与当前绑定号码相同', 'aksrt'), 409); } $users = get_users(['meta_key' => 'phone_number', 'meta_value' => $phone, 'number' => 1, 'fields' => 'ID']); if (!empty($users) && (int) $users[0] !== $user_id) return $this->error(__('该手机号已被其他账号绑定', 'aksrt'), 409); $agreement = function_exists('aksrt_get_auth_agreement_view') ? aksrt_get_auth_agreement_view() : ['enabled' => false, 'required' => false]; if (!empty($agreement['required']) && empty($payload['agree_terms'])) return $this->error(__('请先同意相关协议', 'aksrt'), 400); $code_scene = 'account_bind_new_phone_' . $user_id; if (!$this->auth_consume_code($code_scene, $phone, $code, false)) { if (class_exists('\AKSRT\Extensions\SecurityHardening')) { \AKSRT\Extensions\SecurityHardening::error_frequency_record(); } return $this->error(__('验证码错误或已过期', 'aksrt')); } if (!class_exists('\AKSRT\Auth\Service')) return $this->error(__('认证服务不可用', 'aksrt'), 500); $result = \AKSRT\Auth\Service::bind_phone($payload, true); if (empty($result['success'])) return $this->error((string) ($result['error'] ?? __('绑定失败', 'aksrt'))); if (class_exists('\AKSRT\Extensions\SecurityHardening')) { \AKSRT\Extensions\SecurityHardening::error_frequency_clear(); } delete_transient($this->auth_code_key($code_scene, $phone)); delete_transient('aksrt_account_phone_old_' . $user_id); return $this->success(['message' => $result['message'] ?? __('绑定成功', 'aksrt')]); } public function test_sms_send(\WP_REST_Request $request): \WP_REST_Response { $params = $this->get_json_params($request); $phone = sanitize_text_field((string) ($params['phone'] ?? '')); if (!class_exists('\\AKSRT\\Sms\\SMS')) { return $this->error(__('短信服务不可用', 'aksrt'), 503); } $result = \AKSRT\Sms\SMS::get_instance()->test_send($phone); return !empty($result['success']) ? $this->success(['message' => (string) ($result['message'] ?? __('测试短信已发送', 'aksrt'))]) : $this->error((string) ($result['message'] ?? __('测试短信发送失败', 'aksrt'))); } }