feat: enforce login and refresh portal

This commit is contained in:
Codex
2026-03-24 16:00:44 +08:00
parent 2d826474bb
commit dfbe28a047
2 changed files with 199 additions and 712 deletions

View File

@@ -23,6 +23,7 @@
</global-results> </global-results>
<action name="index"> <action name="index">
<interceptor-ref name="secureStack"/>
<result>/WEB-INF/views/index.jsp</result> <result>/WEB-INF/views/index.jsp</result>
</action> </action>
@@ -31,6 +32,7 @@
</action> </action>
<action name="hello" class="com.demo.action.HelloAction" method="execute"> <action name="hello" class="com.demo.action.HelloAction" method="execute">
<interceptor-ref name="secureStack"/>
<result>/hello.jsp</result> <result>/hello.jsp</result>
</action> </action>
@@ -76,6 +78,7 @@
</action> </action>
<action name="ajax" class="com.demo.action.AjaxAction" method="execute"> <action name="ajax" class="com.demo.action.AjaxAction" method="execute">
<interceptor-ref name="secureStack"/>
<result type="json"/> <result type="json"/>
</action> </action>
@@ -87,7 +90,15 @@
</package> </package>
<package name="rest" namespace="/api" extends="json-default"> <package name="rest" namespace="/api" extends="json-default">
<interceptors>
<interceptor name="auth" class="com.demo.action.interceptor.AuthInterceptor"/>
<interceptor-stack name="secureStack">
<interceptor-ref name="auth"/>
<interceptor-ref name="defaultStack"/>
</interceptor-stack>
</interceptors>
<action name="users" class="com.demo.action.rest.UserRestAction" method="execute"> <action name="users" class="com.demo.action.rest.UserRestAction" method="execute">
<interceptor-ref name="secureStack"/>
<result type="json"/> <result type="json"/>
</action> </action>
</package> </package>

View File

