aksrt_is_captcha_enabled_for(scene) * -> aksrt_render_captcha(scene) -> 客户端 AKSRTCaptchaState + token * -> 业务入口 aksrt_require_captcha(scene, $payload) -> proceed/fail。 * * 仅支持云厂商:tencent / aliyun / cloudflare / geetest。 * 不含本地图形码 / 本地滑块。 * * @package AKSRT */ defined('ABSPATH') || exit; /** * 支持的验证服务商。 */ function aksrt_captcha_providers(): array { return ['tencent', 'aliyun', 'cloudflare', 'geetest']; } /** * 人机验证场景白名单。 */ function aksrt_captcha_scenes(): array { return [ 'comment' => __('发表评论', 'aksrt'), 'login' => __('用户登录', 'aksrt'), 'register' => __('用户注册', 'aksrt'), 'bind' => __('绑定第三方', 'aksrt'), 'bind_phone' => __('绑定手机', 'aksrt'), 'lostpassword' => __('找回密码', 'aksrt'), 'change_password' => __('修改密码', 'aksrt'), 'change_email' => __('更改邮箱', 'aksrt'), 'verify_email' => __('邮箱验证', 'aksrt'), 'download' => __('资源下载', 'aksrt'), 'signin' => __('每日签到', 'aksrt'), ]; } /** * 默认配置结构。 */ function aksrt_captcha_default_config(): array { $all_scenes = array_keys(aksrt_captcha_scenes()); return [ 'enabled' => false, 'active_provider' => '', // Positive list: checked scenes require captcha. Default all scenes. 'enabled_scenarios' => $all_scenes, // Legacy mirror kept for BC consumers / admin summaries. 'disabled_scenarios' => [], 'tencent' => [], 'aliyun' => [], 'cloudflare' => [], 'geetest' => [], ]; } /** * Sanitize a list of captcha scene keys against the whitelist. * * @param mixed $raw * @return array */ function aksrt_captcha_sanitize_scene_list($raw): array { $list = is_array($raw) ? $raw : []; $clean = []; foreach ($list as $scene) { $scene = sanitize_key((string) $scene); if ($scene !== '' && array_key_exists($scene, aksrt_captcha_scenes()) && !in_array($scene, $clean, true)) { $clean[] = $scene; } } return array_values($clean); } /** * 获取人机验证完整配置(规范化,不合并历史脏键)。 * * @return array */ function aksrt_get_captcha_config(): array { $cfg = get_option('aksrt_captcha_config', []); if (!is_array($cfg)) { $cfg = []; } $all_scenes = array_keys(aksrt_captcha_scenes()); $provider = isset($cfg['active_provider']) ? sanitize_key((string) $cfg['active_provider']) : ''; if (!in_array($provider, aksrt_captcha_providers(), true)) { $provider = ''; } if (array_key_exists('enabled_scenarios', $cfg)) { // Explicit positive list (may be empty = no scenes). $enabled = aksrt_captcha_sanitize_scene_list($cfg['enabled_scenarios']); } else { // Legacy: only disabled list stored → all minus disabled. $disabled_legacy = aksrt_captcha_sanitize_scene_list($cfg['disabled_scenarios'] ?? []); $enabled = array_values(array_diff($all_scenes, $disabled_legacy)); } $disabled = array_values(array_diff($all_scenes, $enabled)); $result = [ 'enabled' => !empty($cfg['enabled']), 'active_provider' => $provider, 'enabled_scenarios' => $enabled, 'disabled_scenarios' => $disabled, ]; foreach (aksrt_captcha_providers() as $p) { $result[$p] = isset($cfg[$p]) && is_array($cfg[$p]) ? $cfg[$p] : []; } return $result; } /** * 各服务商必填字段。 */ function aksrt_captcha_required_fields(string $provider): array { $map = [ 'tencent' => ['secret_id', 'secret_key', 'app_id', 'app_secret_key'], 'aliyun' => ['region', 'prefix', 'scene_id', 'access_key_id', 'access_key_secret'], 'cloudflare' => ['site_key', 'secret_key'], 'geetest' => ['captcha_id', 'captcha_key'], ]; return $map[$provider] ?? []; } /** * 当前服务商必填凭据是否齐全(避免空 captcha_id 打到第三方 403)。 */ function aksrt_captcha_provider_is_configured(string $provider, array $pcfg = []): bool { if (!in_array($provider, aksrt_captcha_providers(), true)) { return false; } $required = aksrt_captcha_required_fields($provider); if (empty($required)) { return false; } foreach ($required as $field) { if (!isset($pcfg[$field]) || trim((string) $pcfg[$field]) === '') { return false; } } return true; } /** * 指定场景是否启用人机验证。 * * @param string $scene 见 aksrt_captcha_scenes() * @return bool */ function aksrt_is_captcha_enabled_for($scene): bool { $scene = sanitize_key((string) $scene); if (!array_key_exists($scene, aksrt_captcha_scenes())) { return false; } $cfg = aksrt_get_captcha_config(); if (empty($cfg['enabled']) || $cfg['active_provider'] === '') { return false; } $provider = $cfg['active_provider']; $pcfg = $cfg[$provider] ?? []; if (!aksrt_captcha_provider_is_configured($provider, $pcfg)) { return false; } $enabled = isset($cfg['enabled_scenarios']) && is_array($cfg['enabled_scenarios']) ? $cfg['enabled_scenarios'] : array_values(array_diff(array_keys(aksrt_captcha_scenes()), (array) ($cfg['disabled_scenarios'] ?? []))); return in_array($scene, $enabled, true); } /** * 业务入口统一调用:未启用放行;启用则校验。 * * @param string $scene 场景 * @param array $payload 客户端提交的验证字段 * @return true|string 成功返回 true,失败返回错误信息字符串 */ function aksrt_require_captcha(string $scene, array $payload = []) { if (!aksrt_is_captcha_enabled_for($scene)) { return true; } return aksrt_verify_captcha($payload); } /** * 后台摘要:enabled / provider / configured / 场景命中状态。 */ function aksrt_get_captcha_status_summary(): array { $cfg = aksrt_get_captcha_config(); $provider = $cfg['active_provider']; $scenes = []; foreach (aksrt_captcha_scenes() as $key => $label) { $scenes[$key] = aksrt_is_captcha_enabled_for($key); } return [ 'enabled' => !empty($cfg['enabled']), 'provider' => $provider, 'configured' => $provider !== '' && aksrt_captcha_provider_is_configured($provider, $cfg[$provider] ?? []), 'scenes' => $scenes, ]; } // ============================================================ // 客户端真实 IP(穿透 CDN / 代理) // ============================================================ function aksrt_get_client_ip(): string { $keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP']; foreach ($keys as $key) { if (!empty($_SERVER[$key])) { $parts = explode(',', (string) $_SERVER[$key]); $ip = trim($parts[0]); if (filter_var($ip, FILTER_VALIDATE_IP)) { return $ip; } } } return isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; } // ============================================================ // 腾讯云 API 3.0 TC3-HMAC-SHA256 签名 // ============================================================ function aksrt_tencent_tc3_sign($secret_id, $secret_key, $service, $timestamp, $date, $action, $version, $payload): array { $content_type = 'application/json; charset=utf-8'; $host = 'captcha.tencentcloudapi.com'; $payload_hash = hash('sha256', $payload); $canonical_headers = "content-type:{$content_type}\nhost:{$host}\n"; $signed_headers = 'content-type;host'; $canonical_request = "POST\n/\n\n{$canonical_headers}\n{$signed_headers}\n{$payload_hash}"; $credential_scope = "{$date}/{$service}/tc3_request"; $string_to_sign = "TC3-HMAC-SHA256\n{$timestamp}\n{$credential_scope}\n" . hash('sha256', $canonical_request); $secret_date = hash_hmac('sha256', $date, 'TC3' . $secret_key, true); $secret_service = hash_hmac('sha256', $service, $secret_date, true); $secret_signing = hash_hmac('sha256', 'tc3_request', $secret_service, true); $signature = hash_hmac('sha256', $string_to_sign, $secret_signing); $authorization = 'TC3-HMAC-SHA256 ' . "Credential={$secret_id}/{$credential_scope}, " . "SignedHeaders={$signed_headers}, " . "Signature={$signature}"; return [ 'Authorization' => $authorization, 'Content-Type' => $content_type, 'Host' => $host, 'X-TC-Action' => $action, 'X-TC-Timestamp'=> (string) $timestamp, 'X-TC-Version' => $version, 'X-TC-Region' => '', ]; } /** * 阿里云 OpenAPI POP Signature v1.0 (HMAC-SHA1)。 */ function aksrt_aliyun_afs_sign(array $params, string $secret): string { ksort($params); $canonicalized = ''; foreach ($params as $k => $v) { $canonicalized .= '&' . rawurlencode((string) $k) . '=' . rawurlencode((string) $v); } $string_to_sign = 'POST&%2F&' . rawurlencode(substr($canonicalized, 1)); return base64_encode(hash_hmac('sha1', $string_to_sign, $secret . '&', true)); } // ============================================================ // 服务端二次校验 // ============================================================ /** * 校验客户端提交的人机验证字段。 * * @param array $post_data 客户端字段(captcha_verified + 厂商 token) * @return true|string 成功返回 true,失败返回错误信息 */ function aksrt_verify_captcha($post_data) { $cfg = aksrt_get_captcha_config(); if (empty($cfg['enabled']) || $cfg['active_provider'] === '') { return true; } $provider = $cfg['active_provider']; $pcfg = $cfg[$provider] ?? []; $client_ip = aksrt_get_client_ip(); // 客户端验证字段(前端写入 captcha_verified=1 表示用户已完成交互) if (($post_data['captcha_verified'] ?? '') !== '1') { return __('人机验证失败', 'aksrt'); } switch ($provider) { case 'tencent': $result = aksrt_verify_tencent($pcfg, $post_data, $client_ip); break; case 'cloudflare': $result = aksrt_verify_cloudflare($pcfg, $post_data, $client_ip); break; case 'geetest': $result = aksrt_verify_geetest($pcfg, $post_data); break; case 'aliyun': $result = aksrt_verify_aliyun($pcfg, $post_data); break; default: return true; } return $result === true ? true : ($result ?: __('人机验证失败', 'aksrt')); } /** * 腾讯云 Captcha 2.0(DescribeCaptchaResult + TC3-HMAC-SHA256)。 * * @return true|string */ function aksrt_verify_tencent(array $pcfg, array $post_data, string $client_ip) { $ticket = (string) ($post_data['tencent_ticket'] ?? ''); $randstr = (string) ($post_data['tencent_randstr'] ?? ''); if ($ticket === '' || $randstr === '') { return __('缺少验证参数', 'aksrt'); } if (str_starts_with($ticket, 'trerror_')) { return __('验证码 SDK 异常,请刷新页面重试(容灾票据)', 'aksrt'); } $secret_id = (string) ($pcfg['secret_id'] ?? ''); $secret_key = (string) ($pcfg['secret_key'] ?? ''); if ($secret_id === '' || $secret_key === '') { return __('验证码配置不完整:缺少云 API 密钥 (SecretId/SecretKey)', 'aksrt'); } $timestamp = time(); $date = gmdate('Y-m-d', $timestamp); $payload = wp_json_encode([ 'CaptchaType' => 9, 'CaptchaAppId' => (int) ($pcfg['app_id'] ?? 0), 'AppSecretKey' => (string) ($pcfg['app_secret_key'] ?? ''), 'Ticket' => $ticket, 'Randstr' => $randstr, 'UserIp' => $client_ip, ]); $headers = aksrt_tencent_tc3_sign($secret_id, $secret_key, 'captcha', $timestamp, $date, 'DescribeCaptchaResult', '2019-07-22', $payload); $curl_headers = []; foreach ($headers as $k => $v) { $curl_headers[] = "{$k}: {$v}"; } $ch = curl_init('https://captcha.tencentcloudapi.com/'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => $curl_headers, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_FOLLOWLOCATION => false, ]); $response_body = curl_exec($ch); $curl_error = curl_error($ch); curl_close($ch); if ($curl_error) { return '验证码服务请求失败: ' . $curl_error; } $body = json_decode((string) $response_body, true); if (isset($body['Response']['Error'])) { $err = $body['Response']['Error']; return '腾讯 API 调用失败: ' . ($err['Message'] ?? $err['Code'] ?? '未知错误'); } $resp = $body['Response'] ?? []; $captcha_code = (int) ($resp['CaptchaCode'] ?? 0); if ($captcha_code === 1) { return true; } $messages = [ 0 => '验证未通过', 7 => 'Randstr 不合法', 8 => 'Ticket 已过期(有效期 5 分钟)', 9 => 'Ticket 被重复使用', 15 => 'Ticket 不合法(解密失败)', 16 => 'CaptchaAppId 与 Ticket 不匹配', 21 => '票据校验异常(风控拦截)', 100 => '参数校验失败(AppId/SecretKey/Ticket 不匹配)', ]; $msg = $resp['CaptchaMsg'] ?? ($messages[$captcha_code] ?? '未知错误'); return '腾讯验证失败: ' . $msg . " (CaptchaCode: {$captcha_code})"; } /** * Cloudflare Turnstile siteverify。 * * @return true|string */ function aksrt_verify_cloudflare(array $pcfg, array $post_data, string $client_ip) { $token = (string) ($post_data['cf_turnstile_response'] ?? ''); if ($token === '' || strlen($token) < 10) { return __('缺少验证参数', 'aksrt'); } $secret = (string) ($pcfg['secret_key'] ?? ''); $bodies = [ ['secret' => $secret, 'response' => $token], ['secret' => $secret, 'response' => $token, 'remoteip' => $client_ip], ]; $body = []; foreach ($bodies as $attempt) { $resp = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [ 'timeout' => 10, 'body' => $attempt, ]); if (is_wp_error($resp)) { continue; } $body = json_decode(wp_remote_retrieve_body($resp), true); if (!empty($body['success'])) { return true; } } $codes = isset($body['error-codes']) && is_array($body['error-codes']) ? implode(', ', $body['error-codes']) : '未知错误'; return 'Cloudflare 验证失败: ' . $codes; } /** * 极验 GeeTest v4 validate。 * * @return true|string */ function aksrt_verify_geetest(array $pcfg, array $post_data) { $lot_number = (string) ($post_data['geetest_lot_number'] ?? ''); $captcha_output = (string) ($post_data['geetest_captcha_output'] ?? ''); $pass_token = (string) ($post_data['geetest_pass_token'] ?? ''); $gen_time = (string) ($post_data['geetest_gen_time'] ?? ''); if ($lot_number === '' || $captcha_output === '' || $pass_token === '') { return __('缺少验证参数', 'aksrt'); } $captcha_key = (string) ($pcfg['captcha_key'] ?? ''); $sign_token = hash_hmac('sha256', $lot_number, $captcha_key); $resp = wp_remote_post('https://gcaptcha4.geetest.com/validate', [ 'timeout' => 10, 'body' => [ 'captcha_id' => (string) ($pcfg['captcha_id'] ?? ''), 'lot_number' => $lot_number, 'captcha_output' => $captcha_output, 'pass_token' => $pass_token, 'gen_time' => $gen_time, 'sign_token' => $sign_token, ], ]); if (is_wp_error($resp)) { return '验证请求失败: ' . $resp->get_error_message(); } $body = json_decode(wp_remote_retrieve_body($resp), true); if (!empty($body['result']) && $body['result'] === 'success') { return true; } $reason = $body['reason'] ?? ($body['msg'] ?? '未知错误'); return '极验验证失败: ' . $reason; } /** * 阿里云 CAPTCHA 2.0 V3 (VerifyIntelligentCaptcha)。 * * @return true|string */ function aksrt_verify_aliyun(array $pcfg, array $post_data) { $captcha_verify_param = (string) ($post_data['aliyun_captcha_verify_param'] ?? ''); if ($captcha_verify_param === '') { return __('缺少验证参数 (captchaVerifyParam)', 'aksrt'); } $region = (string) ($pcfg['region'] ?? 'cn'); $endpoint = $region === 'sgp' ? 'captcha.ap-southeast-1.aliyuncs.com' : 'captcha.cn-shanghai.aliyuncs.com'; $params = [ 'AccessKeyId' => (string) ($pcfg['access_key_id'] ?? ''), 'Action' => 'VerifyIntelligentCaptcha', 'Format' => 'JSON', 'SignatureMethod' => 'HMAC-SHA1', 'SignatureVersion' => '1.0', 'SignatureNonce' => uniqid('', true), 'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), 'Version' => '2023-03-05', 'CaptchaVerifyParam' => $captcha_verify_param, ]; if (!empty($pcfg['scene_id'])) { $params['SceneId'] = (string) $pcfg['scene_id']; } $params['Signature'] = aksrt_aliyun_afs_sign($params, (string) ($pcfg['access_key_secret'] ?? '')); $resp = wp_remote_post('https://' . $endpoint . '/', [ 'timeout' => 10, 'body' => $params, ]); if (is_wp_error($resp)) { return '验证请求失败: ' . $resp->get_error_message(); } $body = json_decode(wp_remote_retrieve_body($resp), true); if (!empty($body['Success'])) { $verify_result = $body['Result']['VerifyResult'] ?? false; if ($verify_result === true) { return true; } $code_messages = [ 'F001' => '疑似攻击请求,风险策略不通过', 'F002' => '验证参数为空', 'F003' => '验证参数格式不合法', 'F004' => '测试模式配置为不通过', 'F005' => '场景 ID 不合法', 'F006' => '场景 ID 与配置不匹配', 'F008' => '验证数据重复提交', 'F009' => '检测到虚拟设备环境', 'F010' => '同 IP 访问频率超限', 'F011' => '同设备访问频率超限', 'F012' => 'SceneID 与前端配置不一致', 'F014' => '无初始化记录或已过期(20 分钟)', 'F015' => '验证交互不通过', 'F018' => 'CaptchaVerifyParam 重复使用', 'F019' => '验证请求超时(90 秒)', 'F020' => 'CaptchaVerifyParam 与场景 ID 不匹配', ]; $verify_code = (string) ($body['Result']['VerifyCode'] ?? ''); return '阿里云 V3 验证失败: ' . ($code_messages[$verify_code] ?? '未知错误 (' . ($verify_code ?: 'N/A') . ')'); } return '阿里云 V3 验证失败: ' . ($body['Message'] ?? '未知错误'); } // ============================================================ // 前端渲染(挂载点 + 内联 SDK + AKSRTCaptchaState) // ============================================================ /** * 渲染人机验证挂载点。未启用场景直接返回。 */ function aksrt_render_captcha($scene = 'comment') { $scene = sanitize_key((string) $scene); if (!aksrt_is_captcha_enabled_for($scene)) { return; } $cfg = aksrt_get_captcha_config(); $provider = $cfg['active_provider']; $pcfg = $cfg[$provider] ?? []; if (!aksrt_captcha_provider_is_configured($provider, $pcfg)) { if (current_user_can('manage_options')) { echo '
'; echo esc_html(sprintf( /* translators: %s: captcha provider key */ __('人机验证服务商「%s」已启用但凭据未配齐,前台已跳过渲染。请到「功能&权限 → 人机验证」补全配置。', 'aksrt'), $provider )); echo '
'; } return; } $uid = 'aksrt-captcha-' . $scene . '-' . wp_unique_id(); $vid = 'captcha_verified_' . $scene . '_' . wp_unique_id(); $js_config = [ 'provider' => $provider, 'scene' => $scene, 'uid' => $uid, 'vid' => $vid, 'tc' => ['app_id' => (string) ($pcfg['app_id'] ?? '')], 'cf' => [ 'site_key' => (string) ($pcfg['site_key'] ?? ''), 'theme' => (string) ($pcfg['theme'] ?? 'auto'), 'size' => (string) ($pcfg['size'] ?? 'normal'), ], 'gt' => [ 'captcha_id' => trim((string) ($pcfg['captcha_id'] ?? '')), 'product' => (string) ($pcfg['product'] ?? 'bind'), ], 'al' => [ 'region' => (string) ($pcfg['region'] ?? 'cn'), 'prefix' => (string) ($pcfg['prefix'] ?? ''), 'scene_id' => (string) ($pcfg['scene_id'] ?? ''), ], ]; $token_fields = ['cf_turnstile_response', 'tencent_ticket', 'tencent_randstr', 'tencent_sid', 'geetest_lot_number', 'geetest_captcha_output', 'geetest_pass_token', 'geetest_gen_time', 'aliyun_captcha_verify_param']; $token_fields_json = wp_json_encode($token_fields, JSON_UNESCAPED_UNICODE); ?>