feat: 登录页接入图形验证码,登录接口强制校验
登录页展示 captcha 图形码(点击刷新),登录请求携带 captcha_token/captcha_answer;gateway 的 /login 启用验证码校验,经 pymsg 调 captcha verify 路由(fail-closed),补齐防爆破短板。
This commit is contained in:
1 parent
323dd3061b
commit
6fcb70d235
3 files changed
+88
-13
No files matched your search
+30
-11
@@ -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 <sstream>
|
||||
@@ -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<http::string_body> 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 = "";
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -17,6 +17,10 @@
|
||||
<label>密码:</label>
|
||||
<input type="password" id="loginPassword" placeholder="请输入密码">
|
||||
<span id="pwdTip"></span>
|
||||
<label>验证码:</label>
|
||||
<input type="text" id="captchaAnswer" placeholder="请输入验证码" maxlength="5" autocomplete="off" style="width:110px">
|
||||
<img id="captchaImg" alt="加载中…" title="点击刷新验证码" style="width:120px;height:38px;vertical-align:middle;cursor:pointer;margin-left:8px;border:1px solid #ddd;border-radius:4px;box-sizing:border-box">
|
||||
<span id="captchaTip"></span>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" id="registerBtn">注册</button>
|
||||
|
||||
Reference in new issue
Block a user