@@ -5,264 +5,118 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Struts2 学习实验台</title> <title>Struts2 学习门户</title>
<style> <style>
:root { :root {
--bg: #f4f6fb; --bg: #eff2f5;
--panel: rgba(255,255,255,0.94); --panel: rgba(255,255,255,0.97);
--line: #d7e0ea; --line: #d4d9e6;
--text: #122033; --text: #1f2b3d;
--muted: #5c6f84; --muted: #52607a;
--brand: #1464c7; --brand: #1464c7;
--brand-2: #f68b1f; --accent: #0d9488;
--soft: #e8f2ff;
--shadow: 0 20px 44px rgba(18, 32, 51, 0.14);
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
body { body {
margin: 0; margin: 0;
min-height: 100vh; min-height: 100vh;
font-family: "Aptos", "Segoe UI", "Microsoft YaHei", sans-serif; font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
background: linear-gradient(135deg, #e3ebff 0%, #f1f5f9 55%, #effaf5 100%);
color: var(--text); color: var(--text);
background:
radial-gradient(circle at top right, rgba(20, 100, 199, 0.15), transparent 28%),
radial-gradient(circle at bottom left, rgba(246, 139, 31, 0.12), transparent 24%),
var(--bg);
} }
a { color: inherit; text-decoration: none; } .shell {
.shell { max-width: 1480px; margin: 0 auto; padding: 24px; } width: min(1200px, 100%);
.hero, .card { margin: 0 auto;
padding: 24px;
}
.card {
background: var(--panel); background: var(--panel);
border: 1px solid var(--line); border-radius: 26px;
border-radius: 28px;
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}
.hero {
padding: 28px; padding: 28px;
box-shadow: 0 25px 50px rgba(15, 23, 42, 0.1);
border: 1px solid var(--line);
margin-bottom: 18px; margin-bottom: 18px;
} }
.eyebrow { .eyebrow {
font-size: 12px; font-size: 12px;
letter-spacing: 0.12em; letter-spacing: 0.12em;
text-transform: uppercase; text-transform: uppercase;
font-weight: 800; color: var(--accent);
color: var(--brand); margin-bottom: 6px;
font-weight: 700;
} }
.hero-head { h1, h2, h3 {
display: flex; margin: 8px 0 12px;
justify-content: space-between; font-weight: 700;
gap: 20px;
align-items: flex-start;
} }
.hero h1 { p {
margin: 10px 0 12px;
font-size: 42px;
line-height: 1.1;
}
.hero p {
margin: 0; margin: 0;
color: var(--muted); color: var(--muted);
line-height: 1.9; line-height: 1.7;
max-width: 900px; }
.hero-card {
display: flex;
flex-direction: column;
gap: 12px;
} }
.hero-actions { .hero-actions {
display: flex; display: flex;
gap: 10px; gap: 12px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.btn, .btn-soft, .btn-ghost { .btn, .btn-soft, .btn-ghost {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
padding: 12px 18px; padding: 12px 18px;
border-radius: 999px;
font-weight: 700; font-weight: 700;
border: 0; border: none;
cursor: pointer; cursor: pointer;
} }
.btn { background: linear-gradient(135deg, var(--brand), #3a8dff); color: white; } .btn {
.btn-soft { background: var(--soft); color: var(--brand); } background: linear-gradient(135deg, var(--brand), #3d6fe7);
.btn-ghost { background: transparent; color: var(--text); border: 1px solid var(--line); } color: white;
.metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 20px;
} }
.metric { .btn-soft {
padding: 16px; background: #f4f7ff;
border-radius: 18px; color: var(--brand);
border: 1px solid var(--line); border: 1px solid var(--line);
background: rgba(255,255,255,0.56);
} }
.metric span { display: block; color: var(--muted); font-size: 12px; margin-bottom: 8px; } .btn-ghost {
.metric strong { font-size: 26px; }
.layout {
display: grid;
grid-template-columns: 360px minmax(0, 1fr);
gap: 18px;
align-items: start;
}
.sidebar, .content { display: flex; flex-direction: column; gap: 18px; }
.card { padding: 22px; }
.search {
display: flex;
gap: 10px;
margin-top: 14px;
}
.search input {
flex: 1;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 16px;
padding: 13px 14px;
font: inherit;
background: transparent; background: transparent;
color: var(--text); color: var(--text);
outline: none;
} }
.helper-list { .lang-warning {
margin: 14px 0 0; font-size: 14px;
padding-left: 18px;
color: var(--muted);
line-height: 1.9;
}
.status-card {
padding: 16px;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255,255,255,0.56);
color: var(--muted);
line-height: 1.85;
}
.status-card strong {
display: block;
margin-bottom: 8px;
color: var(--text);
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.demo-card {
border: 1px solid var(--line);
border-radius: 22px;
padding: 18px;
background: rgba(255,255,255,0.45);
transition: transform 0.18s ease, border-color 0.18s ease;
}
.demo-card:hover {
transform: translateY(-3px);
border-color: rgba(20, 100, 199, 0.28);
}
.demo-card h3 {
margin: 10px 0 8px;
font-size: 20px;
}
.demo-card p {
margin: 0 0 14px;
color: var(--muted);
line-height: 1.8;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.tag {
display: inline-flex;
padding: 6px 10px;
border-radius: 999px;
background: var(--soft);
color: var(--brand); color: var(--brand);
font-size: 12px;
font-weight: 700;
} }
.demo-links { .portal-grid {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.link-btn {
display: inline-flex;
padding: 9px 14px;
border-radius: 999px;
border: 1px solid var(--line);
color: var(--text);
background: white;
font-size: 13px;
font-weight: 700;
}
.pipeline {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 12px; gap: 16px;
margin-top: 14px; margin-top: 16px;
} }
.step { .portal-card {
padding: 16px; border: 1px solid var(--line);
border-radius: 18px; border-radius: 18px;
border: 1px solid var(--line); background: rgba(255,255,255,0.85);
background: rgba(255,255,255,0.52); padding: 16px;
}
.step strong { display: block; margin-bottom: 8px; }
.step p { margin: 0; color: var(--muted); line-height: 1.75; }
.empty {
padding: 14px;
border: 1px dashed var(--line);
border-radius: 16px;
color: var(--muted);
text-align: center;
}
.learning-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px;
margin-top: 18px;
}
.learning-panel {
border: 1px solid var(--line);
border-radius: 20px;
padding: 18px;
background: rgba(255,255,255,0.65);
box-shadow: inset 0 4px 12px rgba(18,32,51,0.06);
}
.learning-panel h3 {
margin: 0 0 6px;
font-size: 18px;
}
.learning-panel p {
margin: 0;
color: var(--muted);
line-height: 1.6;
}
.flow-steps {
margin-top: 12px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
.flow-step { .portal-links {
padding: 10px; display: flex;
border-radius: 14px; gap: 6px;
border: 1px solid rgba(20,100,199,0.25); flex-wrap: wrap;
background: rgba(13,143,124,0.06);
} }
.flow-step strong { .portal-links .link-btn {
display: block; padding: 8px 12px;
font-size: 14px; border-radius: 12px;
}
.chain-pill {
display: inline-flex;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid var(--line); border: 1px solid var(--line);
background: #ffffff; background: #fff;
font-size: 12px; color: var(--text);
margin-right: 6px; text-decoration: none;
margin-top: 6px; font-weight: 600;
} }
.insight-grid { .insight-grid {
display: grid; display: grid;
@@ -271,540 +125,162 @@
margin-top: 16px; margin-top: 16px;
} }
.insight-card { .insight-card {
border-radius: 20px;
padding: 18px;
border: 1px solid var(--line); border: 1px solid var(--line);
background: linear-gradient(135deg, rgba(255,255,255,0.9), rgba(255,255,255,0.7)); border-radius: 18px;
min-height: 200px; padding: 16px;
position: relative; background: rgba(255,255,255,0.95);
min-height: 150px;
} }
.insight-card strong { .chain-pill {
display: block; display: inline-flex;
margin-top: 10px; border-radius: 999px;
font-size: 22px; padding: 4px 10px;
color: var(--brand); border: 1px solid var(--line);
background: #f4f7ff;
margin-right: 6px;
font-size: 12px;
} }
.insight-card ul { .session-note {
margin: 10px 0 0 16px; font-size: 14px;
color: var(--muted); color: var(--muted);
line-height: 1.6;
}
@media (max-width: 1100px) {
.layout { grid-template-columns: 1fr; }
.grid, .pipeline, .metrics { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 720px) {
.shell { padding: 14px; }
.hero-head, .search { flex-direction: column; align-items: stretch; }
.grid, .pipeline, .metrics { grid-template-columns: 1fr; }
} }
</style> </style>
</head> </head>
<body> <body>
<div class="shell"> <div class="shell">
<section class="hero"> <section class="card hero-card">
<div class="hero-head"> <div class="eyebrow" id="heroEyebrow">Struts2 学习门户</div>
<div> <h1 id="heroTitle">登录之后才能继续学习</h1>
<div class="eyebrow" id="heroEyebrow">Struts2 学习实验台</div> <p id="heroDesc">所有课程模块(首页/实验/JSON 等)都由 AuthInterceptor 保护,未登录访问会自动跳转。请先登录再回来。</p>
<h1 id="heroTitle">把零散示例整理成一条可讲解、可演示、可验证的学习路线</h1> <div class="lang-warning" id="heroWarning">登录后才能看到完整课程导航与实验入口。</div>
<p id="heroText">这个入口页现在不再只是样例链接集合,而是把经典 Struts2 的动作映射、参数绑定、Session 登录、表单校验和文件上传串成一个可循序学习的 Demo 门户。</p>
</div>
<div class="hero-actions"> <div class="hero-actions">
<button class="btn-ghost" type="button" id="languageBtn">EN</button> <a class="btn" href="dashboard.action" id="heroActionPrimary">进入仪表盘</a>
<a class="btn" href="loginPage.action" id="primaryAction">进入登录章节</a> <a class="btn-soft" href="loginPage.action" id="heroActionSecondary">立即登录</a>
<a class="btn-soft" href="hello.action?name=Platform%20Team" id="secondaryAction">运行 Hello Action</a> <button class="btn-ghost" type="button" id="languageBtnHero">EN</button>
</div> </div>
</div> <div class="session-note" id="heroSessionNote">
<div class="metrics">
<div class="metric"><span id="metricRoutesLabel">关键动作</span><strong>8</strong></div>
<div class="metric"><span id="metricSecureLabel">鉴权节点</span><strong>4</strong></div>
<div class="metric"><span id="metricFormsLabel">表单实验</span><strong>3</strong></div>
<div class="metric"><span id="metricApiLabel">JSON 示例</span><strong>2</strong></div>
</div>
</section>
<div class="layout">
<aside class="sidebar">
<section class="card">
<div class="eyebrow" id="searchEyebrow">快速检索</div>
<h2 id="searchTitle">按关键词筛选实验</h2>
<div class="search">
<input id="searchInput" type="text" placeholder="试试 login、session、validation、upload、json" />
</div>
<ul class="helper-list" id="helperList">
<li>先看登录和仪表盘,理解最经典的 Session 鉴权链路。</li>
<li>再看表单、校验、上传,理解 Action 如何接收与处理输入。</li>
<li>最后再看 AJAX 和 REST 风格返回,理解 Struts2 的扩展能力。</li>
</ul>
</section>
<section class="card">
<div class="eyebrow" id="statusEyebrow">当前会话</div>
<h2 id="statusTitle">登录状态与访问建议</h2>
<div class="status-card">
<s:if test="#session.demoUser != null"> <s:if test="#session.demoUser != null">
<strong id="sessionStateTitle">当前已登录</strong> 当前:<s:property value="#session.demoUser"/><s:property value="#session.demoRole"/>
<div id="sessionStateBody"
data-authenticated="true"
data-user="<s:property value='#session.demoUser'/>"
data-role="<s:property value='#session.demoRole'/>"></div>
<div class="demo-links" style="margin-top: 14px;">
<a class="link-btn" href="dashboard.action" id="dashboardLink">打开仪表盘</a>
<a class="link-btn" href="logout.action" id="logoutLink">退出登录</a>
</div>
</s:if> </s:if>
<s:else> <s:else>
<strong id="sessionStateTitle">当前未登录</strong> 当前未登录,请先完成登录再浏览实验。
<div id="sessionStateBody" data-authenticated="false"></div>
<div class="demo-links" style="margin-top: 14px;">
<a class="link-btn" href="loginPage.action" id="loginLink">打开登录页</a>
</div>
</s:else> </s:else>
</div> </div>
</section> </section>
<section class="card"> <section class="card portal-section">
<div class="eyebrow" id="routeEyebrow">学习顺序</div> <div class="eyebrow" id="portalEyebrow">学习模块导航</div>
<h2 id="routeTitle">建议你按这个顺序看</h2> <h2 id="portalTitle">分步骤理解 Struts2</h2>
<div class="pipeline"> <p id="portalDesc">每个模块都通过 `.action` 路由触达,由统一的 Session 鉴权链路保护。</p>
<div class="step"> <div class="portal-grid">
<strong id="routeStep1Title">1. Hello Action</strong> <article class="portal-card">
<p id="routeStep1Text">先看最小动作映射,建立 request -> action -> result 的基本心智模型。</p> <h3 id="portalCard1Title">请求生命周期</h3>
</div> <p id="portalCard1Desc">Dispatcher → Action → Interceptor → Result严格按链路走向。</p>
<div class="step"> <div>
<strong id="routeStep2Title">2. Session 登录</strong> <span class="chain-pill">Dispatcher</span>
<p id="routeStep2Text">看登录动作如何写入 Session并由拦截器保护后续实验页。</p> <span class="chain-pill">Action</span>
</div> <span class="chain-pill">Interceptors</span>
<div class="step"> <span class="chain-pill">Result</span>
<strong id="routeStep3Title">3. 表单与校验</strong>
<p id="routeStep3Text">看字段绑定、校验错误和成功页汇总。</p>
</div>
<div class="step">
<strong id="routeStep4Title">4. 上传与 JSON</strong>
<p id="routeStep4Text">最后再看文件上传和 JSON 返回,理解经典 MVC 如何扩展。</p>
</div>
</div>
</section>
</aside>
<main class="content">
<section class="card">
<div class="eyebrow" id="catalogEyebrow">实验目录</div>
<h2 id="catalogTitle">核心实验入口</h2>
<div class="grid" id="demoGrid">
<article class="demo-card" data-keywords="hello action request parameter basics 基础 动作 映射">
<div class="tag-list">
<span class="tag" id="tagHello1">动作</span>
<span class="tag" id="tagHello2">基础</span>
</div>
<h3 id="cardHelloTitle">Hello Action</h3>
<p id="cardHelloText">运行最小 Struts2 动作,观察请求参数如何进入 Action再由结果页返回浏览器。</p>
<div class="demo-links">
<a class="link-btn" href="hello.action?name=Platform%20Team" id="cardHelloRun">运行示例</a>
</div> </div>
</article> </article>
<article class="portal-card">
<article class="demo-card" data-keywords="login session auth interceptor dashboard 登录 会话 鉴权 拦截器"> <h3 id="portalCard2Title">登录保护</h3>
<div class="tag-list"> <p id="portalCard2Desc">`LoginAction` 写入 Session`AuthInterceptor` 阻止任何未登录访问。</p>
<span class="tag" id="tagLogin1">登录</span> <div class="portal-links">
<span class="tag" id="tagLogin2">Session</span> <a class="link-btn" href="loginPage.action">登录页</a>
</div> <a class="link-btn" href="dashboard.action">仪表盘</a>
<h3 id="cardLoginTitle">登录与仪表盘</h3>
<p id="cardLoginText">这部分是本项目的核心改造点:用经典 Session 登录和拦截器串起后续实验页。</p>
<div class="demo-links">
<a class="link-btn" href="loginPage.action" id="cardLoginOpen">打开登录</a>
<a class="link-btn" href="dashboard.action" id="cardLoginDashboard">打开仪表盘</a>
</div> </div>
</article> </article>
<article class="portal-card">
<article class="demo-card" data-keywords="user form submit binding profile 用户 表单 绑定"> <h3 id="portalCard3Title">表单与校验</h3>
<div class="tag-list"> <p id="portalCard3Desc">表单字段直接绑定到 ActionValidationAction 负责完整校验流程。</p>
<span class="tag" id="tagUser1">表单</span> <div class="portal-links">
<span class="tag" id="tagUser2">绑定</span> <a class="link-btn" href="userFormPage.action">表单</a>
</div> <a class="link-btn" href="validationPage.action">校验</a>
<h3 id="cardUserTitle">用户资料提交</h3>
<p id="cardUserText">演示字段绑定、错误回显和成功汇总页,也是最适合讲参数注入的例子。</p>
<div class="demo-links">
<a class="link-btn" href="userFormPage.action" id="cardUserOpen">打开用户表单</a>
</div> </div>
</article> </article>
<article class="portal-card">
<article class="demo-card" data-keywords="validation field errors age email 校验 错误 表单"> <h3 id="portalCard4Title">AJAX / REST 对照</h3>
<div class="tag-list"> <p id="portalCard4Desc">对比 `ajax.action` 与 `api/users.action` 的 JSON 输出。</p>
<span class="tag" id="tagValidation1">校验</span> <div class="portal-links">
<span class="tag" id="tagValidation2">输入规则</span> <a class="link-btn" href="ajax.action">AJAX</a>
<a class="link-btn" href="api/users.action">REST</a>
</div> </div>
<h3 id="cardValidationTitle">字段校验实验</h3>
<p id="cardValidationText">对比校验失败和成功页面,理解为什么 Struts2 会在业务逻辑之前先跑 validate。</p>
<div class="demo-links">
<a class="link-btn" href="validationPage.action" id="cardValidationOpen">打开校验页</a>
</div>
</article>
<article class="demo-card" data-keywords="upload multipart metadata 文件 上传 multipart">
<div class="tag-list">
<span class="tag" id="tagUpload1">上传</span>
<span class="tag" id="tagUpload2">安全演示</span>
</div>
<h3 id="cardUploadTitle">文件上传元数据</h3>
<p id="cardUploadText">保留 multipart 绑定教学价值,但不真正落盘,适合本地和 VPS 环境安全演示。</p>
<div class="demo-links">
<a class="link-btn" href="uploadPage.action" id="cardUploadOpen">打开上传页</a>
</div>
</article>
<article class="demo-card" data-keywords="ajax json rest api 接口 json ajax">
<div class="tag-list">
<span class="tag" id="tagJson1">JSON</span>
<span class="tag" id="tagJson2">接口风格</span>
</div>
<h3 id="cardJsonTitle">AJAX 与 REST 风格返回</h3>
<p id="cardJsonText">保留 JSON 动作和 REST 风格示例,用来说明经典 MVC 项目如何逐步演进到接口输出。</p>
<div class="demo-links">
<a class="link-btn" href="ajax.action" id="cardJsonAjax">打开 AJAX JSON</a>
<a class="link-btn" href="api/users.action" id="cardJsonRest">打开 REST JSON</a>
</div>
</article>
</div>
<div class="empty" id="emptyState" style="display: none; margin-top: 16px;">当前关键词没有匹配到实验。</div>
</section>
<section class="card learning-panel-wrapper">
<div class="eyebrow" id="learningBoardEyebrow">可视化学习</div>
<h2 id="learningBoardTitle">Struts2 请求与鉴权流程图</h2>
<p id="learningBoardDesc">通过时序卡片展示请求到达、拦截、执行和结果之间的关系,并把登录保护链路具体化,增强讲解能力。</p>
<div class="learning-grid">
<article class="learning-panel">
<div class="eyebrow" id="timelineEyebrow">请求生命周期</div>
<h3 id="timelineTitle">请求进入 → Action → 结果</h3>
<p id="timelineDesc">Struts2 将 URL 映射到 Action执行之前先跑拦截器再决定返回的 JSP 或 JSON。</p>
<div class="flow-steps">
<div class="flow-step">
<strong id="timelineStep1">1. 请求进来</strong>
<p id="timelineStep1Text">Dispatcher 解析 namespace 与 action构造参数并执行</p>
</div>
<div class="flow-step">
<strong id="timelineStep2">2. 拦截器链</strong>
<p id="timelineStep2Text">`AuthInterceptor` 检测 Session其他拦截器做参数/校验/文件准备</p>
</div>
<div class="flow-step">
<strong id="timelineStep3">3. 结果输出</strong>
<p id="timelineStep3Text">Action 返回 SUCCESS/INPUTStruts 渲染 JSP 或 JSON浏览器读取响应</p>
</div>
</div>
</article>
<article class="learning-panel">
<div class="eyebrow" id="chainEyebrow">Action → Interceptor → Result</div>
<h3 id="chainTitle">链式控制:操作 → 鉴权 → 渲染</h3>
<p id="chainDesc">每个请求都必须穿过这个三段式,理解它能帮助你解释 Struts2 的核心执行模型。</p>
<div class="chain-pill" id="chainAction">Action</div>
<div class="chain-pill" id="chainInterceptor">Interceptor</div>
<div class="chain-pill" id="chainResult">Result</div>
<p id="chainExplain">Action 负责业务,拦截器负责验证/鉴权Result 负责视图渲染或 JSON 返回。</p>
</article> </article>
</div> </div>
</section> </section>
<section class="card"> <section class="card insight-section">
<div class="eyebrow" id="insightEyebrow">学习洞察</div> <div class="eyebrow">Insight</div>
<h2 id="insightTitle">关键链路 & 实验对照</h2> <h2>安全学习链路速览</h2>
<div class="insight-grid"> <div class="insight-grid">
<article class="insight-card"> <article class="insight-card">
<div class="eyebrow">登录保护链路</div> <h3>登录才能使用</h3>
<h3 id="loginCardTitle">Session 登录 + 拦截器</h3> <p>未登录访问 `dashboard.action` 等任何端点都会被 AuthInterceptor 重定向至 `loginPage.action`。</p>
<p id="loginCardDesc">运用 `LoginAction`、`AuthInterceptor` 和 `DashboardAction` 来完成端到端登录和受保护导航。</p>
<ul>
<li id="loginCardItem1">输入 admin / 123456 写入 Session</li>
<li id="loginCardItem2">`AuthInterceptor` 拦截未登录访问</li>
<li id="loginCardItem3">登录后跳转到专属仪表盘再进实验</li>
</ul>
</article> </article>
<article class="insight-card"> <article class="insight-card">
<div class="eyebrow">表单绑定 & 校验</div> <h3>表单绑定观察</h3>
<h3 id="bindingCardTitle">数据绑定观察卡片</h3> <p>`UserAction` 按字段绑定,`ValidationAction` 造成的字段错误会带回当前 JSP。</p>
<p id="bindingCardDesc">从 `userFormPage` 到 `submitUser`,展示字段绑定、校验触发和成功结果的行为差异。</p>
<ul>
<li id="bindingCardItem1">字段名Action 属性与表单 `name` 一一对应</li>
<li id="bindingCardItem2">验证逻辑:`UserAction#submit` 里点亮不同结果</li>
<li id="bindingCardItem3">成功后跳转 `user/success.jsp` 显示绑定概览</li>
</ul>
</article> </article>
<article class="insight-card"> <article class="insight-card">
<div class="eyebrow">JSON 对照模块</div> <h3>AJAX vs REST</h3>
<h3 id="jsonCardTitle">AJAX vs REST</h3> <p>`ajax.action` 返回对话式 JSON`api/users.action` 呈现 REST 样式,便于教学对照。</p>
<p id="jsonCardDesc">并列演示 `ajax.action`AJAX JSON和 `api/users.action`REST JSON的输出差异便于讲解 Struts 如何扩展接口。</p>
<ul>
<li id="jsonCardItem1">`ajax.action` 面向浏览器交互,返回 `success` + data</li>
<li id="jsonCardItem2">`api/users.action` 保持 REST 语义,便于讲解 JSON 策略</li>
</ul>
</article> </article>
</div> </div>
</section> </section>
</main>
</div>
</div> </div>
<script src="assets/struts-lab.js"></script> <script src="../assets/struts-lab.js"></script>
<script> <script>
const searchInput = document.getElementById("searchInput"); strutsLab.mount({
const cards = Array.from(document.querySelectorAll(".demo-card")); buttonId: "languageBtnHero",
const emptyState = document.getElementById("emptyState");
let ui = null;
function runSearch() {
const keyword = searchInput.value.trim().toLowerCase();
let visible = 0;
cards.forEach((card) => {
const match = !keyword || card.dataset.keywords.includes(keyword);
card.style.display = match ? "block" : "none";
if (match) {
visible += 1;
}
});
emptyState.style.display = visible ? "none" : "block";
if (ui) {
emptyState.textContent = ui.messages.emptyState;
}
}
searchInput.addEventListener("input", runSearch);
ui = strutsLab.mount({
messages: { messages: {
zh: { zh: {
pageTitle: "Struts2 学习实验台", pageTitle: "Struts2 学习门户",
text: { text: {
heroEyebrow: "Struts2 学习实验台", heroEyebrow: "Struts2 学习门户",
heroTitle: "把零散示例整理成一条可讲解、可演示、可验证的学习路线", heroTitle: "登录之后才能继续学习",
heroText: "这个入口页现在不再只是样例链接集合,而是把经典 Struts2 的动作映射、参数绑定、Session 登录、表单校验和文件上传串成一个可循序学习的 Demo 门户。", heroDesc: "所有入口都由 Session + AuthInterceptor 保护,未登录将自动跳回登录页。",
primaryAction: "进入登录章节", heroWarning: "未登录不能访问实验页;登录后可畅玩全部模块。",
secondaryAction: "运行 Hello Action", heroActionPrimary: "进入仪表盘",
metricRoutesLabel: "关键动作", heroActionSecondary: "立即登录",
metricSecureLabel: "鉴权节点", portalEyebrow: "学习模块导航",
metricFormsLabel: "表单实验", portalTitle: "分步骤理解 Struts2",
metricApiLabel: "JSON 示例", portalDesc: "每个 `.action` 路径都在登录后才能访问Session 鉴权串联整个体验。",
searchEyebrow: "快速检索", portalCard1Title: "请求生命周期",
searchTitle: "按关键词筛选实验", portalCard1Desc: "Dispatcher → Action → Interceptor → Result典型执行路径。",
statusEyebrow: "当前会话", portalCard2Title: "登录保护",
statusTitle: "登录状态与访问建议", portalCard2Desc: "`LoginAction` 写入 Session`AuthInterceptor` 拦截未登录。",
routeEyebrow: "学习顺序", portalCard3Title: "表单与校验",
routeTitle: "建议你按这个顺序看", portalCard3Desc: "Action 字段绑定 + ValidationAction 校验,成功后进入汇总。",
sessionStateTitle: "当前已登录", portalCard4Title: "AJAX / REST 对照",
dashboardLink: "打开仪表盘", portalCard4Desc: "对比 `ajax.action`AJAX与 `api/users.action`REST。"
logoutLink: "退出登录", }
loginLink: "打开登录页",
routeStep1Title: "1. Hello Action",
routeStep1Text: "先看最小动作映射,建立 request -> action -> result 的基本心智模型。",
routeStep2Title: "2. Session 登录",
routeStep2Text: "看登录动作如何写入 Session并由拦截器保护后续实验页。",
routeStep3Title: "3. 表单与校验",
routeStep3Text: "看字段绑定、校验错误和成功页汇总。",
routeStep4Title: "4. 上传与 JSON",
routeStep4Text: "最后再看文件上传和 JSON 返回,理解经典 MVC 如何扩展。",
catalogEyebrow: "实验目录",
catalogTitle: "核心实验入口",
tagHello1: "动作",
tagHello2: "基础",
cardHelloTitle: "Hello Action",
cardHelloText: "运行最小 Struts2 动作,观察请求参数如何进入 Action再由结果页返回浏览器。",
cardHelloRun: "运行示例",
tagLogin1: "登录",
tagLogin2: "Session",
cardLoginTitle: "登录与仪表盘",
cardLoginText: "这部分是本项目的核心改造点:用经典 Session 登录和拦截器串起后续实验页。",
cardLoginOpen: "打开登录",
cardLoginDashboard: "打开仪表盘",
tagUser1: "表单",
tagUser2: "绑定",
cardUserTitle: "用户资料提交",
cardUserText: "演示字段绑定、错误回显和成功汇总页,也是最适合讲参数注入的例子。",
cardUserOpen: "打开用户表单",
tagValidation1: "校验",
tagValidation2: "输入规则",
cardValidationTitle: "字段校验实验",
cardValidationText: "对比校验失败和成功页面,理解为什么 Struts2 会在业务逻辑之前先跑 validate。",
cardValidationOpen: "打开校验页",
tagUpload1: "上传",
tagUpload2: "安全演示",
cardUploadTitle: "文件上传元数据",
cardUploadText: "保留 multipart 绑定教学价值,但不真正落盘,适合本地和 VPS 环境安全演示。",
cardUploadOpen: "打开上传页",
tagJson1: "JSON",
tagJson2: "接口风格",
cardJsonTitle: "AJAX 与 REST 风格返回",
cardJsonText: "保留 JSON 动作和 REST 风格示例,用来说明经典 MVC 项目如何逐步演进到接口输出。",
cardJsonAjax: "打开 AJAX JSON",
cardJsonRest: "打开 REST JSON",
learningBoardEyebrow: "可视化学习",
learningBoardTitle: "Struts2 请求与鉴权流程图",
learningBoardDesc: "用时序卡片展示请求穿越 Action、拦截器和 Result 的全链路,让讲解更直观。",
timelineEyebrow: "请求生命周期",
timelineTitle: "请求进入 → Action → 结果",
timelineDesc: "Struts2 先解析 action再跑拦截器最后决定 JSP 或 JSON 的渲染。",
timelineStep1: "1. 请求进来",
timelineStep1Text: "Dispatcher 解析 namespace、action注入参数并准备执行。",
timelineStep2: "2. 拦截器链",
timelineStep2Text: "`AuthInterceptor` 检查 Session其它拦截器做校验/上传处理。",
timelineStep3: "3. 结果输出",
timelineStep3Text: "Action 返回 SUCCESS/INPUTStruts 渲染 JSP 或 JSON 回应。",
chainEyebrow: "Action → Interceptor → Result",
chainTitle: "链式控制:操作 → 鉴权 → 渲染",
chainDesc: "中间拦截器决定请求是否通过,结果决定渲染视图或接口响应。",
chainExplain: "Action 负责业务Interceptor 掌握鉴权细节Result 才真正交给浏览器。",
insightEyebrow: "学习洞察",
insightTitle: "关键链路与实验对照",
loginCardTitle: "Session 登录 + 拦截器",
loginCardDesc: "用 `LoginAction`、`AuthInterceptor` 和仪表盘串起端到端链路。",
loginCardItem1: "输入 admin / 123456 写入 Session",
loginCardItem2: "`AuthInterceptor` 拦截未登录访问",
loginCardItem3: "登录后跳转仪表盘再进入实验页",
bindingCardTitle: "表单绑定观察卡片",
bindingCardDesc: "从用户表单到 `submitUser`,看字段绑定与校验行为如何变化。",
bindingCardItem1: "字段名与 Action 属性一一对应",
bindingCardItem2: "`UserAction#submit` 根据校验返回不同结果",
bindingCardItem3: "成功后跳转 `user/success.jsp` 显示汇总",
jsonCardTitle: "AJAX vs REST JSON",
jsonCardDesc: "对照 `ajax.action` 与 `api/users.action` 的输出,讲解 Struts JSON 扩展。",
jsonCardItem1: "`ajax.action` 面向浏览器交互,返回 success + 数据",
jsonCardItem2: "`api/users.action` 保持 REST 格式,便于接口讲解"
},
html: {
helperList: "<li>先看登录和仪表盘,理解最经典的 Session 鉴权链路。</li><li>再看表单、校验、上传,理解 Action 如何接收与处理输入。</li><li>最后再看 AJAX 和 REST 风格返回,理解 Struts2 的扩展能力。</li>"
},
placeholders: {
searchInput: "试试 login、session、validation、upload、json"
},
emptyState: "当前关键词没有匹配到实验。"
}, },
en: { en: {
pageTitle: "Struts2 Learning Lab", pageTitle: "Struts2 Learning Portal",
text: { text: {
heroEyebrow: "Struts2 Learning Lab", heroEyebrow: "Struts2 Learning Portal",
heroTitle: "Turn scattered examples into one explainable and verifiable learning route", heroTitle: "Login before you explore",
heroText: "This entry page is now a structured learning portal that connects action mapping, parameter binding, session login, form validation, and file upload into one guided Struts2 demo.", heroDesc: "All modules are protected by Session + AuthInterceptor. Unauthenticated access goes back to login.",
primaryAction: "Open login chapter", heroWarning: "Access denied until login; dashboard and labs available after authentication.",
secondaryAction: "Run hello action", heroActionPrimary: "Go to dashboard",
metricRoutesLabel: "Key actions", heroActionSecondary: "Login now",
metricSecureLabel: "Protected nodes", portalEyebrow: "Module guide",
metricFormsLabel: "Form labs", portalTitle: "Step-by-step Struts2",
metricApiLabel: "JSON samples", portalDesc: "Every `.action` route sits behind login; the session chain secures the experience.",
searchEyebrow: "Quick search", portalCard1Title: "Request lifecycle",
searchTitle: "Filter labs by keyword", portalCard1Desc: "Dispatcher → Action → Interceptor → Result shows the execution path.",
statusEyebrow: "Current session", portalCard2Title: "Login protection",
statusTitle: "Login state and access guidance", portalCard2Desc: "`LoginAction` writes Session, `AuthInterceptor` blocks unauthenticated hits.",
routeEyebrow: "Study order", portalCard3Title: "Forms & validation",
routeTitle: "Suggested learning order", portalCard3Desc: "Field binding plus `ValidationAction` determine success/failure views.",
sessionStateTitle: "Logged in", portalCard4Title: "AJAX / REST contrast",
dashboardLink: "Open dashboard", portalCard4Desc: "Compare `ajax.action` (AJAX) with `api/users.action` (REST) outputs."
logoutLink: "Log out",
loginLink: "Open login page",
routeStep1Title: "1. Hello action",
routeStep1Text: "Start with the smallest action mapping and build the request -> action -> result mental model.",
routeStep2Title: "2. Session login",
routeStep2Text: "See how login writes into session and how an interceptor protects later pages.",
routeStep3Title: "3. Forms and validation",
routeStep3Text: "Review field binding, validation errors, and success summaries.",
routeStep4Title: "4. Upload and JSON",
routeStep4Text: "Finish with file upload and JSON responses to see how classic MVC extends outward.",
catalogEyebrow: "Catalog",
catalogTitle: "Core lab entries",
tagHello1: "Action",
tagHello2: "Basics",
cardHelloTitle: "Hello action",
cardHelloText: "Run the smallest Struts2 action and inspect how request parameters reach the action and return through a result page.",
cardHelloRun: "Run demo",
tagLogin1: "Login",
tagLogin2: "Session",
cardLoginTitle: "Login and dashboard",
cardLoginText: "This is the main upgrade in this project: a classic session login plus interceptor-protected learning pages.",
cardLoginOpen: "Open login",
cardLoginDashboard: "Open dashboard",
tagUser1: "Form",
tagUser2: "Binding",
cardUserTitle: "User profile submit",
cardUserText: "Demonstrates field binding, error echoing, and a success summary page.",
cardUserOpen: "Open user form",
tagValidation1: "Validation",
tagValidation2: "Rules",
cardValidationTitle: "Field validation lab",
cardValidationText: "Compare failed and successful validation states and explain why validate() runs before business output.",
cardValidationOpen: "Open validation page",
tagUpload1: "Upload",
tagUpload2: "Safe demo",
cardUploadTitle: "Upload metadata lab",
cardUploadText: "Keeps multipart binding visible but avoids writing files to disk, which is safer for local and VPS demos.",
cardUploadOpen: "Open upload page",
tagJson1: "JSON",
tagJson2: "API style",
cardJsonTitle: "AJAX and REST responses",
cardJsonText: "Keeps JSON and REST-style samples to show how a classic MVC project can evolve into API output.",
cardJsonAjax: "Open AJAX JSON",
cardJsonRest: "Open REST JSON",
learningBoardEyebrow: "Visual learning",
learningBoardTitle: "Struts2 request & auth flow",
learningBoardDesc: "Timeline cards highlight how requests traverse Action, Interceptor and Result so the flow is easier to explain.",
timelineEyebrow: "Request lifecycle",
timelineTitle: "Inbound → Action → Result",
timelineDesc: "The dispatcher resolves the action, runs interceptors, then chooses which JSP or JSON should render.",
timelineStep1: "1. Request arrives",
timelineStep1Text: "Dispatcher maps namespace/action, binds parameters, prepares execution.",
timelineStep2: "2. Interceptor chain",
timelineStep2Text: "`AuthInterceptor` enforces session while others handle validation/uploads.",
timelineStep3: "3. Result output",
timelineStep3Text: "Action returns SUCCESS/INPUT and Struts renders JSP or JSON back to the browser.",
chainEyebrow: "Action → Interceptor → Result",
chainTitle: "Control chain: ops → auth → render",
chainDesc: "Interceptors decide if the request passes; the result determines view or API output.",
chainExplain: "Action owns business logic, Interceptor handles guardrails, Result delivers the final payload.",
insightEyebrow: "Learning insights",
insightTitle: "Key links & lab comparison",
loginCardTitle: "Session login + interceptor",
loginCardDesc: "`LoginAction`, `AuthInterceptor`, and the dashboard string together a safe auth chapter.",
loginCardItem1: "Submit admin / 123456 to write session",
loginCardItem2: "`AuthInterceptor` blocks unauthenticated access",
loginCardItem3: "Successful login lands on dashboard before labs",
bindingCardTitle: "Form binding snapshot",
bindingCardDesc: "Observe how the user form, validation, and result respond to field binding and checks.",
bindingCardItem1: "Form field names mirror Action properties",
bindingCardItem2: "`UserAction#submit` routes based on validation",
bindingCardItem3: "Success page shows a structured summary",
jsonCardTitle: "AJAX vs REST JSON",
jsonCardDesc: "Line up `ajax.action` and `api/users.action` to explain Struts JSON modes.",
jsonCardItem1: "`ajax.action` targets browser interactions with success + payload",
jsonCardItem2: "`api/users.action` stays RESTy for API teaching"
},
html: {
helperList: "<li>Start with login and the dashboard to understand the classic session-auth path.</li><li>Move to forms, validation, and upload to see how actions receive and process input.</li><li>Finish with AJAX and REST-style responses to explain Struts2 extension points.</li>"
},
placeholders: {
searchInput: "Try login, session, validation, upload, json"
},
emptyState: "No lab matched the current keyword."
} }
},
render: function (uiState) {
const sessionTitle = document.getElementById("sessionStateTitle");
const sessionBody = document.getElementById("sessionStateBody");
if (!sessionTitle || !sessionBody) {
return;
}
const authenticated = sessionBody.dataset.authenticated === "true";
if (authenticated) {
const roleLabel = uiState.language === "zh" ? "演示管理员" : "Demo administrator";
sessionTitle.textContent = uiState.language === "zh" ? "当前已登录" : "Logged in";
sessionBody.innerHTML = uiState.language === "zh"
? `用户:${sessionBody.dataset.user}<br/>角色:${roleLabel}<br/>建议先打开仪表盘,再进入受保护表单页。`
: `User: ${sessionBody.dataset.user}<br/>Role: ${roleLabel}<br/>Open the dashboard first, then continue to the protected form pages.`;
} else {
sessionTitle.textContent = uiState.language === "zh" ? "当前未登录" : "Not logged in";
sessionBody.innerHTML = uiState.language === "zh"
? "建议先进入登录页。用户表单、校验页和上传页都已经接入 Session 鉴权链路。"
: "Start with the login page. The user form, validation page, and upload page now sit behind the session-auth path.";
} }
} }
}); });
runSearch();
</script> </script>
</body> </body>
</html> </html>