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:
1 parent
d2e95acd75
commit
ff4ec1eb43
45 files changed
+2694
-1435
No files matched your search
+38
-11
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user