From 6fcb70d235bb4fd937b6feb923517ef67a5c7ce1 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Tue, 28 Jul 2026 10:15:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=99=BB=E5=BD=95=E9=A1=B5=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E5=9B=BE=E5=BD=A2=E9=AA=8C=E8=AF=81=E7=A0=81=EF=BC=8C?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=8E=A5=E5=8F=A3=E5=BC=BA=E5=88=B6=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 登录页展示 captcha 图形码(点击刷新),登录请求携带 captcha_token/captcha_answer;gateway 的 /login 启用验证码校验,经 pymsg 调 captcha verify 路由(fail-closed),补齐防爆破短板。 --- gateway/src/main.cpp | 41 ++++++++++++++++++++-------- static/public/js/login.js | 56 +++++++++++++++++++++++++++++++++++++-- static/public/login.html | 4 +++ 3 files changed, 88 insertions(+), 13 deletions(-) diff --git a/gateway/src/main.cpp b/gateway/src/main.cpp index 652fa39..e5e5530 100644 --- a/gateway/src/main.cpp +++ b/gateway/src/main.cpp @@ -26,7 +26,7 @@ using namespace qing; namespace json = boost::json; namespace po = boost::program_options; -constexpr const char *_VER = "0.4.3"; +constexpr const char *_VER = "0.4.4"; ////////////////////////////////// #include @@ -368,6 +368,27 @@ bool verify_captcha(const std::string& token, const std::string& answer) { return match; } +/* 调用 captcha 服务(pymsg/TCP :8009)校验图形验证码。 + * Token 一次性消费;服务不可用或响应异常一律视为校验失败(fail-closed)。 + * 与内置 verify_captcha 并存:登录/注册可任选其一接入。 */ +bool verify_captcha_remote(const std::string& token, const std::string& answer) { + try { + pmc::net::MsgClient client("127.0.0.1", 8009, "gateway"); + json::value jv_req = { + {"_route", "verify"}, + {"token", token}, + {"answer", answer} + }; + std::string resp = client.call(json::serialize(jv_req), 5.0); + auto jv = json::parse(resp); + auto& obj = jv.as_object(); + if (!obj.at("success").as_bool()) return false; + return obj.at("data").as_object().at("valid").as_bool(); + } catch (const std::exception&) { + return false; + } +} + /* -------- 信号处理回调函数 -------- */ void signalHandler(int signum) { @@ -689,16 +710,14 @@ http::response handleLogin( auto username = std::string(obj.at("username").as_string()); auto password = std::string(obj.at("password").as_string()); - // CAPTCHA verification 验证码 - // 浏览器请求验证码端口,获取验证图片和身份证(速率限制),身份证对应验证码 - // gateway请求验证端口,输入用户的验证和身份证 - //auto captcha_it = obj.find("captcha_token"); - //auto captcha_ans_it = obj.find("captcha_answer"); - //if (captcha_it == obj.end() || captcha_ans_it == obj.end()) - // THROW_EX_CUSTOM(HttpException, 2002, "Bad_Request", 400, "/login"); - //std::string ct = std::string(captcha_it->value().as_string()); - //std::string ca = std::string(captcha_ans_it->value().as_string()); - //if (!verify_captcha(ct, ca)) THROW_EX_CUSTOM(HttpException, 2201, "Invalid CAPTCHA", 400, "/login"); + // CAPTCHA verification 验证码(对接 captcha pymsg 服务,图形码 Token 一次性消费) + auto captcha_it = obj.find("captcha_token"); + auto captcha_ans_it = obj.find("captcha_answer"); + if (captcha_it == obj.end() || captcha_ans_it == obj.end()) + THROW_EX_CUSTOM(HttpException, 2002, "Bad_Request", 400, "/login"); + std::string ct = std::string(captcha_it->value().as_string()); + std::string ca = std::string(captcha_ans_it->value().as_string()); + if (!verify_captcha_remote(ct, ca)) THROW_EX_CUSTOM(HttpException, 2201, "Invalid CAPTCHA", 400, "/login"); std::string stored_hash = ""; { diff --git a/static/public/js/login.js b/static/public/js/login.js index 0dc621c..8690405 100644 --- a/static/public/js/login.js +++ b/static/public/js/login.js @@ -1,23 +1,49 @@ const msgTip = document.getElementById('msgTip'); const pwdTip = document.getElementById('pwdTip'); const agreeTip = document.getElementById('agreeTip'); +const captchaTip = document.getElementById('captchaTip'); const form = document.getElementById('form'); const usernameInput = document.getElementById('loginUsername'); const passwordInput = document.getElementById('loginPassword'); +const captchaInput = document.getElementById('captchaAnswer'); +const captchaImg = document.getElementById('captchaImg'); const agreeBox = document.getElementById('agreeBox'); const resetUser = document.getElementById('resetUser'); const registerBtn = document.getElementById('registerBtn'); const loginButton = document.getElementById('loginBtn'); +/* ── 图形验证码(gateway 代理 captcha 服务:GET /captcha/image) ── */ +let captchaToken = ''; + +async function refreshCaptcha() { + captchaToken = ''; + try { + const res = await fetch('/captcha/image', { cache: 'no-store' }); + 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 = '加载失败,点击重试'; + } +} +captchaImg.addEventListener('click', refreshCaptcha); +refreshCaptcha(); + resetUser.addEventListener('click', function() { clearTipStyle(); }); function clearTipStyle() { msgTip.innerHTML = ''; pwdTip.innerHTML = ''; agreeTip.innerHTML = ''; + captchaTip.innerHTML = ''; msgTip.classList.remove('tip-red', 'tip-yellow', 'tip-green'); pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green'); agreeTip.classList.remove('tip-red'); + captchaTip.classList.remove('tip-red'); } clearTipStyle(); @@ -35,6 +61,23 @@ function checkPwd() { return result.valid ? result.value : false; } +function checkCaptcha() { + captchaTip.innerHTML = ''; + captchaTip.classList.remove('tip-red'); + const v = captchaInput.value.trim(); + if (!v) { + captchaTip.innerHTML = '请输入验证码!'; + captchaTip.classList.add('tip-red'); + return false; + } + if (!captchaToken) { + captchaTip.innerHTML = '验证码未加载,请点击图片刷新'; + captchaTip.classList.add('tip-red'); + return false; + } + return v; +} + function checkAgree() { agreeTip.innerHTML = ''; agreeTip.classList.remove('tip-red'); @@ -60,10 +103,16 @@ form.addEventListener('submit', async function(e) { clearTipStyle(); const userOk = checkUsername(); const pwdOk = checkPwd(); + const captchaOk = checkCaptcha(); const agreeOk = checkAgree(); - if (!userOk || !pwdOk || !agreeOk) return; + if (!userOk || !pwdOk || !captchaOk || !agreeOk) return; - const jsonBody = JSON.stringify({ username: userOk, password: pwdOk }); + const jsonBody = JSON.stringify({ + username: userOk, + password: pwdOk, + captcha_token: captchaToken, + captcha_answer: captchaOk + }); try { loginButton.disabled = true; const res = await fetch('/login', { @@ -85,6 +134,9 @@ form.addEventListener('submit', async function(e) { } catch (err) { alert(err.message || '接口请求失败'); } finally { + // 无论成败都刷新验证码:Token 已被一次性消费,或需防重试 + captchaInput.value = ''; + refreshCaptcha(); loginButton.disabled = false; } }); diff --git a/static/public/login.html b/static/public/login.html index c72a420..e0438a4 100644 --- a/static/public/login.html +++ b/static/public/login.html @@ -17,6 +17,10 @@ + + + 加载中… +