false, 'active_provider' => '', 'mode' => 'always', 'disabled_scenarios'=> array(), ); return get_option('aksrt_captcha_config', $default); } endif; if (!function_exists('aksrt_is_captcha_enabled_for')): /** * 检查指定场景是否启用人机验证 * * @param string $scene comment / login / register / lostpassword * @return bool */ function aksrt_is_captcha_enabled_for($scene) { $cfg = aksrt_get_captcha_config(); if (empty($cfg['enabled'])) return false; if (empty($cfg['active_provider'])) return false; $disabled = isset($cfg['disabled_scenarios']) ? $cfg['disabled_scenarios'] : array(); return !in_array($scene, $disabled); } endif; if (!function_exists('aksrt_is_native_auth_admin_whitelist')): /** * 判断当前请求是否属于原生认证后台白名单。 * * 前台登录/注册/找回密码已经由主题认证路由接管, * 这里仅保留 wp-login.php 场景作为后台认证白名单, * 避免影响 WordPress 后台登录、重新认证等原生流程。 * * @return bool */ function aksrt_is_native_auth_admin_whitelist() { return isset($GLOBALS['pagenow']) && $GLOBALS['pagenow'] === 'wp-login.php'; } endif; // ============================================================ // PHP 7.x 兼容辅助函数(替代 PHP 8.0+ 的 match 表达式) // ============================================================ if (!function_exists('aksrt_captcha_field_names')): /** * 获取指定验证服务商的字段名列表(替代 match) * * @param string $provider 服务商标识 * @return array 字段名数组 */ function aksrt_captcha_field_names($provider) { $map = array( 'tencent' => array('secret_id', 'secret_key', 'app_id', 'app_secret_key'), 'aliyun' => array('region', 'prefix', 'scene_id', 'access_key_id', 'access_key_secret'), 'cloudflare' => array('site_key', 'secret_key', 'domain', 'theme', 'size'), 'geetest' => array('captcha_id', 'captcha_key', 'product'), ); return isset($map[$provider]) ? $map[$provider] : array(); } endif; if (!function_exists('aksrt_captcha_required_fields')): /** * 获取指定验证服务商的必填字段列表(替代 match) * * @param string $provider 服务商标识 * @return array 必填字段名数组 */ function aksrt_captcha_required_fields($provider) { $map = array( 'tencent' => array('secret_id', 'secret_key', 'app_id', 'app_secret_key'), 'aliyun' => array('region', 'prefix', 'scene_id', 'access_key_id', 'access_key_secret'), 'cloudflare' => array('site_key', 'secret_key'), 'geetest' => array('captcha_id', 'captcha_key'), ); return isset($map[$provider]) ? $map[$provider] : array(); } endif; // ============================================================ // 人机验证:获取客户端真实IP(穿透CDN/代理) // ============================================================ if (!function_exists('aksrt_get_client_ip')) : function aksrt_get_client_ip() { // 按优先级检查代理头 $keys = array('HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP'); foreach ($keys as $key) { if (!empty($_SERVER[$key])) { $parts = explode(',', $_SERVER[$key]); $ip = trim($parts[0]); if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip; } } return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; } endif; // ============================================================ // 腾讯云 API 3.0 TC3-HMAC-SHA256 签名函数 // ============================================================ if (!function_exists('aksrt_tencent_tc3_sign')) : function aksrt_tencent_tc3_sign($secret_id, $secret_key, $service, $timestamp, $date, $action, $version, $payload) { // 1. 拼接签名原文 $http_request_method = 'POST'; $canonical_uri = '/'; $canonical_querystring = ''; $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 = "{$http_request_method}\n{$canonical_uri}\n{$canonical_querystring}\n{$canonical_headers}\n{$signed_headers}\n{$payload_hash}"; $algorithm = 'TC3-HMAC-SHA256'; $credential_scope = "{$date}/{$service}/tc3_request"; $string_to_sign = "{$algorithm}\n{$timestamp}\n{$credential_scope}\n" . hash('sha256', $canonical_request); // 2. 计算签名密钥链 $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); // 3. 计算签名 $signature = hash_hmac('sha256', $string_to_sign, $secret_signing); // 4. 组装 Authorization Header(包含 SecretId) $authorization = "{$algorithm} " . "Credential={$secret_id}/{$credential_scope}, " . "SignedHeaders={$signed_headers}, " . "Signature={$signature}"; return array( 'Authorization' => $authorization, 'Content-Type' => $content_type, 'Host' => $host, 'X-TC-Action' => $action, 'X-TC-Timestamp'=> (string) $timestamp, 'X-TC-Version' => $version, 'X-TC-Region' => '', ); } function aksrt_verify_captcha($post_data) { $cfg = aksrt_get_captcha_config(); if (empty($cfg['enabled']) || empty($cfg['active_provider'])) { return true; } $provider = $cfg['active_provider']; $pcfg = isset($cfg[$provider]) && is_array($cfg[$provider]) ? $cfg[$provider] : array(); $client_ip = aksrt_get_client_ip(); // 快速路径:客户端验证字段检查 $captcha_verified = isset($post_data['captcha_verified']) ? $post_data['captcha_verified'] : ''; if ($captcha_verified !== '1') { return __('人机验证失败', 'aksrt'); } $result = false; $error_msg = ''; switch ($provider) { // --- 腾讯云验证码 2.0 (API 3.0 DescribeCaptchaResult + TC3-HMAC-SHA256 签名) --- case 'tencent': $ticket = isset($post_data['tencent_ticket']) ? $post_data['tencent_ticket'] : ''; $randstr = isset($post_data['tencent_randstr']) ? $post_data['tencent_randstr'] : ''; if (!$ticket || !$randstr) { $error_msg = '缺少验证参数'; break; } // 检测容灾票据 (trerror_ 前缀表示 SDK 加载失败或异常) if (strpos($ticket, 'trerror_') === 0) { $error_msg = '验证码SDK异常,请刷新页面重试 (容灾票据)'; break; } // 检查是否配置了云 API 密钥(TC3 签名必须) $cloud_secret_id = isset($pcfg['secret_id']) ? $pcfg['secret_id'] : ''; $cloud_secret_key = isset($pcfg['secret_key']) ? $pcfg['secret_key'] : ''; if (!$cloud_secret_id || !$cloud_secret_key) { $error_msg = '验证码配置不完整:缺少云 API 密钥 (SecretId/SecretKey)'; break; } // API 3.0 DescribeCaptchaResult 请求参数 $tc_action = 'DescribeCaptchaResult'; $tc_version = '2019-07-22'; $tc_service = 'captcha'; $tc_host = 'captcha.tencentcloudapi.com'; $tc_timestamp = time(); $tc_date = gmdate('Y-m-d', $tc_timestamp); $tc_payload = wp_json_encode(array( 'CaptchaType' => 9, 'CaptchaAppId' => (int) (isset($pcfg['app_id']) ? $pcfg['app_id'] : 0), 'AppSecretKey' => isset($pcfg['app_secret_key']) ? $pcfg['app_secret_key'] : '', 'Ticket' => $ticket, 'Randstr' => $randstr, 'UserIp' => $client_ip, )); // 生成 TC3-HMAC-SHA256 签名(使用云 API 密钥) $headers = aksrt_tencent_tc3_sign( $cloud_secret_id, $cloud_secret_key, $tc_service, $tc_timestamp, $tc_date, $tc_action, $tc_version, $tc_payload ); // 使用 cURL 发起签名请求(避免 WordPress HTTP API 干扰 headers) $ch = curl_init("https://{$tc_host}/"); $curl_headers = array(); foreach ($headers as $k => $v) { $curl_headers[] = "{$k}: {$v}"; } curl_setopt_array($ch, array( CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_POSTFIELDS => $tc_payload, CURLOPT_HTTPHEADER => $curl_headers, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_FOLLOWLOCATION => false, )); $response_body = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curl_error = curl_error($ch); curl_close($ch); if ($curl_error) { $error_msg = '验证码服务请求失败: ' . $curl_error; break; } $body = json_decode(!empty($response_body) ? $response_body : '{}', true); // 检查 API 错误 if (isset($body['Response']['Error'])) { $api_err = $body['Response']['Error']; $err_code = isset($api_err['Code']) ? $api_err['Code'] : ''; $err_msg = isset($api_err['Message']) ? $api_err['Message'] : ''; $error_msg = '腾讯 API 调用失败: ' . (!empty($err_msg) ? $err_msg : (!empty($err_code) ? $err_code : '未知错误')); break; } $resp = isset($body['Response']) && is_array($body['Response']) ? $body['Response'] : array(); $captcha_code = (int) (isset($resp['CaptchaCode']) ? $resp['CaptchaCode'] : 0); // CaptchaCode: 1=验证通过, 其他=失败 if ($captcha_code === 1) { $result = true; } else { $last_code = $captcha_code; $last_msg = isset($resp['CaptchaMsg']) ? $resp['CaptchaMsg'] : ''; $messages = array( 0 => '验证未通过', 7 => 'Randstr不合法', 8 => 'Ticket已过期(有效期5分钟)', 9 => 'Ticket被重复使用', 15 => 'Ticket不合法(解密失败)', 16 => 'CaptchaAppId与Ticket不匹配', 21 => '票据校验异常(风控拦截)', 100 => '参数校验失败(AppId/SecretKey/Ticket不匹配)', ); $msg_text = !empty($last_msg) ? $last_msg : (isset($messages[$last_code]) ? $messages[$last_code] : '未知错误'); $error_msg = '腾讯验证失败: ' . $msg_text . " (CaptchaCode: {$last_code})"; } break; // --- Cloudflare Turnstile --- case 'cloudflare': $token = isset($post_data['cf_turnstile_response']) ? $post_data['cf_turnstile_response'] : ''; if (!$token) { $error_msg = '缺少验证参数'; break; } // Token 格式检查 (Turnstile token 通常是长字符串) if (strlen($token) < 10) { $error_msg = '验证码 token 格式异常'; break; } // 尝试方式1: 不带 remoteip(Cloudflare 自动使用服务器 IP) // 如果失败再尝试方式2: 带 remoteip $cf_attempts = array( 'without-remoteip' => array( 'body' => array( 'secret' => isset($pcfg['secret_key']) ? $pcfg['secret_key'] : '', 'response' => $token, ) ), 'with-remoteip' => array( 'body' => array( 'secret' => isset($pcfg['secret_key']) ? $pcfg['secret_key'] : '', 'response' => $token, 'remoteip' => $client_ip, ) ), ); foreach ($cf_attempts as $mode => $attempt) { $resp = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', array( 'timeout' => 10, 'body' => $attempt['body'], )); if (is_wp_error($resp)) { continue; } $raw_body = wp_remote_retrieve_body($resp); $body = json_decode($raw_body, true); if (!empty($body['success'])) { $result = true; break; } } if (!$result) { $error_codes = isset($body['error-codes']) && is_array($body['error-codes']) ? $body['error-codes'] : array(); $error_codes_str = implode(', ', $error_codes); $error_msg = 'Cloudflare验证失败: ' . (!empty($error_codes_str) ? $error_codes_str : '未知错误'); } break; // --- 极验 GeeTest v4 --- case 'geetest': $lot_number = isset($post_data['geetest_lot_number']) ? $post_data['geetest_lot_number'] : ''; $captcha_output = isset($post_data['geetest_captcha_output']) ? $post_data['geetest_captcha_output'] : ''; $pass_token = isset($post_data['geetest_pass_token']) ? $post_data['geetest_pass_token'] : ''; $gen_time = isset($post_data['geetest_gen_time']) ? $post_data['geetest_gen_time'] : ''; if (!$lot_number || !$captcha_output || !$pass_token) { $error_msg = '缺少验证参数'; break; } // 极验 v4 要求使用 HMAC-SHA256 签名(非普通 SHA256) $captcha_key_val = isset($pcfg['captcha_key']) ? $pcfg['captcha_key'] : ''; $sign_token = hash_hmac('sha256', $lot_number, $captcha_key_val); $resp = wp_remote_post('https://gcaptcha4.geetest.com/validate', array( 'timeout' => 10, 'body' => array( 'captcha_id' => isset($pcfg['captcha_id']) ? $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)) { $error_msg = '验证请求失败: ' . $resp->get_error_message(); break; } $raw_body = wp_remote_retrieve_body($resp); $body = json_decode($raw_body, true); if (!empty($body['result']) && $body['result'] === 'success') { $result = true; } else { $reason = isset($body['reason']) ? $body['reason'] : ''; $bmsg = isset($body['msg']) ? $body['msg'] : ''; $error_msg = '极验验证失败: ' . (!empty($reason) ? $reason : (!empty($bmsg) ? $bmsg : '未知错误')); } break; // --- 阿里云 CAPTCHA 2.0 V3 (VerifyIntelligentCaptcha) --- case 'aliyun': $captcha_verify_param = isset($post_data['aliyun_captcha_verify_param']) ? $post_data['aliyun_captcha_verify_param'] : ''; if (!$captcha_verify_param) { $error_msg = '缺少验证参数 (captchaVerifyParam)'; break; } // 确定地域对应的 API 端点 $region = isset($pcfg['region']) ? $pcfg['region'] : 'cn'; $endpoint = $region === 'sgp' ? 'captcha.ap-southeast-1.aliyuncs.com' : 'captcha.cn-shanghai.aliyuncs.com'; $params = array( 'AccessKeyId' => isset($pcfg['access_key_id']) ? $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, ); // 可选: 传入 SceneId 校验一致性(防止前端篡改) if (!empty($pcfg['scene_id'])) { $params['SceneId'] = $pcfg['scene_id']; } $ak_secret = isset($pcfg['access_key_secret']) ? $pcfg['access_key_secret'] : ''; $params['Signature'] = aksrt_aliyun_afs_sign($params, $ak_secret); $resp = wp_remote_post('https://' . $endpoint . '/', array( 'timeout' => 10, 'body' => $params, )); if (is_wp_error($resp)) { $error_msg = '验证请求失败: ' . $resp->get_error_message(); break; } $body = json_decode(wp_remote_retrieve_body($resp), true); if (!empty($body['Success'])) { $verify_result = isset($body['Result']['VerifyResult']) ? $body['Result']['VerifyResult'] : false; $verify_code = isset($body['Result']['VerifyCode']) ? $body['Result']['VerifyCode'] : ''; if ($verify_result === true) { $result = true; } else { // 根据 VerifyCode 映射错误信息 $code_messages = array( '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不匹配', ); $vc_display = !empty($verify_code) ? $verify_code : 'N/A'; $code_msg = isset($code_messages[$verify_code]) ? $code_messages[$verify_code] : ('未知错误 (' . $vc_display . ')'); $error_msg = '阿里云V3验证失败: ' . $code_msg; } } else { $amsg = isset($body['Message']) ? $body['Message'] : ''; $error_msg = '阿里云V3验证失败: ' . (!empty($amsg) ? $amsg : '未知错误'); } break; default: return true; } if ($result) { return true; } return __('人机验证失败', 'aksrt'); } endif; /** * 阿里云 OpenAPI 签名函数 (POP Signature v1.0 / HMAC-SHA1) * 同时适用于 CAPTCHA 2.0 V3 (VerifyIntelligentCaptcha) 和旧版 AFS */ if (!function_exists('aksrt_aliyun_afs_sign')) : function aksrt_aliyun_afs_sign($params, $secret) { ksort($params); $canonicalized = ''; foreach ($params as $k => $v) { $canonicalized .= '&' . rawurlencode($k) . '=' . rawurlencode($v); } $string_to_sign = 'POST&%2F&' . rawurlencode(substr($canonicalized, 1)); $signature = base64_encode(hash_hmac('sha1', $string_to_sign, $secret . '&', true)); return $signature; } function aksrt_render_captcha($scene = 'comment') { $cfg = aksrt_get_captcha_config(); if (empty($cfg['enabled']) || empty($cfg['active_provider'])) return; if (!aksrt_is_captcha_enabled_for($scene)) return; $provider = $cfg['active_provider']; $pcfg = isset($cfg[$provider]) && is_array($cfg[$provider]) ? $cfg[$provider] : array(); $uid = 'aksrt-captcha-' . $scene . '-' . wp_unique_id(); $vid = 'captcha_verified_' . $scene . '_' . wp_unique_id(); // 构建 JS 配置 $js_config = array( 'provider' => $provider, 'scene' => $scene, 'uid' => $uid, 'vid' => $vid, 'tc' => array('app_id' => isset($pcfg['app_id']) ? $pcfg['app_id'] : ''), 'cf' => array( 'site_key' => isset($pcfg['site_key']) ? $pcfg['site_key'] : '', 'theme' => isset($pcfg['theme']) ? $pcfg['theme'] : 'auto', 'size' => isset($pcfg['size']) ? $pcfg['size'] : 'normal' ), 'gt' => array( 'captcha_id' => isset($pcfg['captcha_id']) ? $pcfg['captcha_id'] : '', 'product' => isset($pcfg['product']) ? $pcfg['product'] : 'popup' ), 'al' => array( 'region' => isset($pcfg['region']) ? $pcfg['region'] : 'cn', 'prefix' => isset($pcfg['prefix']) ? $pcfg['prefix'] : '', 'scene_id' => isset($pcfg['scene_id']) ? $pcfg['scene_id'] : '' ), ); ?>
'; ?>