diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index eda85bf..b467ea1 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -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 @@ +
+
+

+

+
+
+ +

+ +
+
+ +

+ +
+
+
+ +
+
+
+ +
+
+
+

@@ -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 => `${f}`).join(""); + const timeline = window.learningShell.getTokenTimeline(); + const timelineEl = document.getElementById("tokenTimeline"); + timelineEl.innerHTML = timeline.map(item => `
${item.stage}${item.detail}
`).join(""); + } + + renderAuthBoard(); + + window.addEventListener("learning-language-changed", renderAuthBoard); diff --git a/src/main/resources/static/learning-shell.js b/src/main/resources/static/learning-shell.js index de74ec7..62fe152 100644 --- a/src/main/resources/static/learning-shell.js +++ b/src/main/resources/static/learning-shell.js @@ -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; + } }; })();