feat(captcha): 打通人机验证 11 个场景的前后端完整链路
- 统一验证入口 aksrt_require_captcha(),返回 true|WP_Error,启用场景配置缺失时 fail-closed 拒绝 - 前端公共 API 重构为 window.AKSRTCaptcha(verify/collect/reset/run),单飞锁防并发重复验证,一次性 proof 绑定浏览器 cookie+场景+服务商+主机名 - challenge/exchange REST 端点:challenge 原子占用防票据放大厂商调用,增加频率限制 - 新增 OAuth bind pass:服务端签发 5 分钟一次性令牌,绑定用户+服务商+浏览器,微信扫码延迟消费 - 覆盖场景:评论、登录、注册、绑定第三方、绑定手机、找回密码、修改密码、更改邮箱、资料下载、签到、前台投稿 - 移除 Service 层重复验证与 _auth_code_verified/_defer_login 等客户端可伪造标志,删除下载共享 pass 死代码 - 修复腾讯 TC3 签名 scope、Cloudflare action/cdata/hostname 严格校验、阿里云验签错误码映射 - 后台 Schema 保存对 secret 字段做掩码/保留合并;导入导出过滤 aksrt_captcha_config - 重建 public bundle 与 admin SPA;新增 captcha 运行时/安全/业务契约测试与 harness
This commit is contained in:
+104
-22
@@ -140,10 +140,8 @@ class SchemaProvider {
|
||||
}
|
||||
}
|
||||
if (!empty($data['__clear_secrets']) && is_array($data['__clear_secrets'])) {
|
||||
$clean['__clear_secrets'] = array_values(array_intersect(
|
||||
array_map('sanitize_key', $data['__clear_secrets']),
|
||||
$this->secret_field_keys($schema)
|
||||
));
|
||||
$requested = array_values(array_filter(array_map([$this, 'normalize_secret_path'], $data['__clear_secrets'])));
|
||||
$clean['__clear_secrets'] = array_values(array_intersect($requested, $this->secret_field_paths($schema)));
|
||||
}
|
||||
|
||||
return $clean;
|
||||
@@ -505,14 +503,14 @@ class SchemaProvider {
|
||||
if ($section !== '') {
|
||||
$defaults = $this->get_defaults($domain, $section);
|
||||
$schema = $this->get_section_schema($domain, $section);
|
||||
$defaults['__clear_secrets'] = $schema ? $this->secret_field_keys($schema) : [];
|
||||
$defaults['__clear_secrets'] = $schema ? $this->secret_field_paths($schema) : [];
|
||||
return $this->write_settings($domain, $defaults, $section);
|
||||
}
|
||||
|
||||
$result = true;
|
||||
foreach ($this->get_sections($domain) as $section_schema) {
|
||||
$defaults = $this->collect_defaults($section_schema);
|
||||
$defaults['__clear_secrets'] = $this->secret_field_keys($section_schema);
|
||||
$defaults['__clear_secrets'] = $this->secret_field_paths($section_schema);
|
||||
$result = $this->write_section_settings($section_schema, $defaults) && $result;
|
||||
}
|
||||
return $result;
|
||||
@@ -541,24 +539,33 @@ class SchemaProvider {
|
||||
}
|
||||
|
||||
$data = $this->collect_defaults($schema);
|
||||
foreach ($this->get_all_fields($schema) as $field) {
|
||||
if (array_key_exists($field['key'], $stored)) {
|
||||
$data[$field['key']] = $stored[$field['key']];
|
||||
}
|
||||
foreach ($this->get_all_fields($schema) as $field) {
|
||||
if (array_key_exists($field['key'], $stored)) {
|
||||
$data[$field['key']] = $stored[$field['key']];
|
||||
}
|
||||
$stored_value = array_key_exists($field['key'], $stored) ? $stored[$field['key']] : ($data[$field['key']] ?? '');
|
||||
if (!empty($field['secret'])) {
|
||||
$data[$field['key']] = trim((string) $stored_value) !== '' ? '***' : '';
|
||||
}
|
||||
}
|
||||
if (!empty($field['captcha_provider'])) {
|
||||
$data[$field['key']] = $this->mask_captcha_provider_secrets(
|
||||
(string) $field['captcha_provider'],
|
||||
is_array($stored_value) ? $stored_value : []
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Captcha: always surface the normalized positive scene list.
|
||||
if ($option_key === 'aksrt_captcha_config' && function_exists('aksrt_get_captcha_config')) {
|
||||
$normalized = aksrt_get_captcha_config();
|
||||
foreach (['enabled', 'active_provider', 'enabled_scenarios', 'tencent', 'aliyun', 'cloudflare', 'geetest'] as $key) {
|
||||
if (array_key_exists($key, $normalized)) {
|
||||
$data[$key] = $normalized[$key];
|
||||
}
|
||||
}
|
||||
foreach (['enabled', 'active_provider', 'enabled_scenarios'] as $key) {
|
||||
if (array_key_exists($key, $normalized)) {
|
||||
$data[$key] = $normalized[$key];
|
||||
}
|
||||
}
|
||||
foreach (function_exists('aksrt_captcha_providers') ? aksrt_captcha_providers() : [] as $provider) {
|
||||
$data[$provider] = $this->mask_captcha_provider_secrets($provider, (array) ($normalized[$provider] ?? []));
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
@@ -576,13 +583,20 @@ class SchemaProvider {
|
||||
$fields[(string) $field['key']] = $field;
|
||||
}
|
||||
$clear_secrets = isset($data['__clear_secrets']) && is_array($data['__clear_secrets'])
|
||||
? array_values(array_map('sanitize_key', $data['__clear_secrets']))
|
||||
? array_values(array_filter(array_map([$this, 'normalize_secret_path'], $data['__clear_secrets'])))
|
||||
: [];
|
||||
foreach ($fields as $key => $field) {
|
||||
if (!empty($field['secret']) && in_array($key, $clear_secrets, true)) {
|
||||
unset($stored[$key]);
|
||||
continue;
|
||||
}
|
||||
if (!empty($field['captcha_provider'])) {
|
||||
$provider = (string) $field['captcha_provider'];
|
||||
$current = isset($stored[$key]) && is_array($stored[$key]) ? $stored[$key] : [];
|
||||
$incoming = array_key_exists($key, $data) && is_array($data[$key]) ? $data[$key] : [];
|
||||
$stored[$key] = $this->merge_captcha_provider_config($provider, $current, $incoming, $clear_secrets);
|
||||
continue;
|
||||
}
|
||||
if (!array_key_exists($key, $data)) {
|
||||
continue;
|
||||
}
|
||||
@@ -599,9 +613,17 @@ class SchemaProvider {
|
||||
|
||||
// Captcha stores a single positive scene list.
|
||||
if ($option_key === 'aksrt_captcha_config' && function_exists('aksrt_captcha_scenes')) {
|
||||
$stored['enabled'] = !empty($stored['enabled']);
|
||||
$active_provider = sanitize_key((string) ($stored['active_provider'] ?? ''));
|
||||
$stored['active_provider'] = in_array($active_provider, aksrt_captcha_providers(), true) ? $active_provider : '';
|
||||
$stored['enabled_scenarios'] = function_exists('aksrt_captcha_sanitize_scene_list')
|
||||
? aksrt_captcha_sanitize_scene_list($stored['enabled_scenarios'] ?? [])
|
||||
: (array) ($stored['enabled_scenarios'] ?? []);
|
||||
foreach (aksrt_captcha_providers() as $provider) {
|
||||
$stored[$provider] = function_exists('aksrt_captcha_sanitize_provider_config')
|
||||
? aksrt_captcha_sanitize_provider_config($provider, $stored[$provider] ?? [])
|
||||
: (array) ($stored[$provider] ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
$updated = update_option($option_key, $stored);
|
||||
@@ -617,14 +639,69 @@ class SchemaProvider {
|
||||
return $updated || true;
|
||||
}
|
||||
|
||||
private function secret_field_keys(array $schema): array {
|
||||
$keys = [];
|
||||
private function normalize_secret_path($path): string {
|
||||
$parts = array_values(array_filter(explode('.', strtolower(trim((string) $path))), static fn(string $part): bool => $part !== ''));
|
||||
$parts = array_map('sanitize_key', $parts);
|
||||
return in_array('', $parts, true) ? '' : implode('.', $parts);
|
||||
}
|
||||
|
||||
private function secret_field_paths(array $schema): array {
|
||||
$paths = [];
|
||||
foreach ($this->get_all_fields($schema) as $field) {
|
||||
if (!empty($field['secret']) && !empty($field['key'])) {
|
||||
$keys[] = sanitize_key((string) $field['key']);
|
||||
$key = sanitize_key((string) ($field['key'] ?? ''));
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
if (!empty($field['secret'])) {
|
||||
$paths[] = $key;
|
||||
}
|
||||
if (!empty($field['captcha_provider']) && function_exists('aksrt_captcha_provider_secret_fields')) {
|
||||
foreach (aksrt_captcha_provider_secret_fields((string) $field['captcha_provider']) as $secret) {
|
||||
$paths[] = $key . '.' . sanitize_key((string) $secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
return array_values(array_unique($keys));
|
||||
return array_values(array_unique($paths));
|
||||
}
|
||||
|
||||
private function mask_captcha_provider_secrets(string $provider, array $config): array {
|
||||
$clean = function_exists('aksrt_captcha_sanitize_provider_config')
|
||||
? aksrt_captcha_sanitize_provider_config($provider, $config)
|
||||
: $config;
|
||||
foreach (function_exists('aksrt_captcha_provider_secret_fields') ? aksrt_captcha_provider_secret_fields($provider) : [] as $secret) {
|
||||
$clean[$secret] = trim((string) ($config[$secret] ?? '')) !== '' ? '***' : '';
|
||||
}
|
||||
return $clean;
|
||||
}
|
||||
|
||||
private function merge_captcha_provider_config(string $provider, array $stored, array $incoming, array $clear_paths): array {
|
||||
$current = function_exists('aksrt_captcha_sanitize_provider_config')
|
||||
? aksrt_captcha_sanitize_provider_config($provider, $stored)
|
||||
: $stored;
|
||||
$defaults = function_exists('aksrt_captcha_provider_defaults') ? aksrt_captcha_provider_defaults($provider) : [];
|
||||
$secret_fields = function_exists('aksrt_captcha_provider_secret_fields') ? aksrt_captcha_provider_secret_fields($provider) : [];
|
||||
foreach (array_keys($defaults) as $key) {
|
||||
$path = $provider . '.' . $key;
|
||||
if (in_array($path, $clear_paths, true)) {
|
||||
$current[$key] = '';
|
||||
continue;
|
||||
}
|
||||
if (!array_key_exists($key, $incoming) || is_array($incoming[$key]) || is_object($incoming[$key])) {
|
||||
continue;
|
||||
}
|
||||
$value = trim((string) $incoming[$key]);
|
||||
if (in_array($key, $secret_fields, true) && ($value === '' || $value === '***')) {
|
||||
continue;
|
||||
}
|
||||
$current[$key] = $value;
|
||||
}
|
||||
return function_exists('aksrt_captcha_sanitize_provider_config')
|
||||
? aksrt_captcha_sanitize_provider_config($provider, $current)
|
||||
: $current;
|
||||
}
|
||||
|
||||
private function secret_field_keys(array $schema): array {
|
||||
return array_values(array_filter($this->secret_field_paths($schema), static fn(string $path): bool => !str_contains($path, '.')));
|
||||
}
|
||||
|
||||
private function collect_defaults(array $schema): array {
|
||||
@@ -721,6 +798,11 @@ class SchemaProvider {
|
||||
}
|
||||
|
||||
private function sanitize_field_value($value, array $field) {
|
||||
if (!empty($field['captcha_provider'])) {
|
||||
return function_exists('aksrt_captcha_sanitize_provider_config')
|
||||
? aksrt_captcha_sanitize_provider_config((string) $field['captcha_provider'], $value)
|
||||
: [];
|
||||
}
|
||||
$type = (string) ($field['type'] ?? 'text');
|
||||
$sanitize_type = (string) ($field['sanitize_type'] ?? '');
|
||||
if ($sanitize_type === 'business_prefix') {
|
||||
|
||||
Reference in New Issue
Block a user