feat: Phase 1 前端认证接入 FBA JWT——三轨会话 + 图形验证码(Epic #159)

This commit is contained in:
2026-08-06 09:46:21 +08:00
parent 70c680a612
commit 5b673fcdc7
2 changed files with 337 additions and 58 deletions
+75 -4
View File
@@ -138,6 +138,24 @@
}
.field .pwd-toggle:hover { color: var(--ant-primary); }
/* ---- FBA 图形验证码(Epic #159 Phase 1) -------------------------------- */
.captcha-row {
display: flex;
gap: 8px;
align-items: stretch;
}
.captcha-row .input-wrap { flex: 1; }
.captcha-img {
height: 40px;
min-width: 110px;
border: 1px solid var(--ant-border);
border-radius: var(--ant-radius);
cursor: pointer;
object-fit: cover;
background: #fafafa;
}
.captcha-img:hover { border-color: var(--ant-primary-hover); }
.btn-login {
width: 100%;
height: 40px;
@@ -210,11 +228,13 @@
</head>
<body>
<!--
iAOP 登录页(issue #150/#134 + 2026-08 Ant Design Pro 风格重构)。
认证双轨(shared/session.js):
· 后端 core/auth 可达 → POST /auth/login(HMAC 会话,HttpOnly cookie);
iAOP 登录页(issue #150/#134 + 2026-08 Ant Design Pro 风格重构 + Epic #159 Phase 1)。
认证三轨(shared/session.js):
· FBA 可用(/fba/api/v1 探测通过)→ FBA JWT 登录(默认强制图形验证码);
· 旧后端 core/auth 可达 → POST /auth/login(HMAC 会话,HttpOnly cookie);
· 纯静态部署 → UserStore 本地账号(首次访问自动播种演示账号)。
角色三级(PRD 8.2):readonly 只读 / engineer 可配置 / admin 可发布回滚,
FBA 侧角色按 session.js 的 FBA_ROLE_MAP 命名即可映射到三级角色,
登录后门户按角色过滤可见模块。
-->
<div class="brand">
@@ -250,6 +270,20 @@
</div>
</div>
<!-- FBA 图形验证码:仅当 FBA 认证可用且开启验证码时显示(session.js fbaStatus) -->
<div class="field" id="captchaField" hidden>
<label for="captcha">图形验证码</label>
<div class="captcha-row">
<div class="input-wrap">
<span class="input-icon">🛡</span>
<input id="captcha" name="captcha" type="text"
autocomplete="off" placeholder="请输入验证码" maxlength="8">
</div>
<img class="captcha-img" id="captchaImg" alt="点击刷新验证码"
title="看不清?点击刷新">
</div>
</div>
<button type="submit" class="btn-login" id="loginBtn">登 录</button>
<!-- issue #152:OIDC SSO 双轨入口——配置了 issuer/client_id 时显示 -->
@@ -299,6 +333,30 @@
// 首次访问播种本地演示账号(幂等;后端模式不影响)
UserStore.seedDefaults().catch(function () {});
/* Epic #159 Phase 1:FBA 可用且开启图形验证码时显示验证码字段 */
var captchaState = { enabled: false, uuid: null };
function refreshCaptcha() {
IAOP_SESSION.getCaptcha().then(function (c) {
if (c) {
captchaState.uuid = c.uuid;
$("captchaImg").src = c.imageSrc;
$("captcha").value = "";
}
});
}
IAOP_SESSION.fbaStatus().then(function (st) {
if (st.available && st.captchaEnabled) {
captchaState.enabled = true;
$("captchaField").hidden = false;
refreshCaptcha();
}
if (st.available) {
// FBA 模式下隐藏本地演示账号提示,避免误导
document.querySelector(".demo-accounts").hidden = true;
}
});
$("captchaImg").addEventListener("click", refreshCaptcha);
// 已登录直接跳转
IAOP_SESSION.currentUser().then(function (u) {
if (u) location.href = nextUrl();
@@ -310,6 +368,7 @@
var p = $("password").value;
$("username").removeAttribute("aria-invalid");
$("password").removeAttribute("aria-invalid");
$("captcha").removeAttribute("aria-invalid");
if (!u) {
$("username").setAttribute("aria-invalid", "true");
showAlert("请输入用户名", "error");
@@ -320,15 +379,27 @@
showAlert("密码不少于 8 位", "error");
return;
}
var captchaOpt = null;
if (captchaState.enabled) {
var c = $("captcha").value.trim();
if (!c) {
$("captcha").setAttribute("aria-invalid", "true");
showAlert("请输入图形验证码", "error");
return;
}
captchaOpt = { uuid: captchaState.uuid, captcha: c };
}
$("loginBtn").disabled = true;
clearAlert();
IAOP_SESSION.login(u, p).then(function (user) {
IAOP_SESSION.login(u, p, captchaOpt).then(function (user) {
showAlert("登录成功,欢迎 " + user.username + "(" +
(IAOP_SESSION.ROLE_LABELS[user.role] || user.role) + ")", "ok");
setTimeout(function () { location.href = nextUrl(); }, 400);
}).catch(function (err) {
$("password").setAttribute("aria-invalid", "true");
showAlert(err.message || "登录失败", "error");
// FBA 验证码一次性有效,失败后必须刷新
if (captchaState.enabled) refreshCaptcha();
}).finally(function () {
$("loginBtn").disabled = false;
});