From 6ee1eca7898af62d9771b0a7168c0a02273c2483 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Sat, 1 Aug 2026 02:42:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(static):=20=E6=B3=A8=E5=86=8C=E9=A1=B5?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E9=AA=8C=E8=AF=81=E7=A0=81=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E5=9B=BE=E5=BD=A2=E9=AA=8C=E8=AF=81=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 配合 gateway v0.5.4 邮件轰炸防护(/register/send_code 强制 CAPTCHA): - register.html: 邮箱与邮箱验证码之间新增图形验证码区块(图片+输入框,复用 login 页结构) - register.js: - 新增 refreshCaptcha() 复用 /captcha/image 代理路由(与 login.js 一致) - 新增 checkCaptcha() 校验图形验证码非空且 Token 已加载 - 获取验证码按钮点击时先校验图形验证码,再携带 captcha_token + captcha_answer 发送 - 无论发送成败都刷新图形验证码(Token 一次性消费) - 重置表单时清空图形验证码并刷新 方案 A:仅在「获取验证码」时要求 CAPTCHA,注册提交不加(邮箱验证码已是二次验证) --- static/public/js/register.js | 70 +++++++++++++++++++++++++++++++++--- static/public/register.html | 12 +++++-- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/static/public/js/register.js b/static/public/js/register.js index 298a93c..f022257 100644 --- a/static/public/js/register.js +++ b/static/public/js/register.js @@ -7,11 +7,45 @@ const repeatPasswordInput = document.getElementById('repwd'); const emlTip = document.getElementById('emlTip'); const pwdTip = document.getElementById('pwdTip'); const codeTip = document.getElementById('codeTip'); +const captchaImg = document.getElementById('captchaImg'); +const captchaInput = document.getElementById('captchaAnswer'); +const captchaTip = document.getElementById('captchaTip'); const submitButton = document.getElementById('formsub'); let codeTarget = ''; // 已发送验证码的目标邮箱(验证码本体由服务端校验,前端不经手) let cooldownTimer = null; // 重发倒计时(与服务端同邮箱 60s 发送冷却对齐) +/* ── 图形验证码(gateway 代理 captcha 服务:GET /captcha/image) ── */ +let captchaToken = ''; +let captchaLoading = false; // 防重复点击:加载中忽略再次触发 + +async function refreshCaptcha() { + if (captchaLoading) return; + captchaLoading = true; + captchaToken = ''; + try { + const res = await fetch('/captcha/image', { cache: 'no-store' }); + if (res.status === 403) { // WAF 拦截:body 非图片,提示后待用户点击重试 + captchaImg.removeAttribute('src'); + captchaImg.alt = '验证码加载被安全规则拦截,点击重试'; + return; + } + if (!res.ok) throw new Error('captcha load failed'); + captchaToken = res.headers.get('X-Captcha-Token') || ''; + const blob = await res.blob(); + if (captchaImg.src && captchaImg.src.startsWith('blob:')) URL.revokeObjectURL(captchaImg.src); + captchaImg.src = URL.createObjectURL(blob); + captchaImg.alt = '点击刷新'; + } catch (e) { + captchaImg.removeAttribute('src'); + captchaImg.alt = '加载失败,点击重试'; + } finally { + captchaLoading = false; + } +} +captchaImg.addEventListener('click', refreshCaptcha); +refreshCaptcha(); + // 发送成功后进入倒计时,避免用户触发服务端同邮箱 60s 发送冷却 function startResendCooldown(seconds) { clearInterval(cooldownTimer); @@ -34,16 +68,20 @@ function startResendCooldown(seconds) { form.addEventListener('reset', function () { clearTipStyle(); codeTarget = ''; + captchaInput.value = ''; + refreshCaptcha(); }); function clearTipStyle() { emlTip.innerHTML = ''; pwdTip.innerHTML = ''; codeTip.innerHTML = ''; + captchaTip.innerHTML = ''; emlTip.classList.remove('tip-red', 'tip-yellow', 'tip-green'); pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green'); codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green'); + captchaTip.classList.remove('tip-red', 'tip-yellow', 'tip-green'); } clearTipStyle(); @@ -101,6 +139,23 @@ function checkCode() { return code; } +function checkCaptcha() { + captchaTip.innerHTML = ''; + captchaTip.classList.remove('tip-red', 'tip-yellow', 'tip-green'); + const v = captchaInput.value.trim(); + if (!v) { + captchaTip.textContent = '请输入图形验证码!'; + captchaTip.classList.add('tip-red'); + return false; + } + if (!captchaToken) { + captchaTip.textContent = '图形验证码未加载,请点击图片刷新'; + captchaTip.classList.add('tip-red'); + return false; + } + return v; +} + passwordInput.addEventListener('focus', checkPassword); repeatPasswordInput.addEventListener('focus', checkPassword); @@ -126,9 +181,9 @@ codeInput.addEventListener('blur', checkCode); getCodeButton.addEventListener('click', async function () { if (getCodeButton.disabled) return; // 防重复点击 const email = checkEmail(); - if (!email) { - return; - } + if (!email) return; + const captchaAnswer = checkCaptcha(); + if (!captchaAnswer) return; getCodeButton.disabled = true; codeTip.textContent = '验证码发送中…'; @@ -139,7 +194,11 @@ getCodeButton.addEventListener('click', async function () { const res = await fetch('/register/send_code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email }) + body: JSON.stringify({ + email, + captcha_token: captchaToken, + captcha_answer: captchaAnswer + }) }); httpStatus = res.status; const responseText = await res.text(); @@ -160,6 +219,9 @@ getCodeButton.addEventListener('click', async function () { codeTip.classList.add('tip-red'); if (httpStatus === 429) startResendCooldown(60); // 命中冷却/限流,倒计时对齐 } finally { + // CAPTCHA Token 一次性消费,无论成败都刷新 + captchaInput.value = ''; + refreshCaptcha(); if (!cooldownTimer) getCodeButton.disabled = false; } }); diff --git a/static/public/register.html b/static/public/register.html index a9c0241..093dadd 100644 --- a/static/public/register.html +++ b/static/public/register.html @@ -13,12 +13,18 @@
-
验证码: + + +
+ + 加载中… +
+ +
邮箱验证码:
- @@ -36,6 +42,6 @@ 蜀ICP备2024107064号-2 - +