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:
kate522 committed 2026-09-02 13:03:24 +08:00
1 parent d2e95acd75
commit ff4ec1eb43
45 files changed
+2694 -1435

No files matched your search

+38 -11
View File
@@ -102,10 +102,19 @@ final class WeixinGzh {
if (is_wp_error($context)) {
return $this->error_response($context->get_error_message(), 403);
}
$config = self::settings();
if ((string) ($config['gzh_type'] ?? '') === 'subscribe') {
return $this->code_mode_response($config, $context);
}
$config = self::settings();
$mode = (string) ($config['gzh_type'] ?? '');
if ($context['action'] === 'bind') {
$data = $this->request_data($request);
$bind_pass = sanitize_text_field((string) ($data['bind_pass'] ?? ''));
if (!OAuth::validate_bind_pass($bind_pass, 'weixingzh', $context['user_id'], false)) {
return $this->error_response(__('请先完成人机验证后再绑定', 'aksrt'), 403);
}
$context['bind_pass'] = $bind_pass;
}
if ($mode === 'subscribe') {
return $this->code_mode_response($config, $context);
}
$appid = trim((string) ($config['appid'] ?? ''));
$secret = trim((string) ($config['appsecret'] ?? ''));
@@ -118,9 +127,10 @@ final class WeixinGzh {
'openid' => '',
'profile' => [],
'redirect_to' => $context['redirect_to'],
'action' => $context['action'],
'user_id' => $context['user_id'],
], self::SCAN_TTL, true);
'action' => $context['action'],
'user_id' => $context['user_id'],
'bind_pass' => (string) ($context['bind_pass'] ?? ''),
], self::SCAN_TTL, true);
if (!$this->store_scene_mapping($scene, $transaction)) {
throw new \RuntimeException('scene_mapping_failed');
}
@@ -211,11 +221,21 @@ final class WeixinGzh {
if (is_wp_error($context)) {
return $this->error_response($context->get_error_message(), 403);
}
$bind_pass = '';
if ($context['action'] === 'bind') {
$bind_pass = sanitize_text_field((string) ($data['bind_pass'] ?? ''));
if (!OAuth::validate_bind_pass($bind_pass, 'weixingzh', $context['user_id'], false)) {
return $this->error_response(__('请先完成人机验证后再绑定', 'aksrt'), 403);
}
}
$claimed = $this->claim_code_transaction($code, $context);
if (!$claimed) {
return $this->error_response(__('验证码无效、已过期或尝试次数过多', 'aksrt'), 400);
}
$payload = is_array($claimed['payload'] ?? null) ? $claimed['payload'] : [];
if ($context['action'] === 'bind') {
$payload['bind_pass'] = $bind_pass;
}
return $this->complete_browser_identity($payload);
}
@@ -297,9 +317,12 @@ final class WeixinGzh {
$html .= '<form class="aksrt-weixingzh-code-form" data-aksrt-weixingzh-code>';
$html .= '<p class="muted-2-color em09">' . esc_html(sprintf(__('关注公众号后发送「%s」获取验证码', 'aksrt'), $keyword)) . '</p>';
$html .= '<input type="hidden" name="action" value="' . esc_attr($context['action']) . '" />';
$html .= '<input type="hidden" name="user_id" value="' . esc_attr((string) $context['user_id']) . '" />';
$html .= '<input type="hidden" name="redirect_to" value="' . esc_attr($context['redirect_to']) . '" />';
$html .= '<input type="text" name="code" class="form-control" minlength="6" maxlength="12" autocomplete="one-time-code" placeholder="' . esc_attr__('请输入验证码', 'aksrt') . '" required />';
$html .= '<input type="hidden" name="user_id" value="' . esc_attr((string) $context['user_id']) . '" />';
$html .= '<input type="hidden" name="redirect_to" value="' . esc_attr($context['redirect_to']) . '" />';
if (!empty($context['bind_pass'])) {
$html .= '<input type="hidden" name="bind_pass" value="' . esc_attr((string) $context['bind_pass']) . '" />';
}
$html .= '<input type="text" name="code" class="form-control" minlength="6" maxlength="12" autocomplete="one-time-code" placeholder="' . esc_attr__('请输入验证码', 'aksrt') . '" required />';
$html .= '<button type="submit" class="but c-blue btn-block mt10">' . esc_html__('登录', 'aksrt') . '</button>';
$html .= '</form>';
return new \WP_REST_Response([
@@ -374,6 +397,10 @@ final class WeixinGzh {
if (!$this->binding_user_matches($payload)) {
return $this->error_response(__('绑定会话与当前用户不匹配', 'aksrt'), 403);
}
$bind_pass = sanitize_text_field((string) ($payload['bind_pass'] ?? ''));
if (!OAuth::validate_bind_pass($bind_pass, 'weixingzh', get_current_user_id(), true)) {
return $this->error_response(__('绑定验证已过期,请重新完成人机验证', 'aksrt'), 403);
}
$result = OAuthAccountService::bind(get_current_user_id(), 'weixingzh', $identity);
if (is_wp_error($result)) {
return $this->error_response($result->get_error_message(), 409);
@@ -382,7 +409,7 @@ final class WeixinGzh {
$redirect = add_query_arg(['oauth_action' => 'bind', 'oauth_provider' => 'weixingzh'], $redirect);
return new \WP_REST_Response([
'success' => true,
'data' => ['logged_in' => false, 'bound' => true, 'status' => 'bound', 'redirect' => $redirect],
'data' => ['logged_in' => false, 'bound' => true, 'status' => 'bound', 'redirect' => $redirect],
], 200);
}