配合 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,注册提交不加(邮箱验证码已是二次验证)
287 lines
8.9 KiB
JavaScript
287 lines
8.9 KiB
JavaScript
const form = document.getElementById('form');
|
||
const emailInput = document.getElementById('regemail');
|
||
const codeInput = document.getElementById('code');
|
||
const getCodeButton = document.getElementById('getCode');
|
||
const passwordInput = document.getElementById('regpsw');
|
||
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);
|
||
let remain = seconds;
|
||
getCodeButton.disabled = true;
|
||
getCodeButton.value = `${remain}s 后重发`;
|
||
cooldownTimer = setInterval(function () {
|
||
remain -= 1;
|
||
if (remain <= 0) {
|
||
clearInterval(cooldownTimer);
|
||
cooldownTimer = null;
|
||
getCodeButton.disabled = false;
|
||
getCodeButton.value = '获取验证码';
|
||
} else {
|
||
getCodeButton.value = `${remain}s 后重发`;
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
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();
|
||
|
||
function checkEmail() {
|
||
const result = FormValidators.email(emailInput.value);
|
||
emlTip.innerHTML = '';
|
||
emlTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||
emlTip.textContent = result.message;
|
||
emlTip.classList.add(result.className);
|
||
return result.valid ? result.value : false;
|
||
}
|
||
|
||
function checkPassword() {
|
||
const result = FormValidators.password(passwordInput.value, 6);
|
||
pwdTip.innerHTML = '';
|
||
pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||
|
||
if (!result.valid) {
|
||
pwdTip.textContent = result.message;
|
||
pwdTip.classList.add(result.className);
|
||
return false;
|
||
}
|
||
if (result.value !== repeatPasswordInput.value) {
|
||
pwdTip.textContent = '两次输入的密码不一致!';
|
||
pwdTip.classList.add('tip-yellow');
|
||
return false;
|
||
}
|
||
|
||
pwdTip.textContent = result.message;
|
||
pwdTip.classList.add(result.className);
|
||
return result.value;
|
||
}
|
||
|
||
function checkCode() {
|
||
const code = codeInput.value.trim();
|
||
codeTip.innerHTML = '';
|
||
codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||
|
||
if (!codeTarget) {
|
||
codeTip.textContent = '请先获取验证码!';
|
||
codeTip.classList.add('tip-red');
|
||
return false;
|
||
}
|
||
if (emailInput.value.trim() !== codeTarget) {
|
||
codeTip.textContent = '邮箱已修改,请重新获取验证码!';
|
||
codeTip.classList.add('tip-red');
|
||
return false;
|
||
}
|
||
if (!/^\d{6}$/.test(code)) {
|
||
codeTip.textContent = '验证码必须是6位数字!';
|
||
codeTip.classList.add('tip-yellow');
|
||
return false;
|
||
}
|
||
// 验证码正确性由服务端在注册提交时校验(一次性消费)
|
||
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);
|
||
|
||
passwordInput.addEventListener('input', checkPassword);
|
||
repeatPasswordInput.addEventListener('input', checkPassword);
|
||
|
||
passwordInput.addEventListener('blur', checkPassword);
|
||
repeatPasswordInput.addEventListener('blur', checkPassword);
|
||
|
||
emailInput.addEventListener('focus', checkEmail);
|
||
emailInput.addEventListener('input', function () {
|
||
checkEmail();
|
||
if (codeTarget) {
|
||
checkCode();
|
||
}
|
||
});
|
||
emailInput.addEventListener('blur', checkEmail);
|
||
|
||
codeInput.addEventListener('focus', checkCode);
|
||
codeInput.addEventListener('input', checkCode);
|
||
codeInput.addEventListener('blur', checkCode);
|
||
|
||
getCodeButton.addEventListener('click', async function () {
|
||
if (getCodeButton.disabled) return; // 防重复点击
|
||
const email = checkEmail();
|
||
if (!email) return;
|
||
const captchaAnswer = checkCaptcha();
|
||
if (!captchaAnswer) return;
|
||
|
||
getCodeButton.disabled = true;
|
||
codeTip.textContent = '验证码发送中…';
|
||
codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||
codeTip.classList.add('tip-yellow');
|
||
let httpStatus = 0;
|
||
try {
|
||
const res = await fetch('/register/send_code', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
email,
|
||
captcha_token: captchaToken,
|
||
captcha_answer: captchaAnswer
|
||
})
|
||
});
|
||
httpStatus = res.status;
|
||
const responseText = await res.text();
|
||
if (window.WafTip && WafTip.isBlocked(res, responseText)) throw new Error(WafTip.MESSAGE);
|
||
let result;
|
||
try { result = JSON.parse(responseText); } catch { throw new Error('服务器响应格式异常'); }
|
||
if (!res.ok || !result.success) throw new Error(result.message || '验证码发送失败');
|
||
|
||
codeTarget = email;
|
||
codeInput.value = '';
|
||
codeTip.textContent = '验证码已发送至邮箱,请查收(10 分钟内有效)';
|
||
codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||
codeTip.classList.add('tip-green');
|
||
startResendCooldown(60);
|
||
} catch (err) {
|
||
codeTip.textContent = err.message || '验证码发送失败,请稍后重试';
|
||
codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
|
||
codeTip.classList.add('tip-red');
|
||
if (httpStatus === 429) startResendCooldown(60); // 命中冷却/限流,倒计时对齐
|
||
} finally {
|
||
// CAPTCHA Token 一次性消费,无论成败都刷新
|
||
captchaInput.value = '';
|
||
refreshCaptcha();
|
||
if (!cooldownTimer) getCodeButton.disabled = false;
|
||
}
|
||
});
|
||
|
||
form.addEventListener('submit', async function (e) {
|
||
e.preventDefault();
|
||
|
||
const email = checkEmail();
|
||
const password = checkPassword();
|
||
const code = checkCode();
|
||
|
||
if (!email || !password || !code) {
|
||
return;
|
||
}
|
||
const jsonBody = JSON.stringify({
|
||
username: email,
|
||
password,
|
||
email_code: code
|
||
});
|
||
|
||
try {
|
||
submitButton.disabled = true;
|
||
const res = await fetch('/register', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded'
|
||
},
|
||
body: jsonBody
|
||
});
|
||
|
||
const responseText = await res.text();
|
||
if (window.WafTip && WafTip.isBlocked(res, responseText)) throw new Error(WafTip.MESSAGE);
|
||
let result;
|
||
|
||
try {
|
||
result = JSON.parse(responseText);
|
||
} catch {
|
||
console.error(`注册接口返回非 JSON 内容(HTTP ${res.status}):`, responseText);
|
||
throw new Error('服务器响应格式异常,请稍后重试');
|
||
}
|
||
|
||
if (!res.ok) {
|
||
throw new Error(result.message || `注册失败(HTTP ${res.status})`);
|
||
}
|
||
|
||
if (result.success) {
|
||
alert('注册成功');
|
||
form.reset();
|
||
codeTarget = '';
|
||
location.href = 'login.html';
|
||
return;
|
||
}
|
||
|
||
alert(result.message || '注册失败');
|
||
|
||
} catch (err) {
|
||
console.error('注册请求异常:', err);
|
||
alert(err.message || '接口请求失败,请检查后端服务是否启动');
|
||
} finally {
|
||
submitButton.disabled = false;
|
||
}
|
||
});
|