feat: add auth strategy visualization

This commit is contained in:
Codex
2026-03-24 12:22:06 +08:00
parent bde4f6b9cf
commit 368a5061b0
2 changed files with 159 additions and 1 deletions

View File

@@ -178,6 +178,52 @@
background: rgba(255,255,255,0.45); background: rgba(255,255,255,0.45);
} }
.list-item p { margin-top: 8px; } .list-item p { margin-top: 8px; }
.auth-board {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-top: 18px;
}
.auth-card {
border: 1px solid var(--line);
border-radius: 20px;
padding: 18px;
background: rgba(255, 255, 255, 0.6);
}
.auth-highlight {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
}
.auth-focus {
font-size: 14px;
color: var(--muted);
}
.timeline {
margin-top: 10px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.timeline-step {
border-radius: 16px;
border: 1px dashed var(--line);
padding: 10px;
font-size: 12px;
background: rgba(255, 255, 255, 0.65);
}
.filter-pill {
display: inline-flex;
padding: 4px 10px;
border-radius: 14px;
border: 1px solid rgba(15, 103, 181, 0.2);
background: rgba(15, 103, 181, 0.08);
color: var(--accent);
font-size: 12px;
margin-right: 6px;
margin-top: 6px;
}
@media (max-width: 1180px) { @media (max-width: 1180px) {
.hero-grid, .workspace, .triple, .flow, .stats { grid-template-columns: 1fr 1fr; } .hero-grid, .workspace, .triple, .flow, .stats { grid-template-columns: 1fr 1fr; }
} }
@@ -232,6 +278,32 @@
</div> </div>
</section> </section>
<section class="card" id="authStrategy">
<div class="eyebrow" data-i18n="authBadge"></div>
<h2 data-i18n="authTitle"></h2>
<p data-i18n="authText"></p>
<div class="auth-board">
<div class="auth-card">
<strong data-i18n="jwtLabel"></strong>
<p data-i18n="jwtSummary"></p>
<span class="auth-focus" data-i18n="jwtLearnMore"></span>
</div>
<div class="auth-card">
<strong data-i18n="satokenLabel"></strong>
<p data-i18n="satokenSummary"></p>
<span class="auth-focus" data-i18n="satokenLearnMore"></span>
</div>
</div>
<div class="auth-highlight">
<span data-i18n="filterLabel"></span>
<div id="filterList"></div>
</div>
<div class="auth-highlight">
<span data-i18n="tokenLabel"></span>
</div>
<div class="timeline" id="tokenTimeline"></div>
</section>
<section class="card" style="margin-bottom:18px;"> <section class="card" style="margin-bottom:18px;">
<div class="eyebrow" data-i18n="archBadge"></div> <div class="eyebrow" data-i18n="archBadge"></div>
<h2 data-i18n="archTitle"></h2> <h2 data-i18n="archTitle"></h2>
@@ -464,6 +536,17 @@
consolePlaceholder: "请选择上方一个实验动作来加载实时输出。", consolePlaceholder: "请选择上方一个实验动作来加载实时输出。",
loadingPrefix: "正在加载 ", loadingPrefix: "正在加载 ",
requestFailedPrefix: "请求失败:", requestFailedPrefix: "请求失败:",
authBadge: "鉴权策略学习站",
authTitle: "从 JWT 到 Sa-Token 的学习空间",
authText: "本区详细对比 JWT 和 Sa-Token展示各自的拦截器、过滤器和请求链路帮助你理解两套鉴权策略的生命周期。",
jwtLabel: "JWT 登录",
jwtSummary: "客户端调用 /access 获取 token后续带 Authorization: Bearer header适合 stateless API 学习。",
jwtLearnMore: "查看 /api/users/stats 的 header 输出、触发 LearningJwtFilter",
satokenLabel: "Sa-Token 流程",
satokenSummary: "模拟 Sa-Token 里的 session 状态,观察拦截器、注解、认证缓存的运行差异。",
satokenLearnMore: "关注 Sa-Token 注解与拦截器的控制流",
filterLabel: "正在观察的过滤器:",
tokenLabel: "token 生命周期示意:",
exp1Badge: "实验 1", exp1Badge: "实验 1",
exp1Title: "追踪校验链路", exp1Title: "追踪校验链路",
exp1Text: "打开用户实验先创建一个用户再用同样邮箱重复创建。对比前端错误提示、DuplicateEmailException 和全局异常处理器。", exp1Text: "打开用户实验先创建一个用户再用同样邮箱重复创建。对比前端错误提示、DuplicateEmailException 和全局异常处理器。",
@@ -552,6 +635,17 @@
consolePlaceholder: "Select one experiment above to load live output.", consolePlaceholder: "Select one experiment above to load live output.",
loadingPrefix: "Loading ", loadingPrefix: "Loading ",
requestFailedPrefix: "Request failed: ", requestFailedPrefix: "Request failed: ",
authBadge: "Auth Strategy Studio",
authTitle: "Explore JWT vs Sa-Token",
authText: "This block contrasts the JWT and Sa-Token flows, visualizing filters, interceptors, and the request chain so you understand lifecycle differences.",
jwtLabel: "JWT Login",
jwtSummary: "Call /access, receive a bearer token, and include Authorization headers on protected calls to see stateless API behavior.",
jwtLearnMore: "Trace headers via /api/users/stats and LearningJwtFilter logs",
satokenLabel: "Sa-Token Flow",
satokenSummary: "Simulate Sa-Token's session-aware interceptor to highlight how stateful auth handles refresh and annotations.",
satokenLearnMore: "Observe how Sa-Token annotations and interceptors gate requests",
filterLabel: "Filters in focus:",
tokenLabel: "Token lifecycle:"
exp1Badge: "Experiment 1", exp1Badge: "Experiment 1",
exp1Title: "Trace validation", exp1Title: "Trace validation",
exp1Text: "Open the user lab, create a user, then repeat with the same email. Compare the frontend error with DuplicateEmailException and the global exception handler.", exp1Text: "Open the user lab, create a user, then repeat with the same email. Compare the frontend error with DuplicateEmailException and the global exception handler.",
@@ -640,6 +734,24 @@
window.learningShell.mountShell({ onLanguageChange: renderLanguage }); window.learningShell.mountShell({ onLanguageChange: renderLanguage });
renderLanguage(); renderLanguage();
function renderAuthBoard() {
const insights = window.learningShell.getAuthInsights();
document.querySelector("#authStrategy strong[data-i18n='jwtLabel']").textContent = insights.jwt.label;
document.querySelector("#authStrategy strong[data-i18n='satokenLabel']").textContent = insights.satoken.label;
document.querySelector("#authStrategy p[data-i18n='jwtSummary']").textContent = insights.jwt.summary;
document.querySelector("#authStrategy p[data-i18n='satokenSummary']").textContent = insights.satoken.summary;
document.querySelector("#authStrategy span[data-i18n='jwtLearnMore']").textContent = insights.jwt.learnMore;
document.querySelector("#authStrategy span[data-i18n='satokenLearnMore']").textContent = insights.satoken.learnMore;
document.querySelector("#filterList").innerHTML = insights.filters.map(f => `<span class="filter-pill">${f}</span>`).join("");
const timeline = window.learningShell.getTokenTimeline();
const timelineEl = document.getElementById("tokenTimeline");
timelineEl.innerHTML = timeline.map(item => `<div class="timeline-step"><strong>${item.stage}</strong><small>${item.detail}</small></div>`).join("");
}
renderAuthBoard();
window.addEventListener("learning-language-changed", renderAuthBoard);
</script> </script>
</body> </body>
</html> </html>

View File

@@ -304,6 +304,46 @@
return payload; return payload;
} }
const AUTH_INSIGHTS = {
zh: {
title: "鉴权策略学习站",
jwt: {
label: "JWT 登录",
summary: "前端调用 /access 拿到 token之后用 Authorization 头,讲解 stateless API 的常见模式。",
learnMore: "查看 /api/users/stats 的 header观察 LearningJwtFilter 拦截"
},
satoken: {
label: "Sa-Token 链路",
summary: "模拟 Sa-Token 把 session + token 坚持在一起便于对比状态ful 策略在并发控制、刷新和注解保护上的差异。",
learnMore: "观察 Sa-Token 注解是否在 {@link} 模块里生效"
},
filters: ["LearningJwtFilter", "CorsFilter", "AuthorizationFilter"],
tokenTips: "token 生命周期1. /access 登录2. Authorization 验证3. 401 触发刷新或重新登录。"
},
en: {
title: "Auth Strategy Studio",
jwt: {
label: "JWT Login",
summary: "Clients call /access and receive a token, then send Authorization headers for stateless APIs, illustrating modern REST flows.",
learnMore: "Trace the header in /api/users/stats and follow LearningJwtFilter"
},
satoken: {
label: "Sa-Token Flow",
summary: "Simulates combining session + token to highlight stateful auth, interceptor guards, and refresh semantics.",
learnMore: "Spot Sa-Token annotations and how the interceptor compares to filters"
},
filters: ["LearningJwtFilter", "CorsFilter", "AuthorizationFilter"],
tokenTips: "Token lifecycle: acquire at /access, transport in headers, backend validation, expired -> refresh/login."
}
};
const TOKEN_TIMELINE = [
{ stage: "Acquire", detail: "POST /access", badge: "jwt" },
{ stage: "Transport", detail: "Authorization: Bearer / Cookie", badge: "header" },
{ stage: "Validate", detail: "LearningJwtFilter / SaTokenInterceptor", badge: "verify" },
{ stage: "Expire/Refresh", detail: "401 -> re-login", badge: "expire" }
];
function describeError(error) { function describeError(error) {
if (error && Number(error.status) === 401) { if (error && Number(error.status) === 401) {
return t("unauthorized"); return t("unauthorized");
@@ -324,6 +364,12 @@
clearAuth: clearAuth, clearAuth: clearAuth,
fetchWithAuth: fetchWithAuth, fetchWithAuth: fetchWithAuth,
requestJson: requestJson, requestJson: requestJson,
describeError: describeError describeError: describeError,
getAuthInsights: function () {
return AUTH_INSIGHTS[getLanguage()] || AUTH_INSIGHTS.zh;
},
getTokenTimeline: function () {
return TOKEN_TIMELINE;
}
}; };
})(); })();