feat: add auth strategy visualization
This commit is contained in:
@@ -178,6 +178,52 @@
|
||||
background: rgba(255,255,255,0.45);
|
||||
}
|
||||
.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) {
|
||||
.hero-grid, .workspace, .triple, .flow, .stats { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
@@ -232,6 +278,32 @@
|
||||
</div>
|
||||
</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;">
|
||||
<div class="eyebrow" data-i18n="archBadge"></div>
|
||||
<h2 data-i18n="archTitle"></h2>
|
||||
@@ -464,6 +536,17 @@
|
||||
consolePlaceholder: "请选择上方一个实验动作来加载实时输出。",
|
||||
loadingPrefix: "正在加载 ",
|
||||
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",
|
||||
exp1Title: "追踪校验链路",
|
||||
exp1Text: "打开用户实验,先创建一个用户,再用同样邮箱重复创建。对比前端错误提示、DuplicateEmailException 和全局异常处理器。",
|
||||
@@ -552,6 +635,17 @@
|
||||
consolePlaceholder: "Select one experiment above to load live output.",
|
||||
loadingPrefix: "Loading ",
|
||||
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",
|
||||
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.",
|
||||
@@ -640,6 +734,24 @@
|
||||
|
||||
window.learningShell.mountShell({ onLanguageChange: 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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -304,6 +304,46 @@
|
||||
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) {
|
||||
if (error && Number(error.status) === 401) {
|
||||
return t("unauthorized");
|
||||
@@ -324,6 +364,12 @@
|
||||
clearAuth: clearAuth,
|
||||
fetchWithAuth: fetchWithAuth,
|
||||
requestJson: requestJson,
|
||||
describeError: describeError
|
||||
describeError: describeError,
|
||||
getAuthInsights: function () {
|
||||
return AUTH_INSIGHTS[getLanguage()] || AUTH_INSIGHTS.zh;
|
||||
},
|
||||
getTokenTimeline: function () {
|
||||
return TOKEN_TIMELINE;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user