fix: enforce html auth redirect at runtime

This commit is contained in:
Codex
2026-03-25 09:13:12 +08:00
parent d81750aaf9
commit 923302ca78
3 changed files with 30 additions and 18 deletions

View File

@@ -28,21 +28,14 @@ public class LearningJwtFilter extends OncePerRequestFilter {
@Override @Override
protected boolean shouldNotFilter(HttpServletRequest request) { protected boolean shouldNotFilter(HttpServletRequest request) {
String uri = request.getRequestURI(); String uri = request.getRequestURI();
boolean learnRoute = "/learn".equals(uri) || uri.startsWith("/learn/"); return !(isProtectedPage(uri)
boolean protectedPage = "/".equals(uri)
|| "/home".equals(uri)
|| "/index.html".equals(uri)
|| "/users.html".equals(uri)
|| "/aop.html".equals(uri)
|| "/events.html".equals(uri);
return !(protectedPage
|| uri.startsWith("/api/secure/") || uri.startsWith("/api/secure/")
|| uri.equals("/api/users") || uri.equals("/api/users")
|| uri.startsWith("/api/users/") || uri.startsWith("/api/users/")
|| "/aop".equals(uri) || "/aop".equals(uri)
|| uri.startsWith("/aop/") || uri.startsWith("/aop/")
|| uri.startsWith("/api/lab/") || uri.startsWith("/api/lab/")
|| learnRoute); || isLearnRoute(uri));
} }
@Override @Override
@@ -61,9 +54,28 @@ public class LearningJwtFilter extends OncePerRequestFilter {
SecurityContextHolder.getContext().setAuthentication(authToken); SecurityContextHolder.getContext().setAuthentication(authToken);
} }
if (isProtectedPage(request.getRequestURI())
&& SecurityContextHolder.getContext().getAuthentication() == null) {
response.sendRedirect("/access.html");
return;
}
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
private boolean isProtectedPage(String uri) {
return "/".equals(uri)
|| "/home".equals(uri)
|| "/index.html".equals(uri)
|| "/users.html".equals(uri)
|| "/aop.html".equals(uri)
|| "/events.html".equals(uri);
}
private boolean isLearnRoute(String uri) {
return "/learn".equals(uri) || uri.startsWith("/learn/");
}
private String resolveToken(HttpServletRequest request) { private String resolveToken(HttpServletRequest request) {
String authorization = request.getHeader("Authorization"); String authorization = request.getHeader("Authorization");
if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer ")) { if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer ")) {

View File

@@ -8,10 +8,10 @@
const TEXT = { const TEXT = {
zh: { zh: {
brand: "Spring 学习工作台", brand: "Spring 学习工作台",
home: "学习总控台", home: "总控台",
access: "登录页", access: "登录页",
loginReady: "已登录,受保护页面和接口可访问", loginReady: "已登录,受保护页面和接口可访问",
loginMissing: "未登录,受保护页面会跳回登录页", loginMissing: "未登录,受保护页面会跳回登录页",
currentUser: "当前用户", currentUser: "当前用户",
logout: "退出登录", logout: "退出登录",
login: "去登录", login: "去登录",
@@ -19,7 +19,7 @@
unauthorized: "未登录或认证已失效,请先打开登录页重新登录。", unauthorized: "未登录或认证已失效,请先打开登录页重新登录。",
requestFailed: "请求失败,请稍后再试。", requestFailed: "请求失败,请稍后再试。",
authRequiredTitle: "登录后即可使用", authRequiredTitle: "登录后即可使用",
authRequiredMessage: "请先去登录页获取 token才能继续访问控台和实验", authRequiredMessage: "请先去登录页获取 token随后再访问控台和实验页。",
authRequiredButton: "前往登录" authRequiredButton: "前往登录"
}, },
en: { en: {
@@ -45,19 +45,19 @@
jwt: { jwt: {
label: "JWT 路线", label: "JWT 路线",
summary: "浏览器先调用 /api/auth/login 获取 Bearer Token之后通过 Authorization 头访问受保护接口,重点学习无状态鉴权。", summary: "浏览器先调用 /api/auth/login 获取 Bearer Token之后通过 Authorization 头访问受保护接口,重点学习无状态鉴权。",
learnMore: "观察 LearningJwtFilter 如何从 Header 或 Cookie 中提取认证信息。" learnMore: "可以观察 LearningJwtFilter 如何从 Header 或 Cookie 中提取认证信息。"
}, },
satoken: { satoken: {
label: "Sa-Token 对照", label: "Sa-Token 对照",
summary: "这里先用可视化说明 Sa-Token 常见的会话、注解与拦截器思路,帮助你和 JWT 的链路做对比。", summary: "这里先用可视化说明 Sa-Token 常见的会话、注解与拦截器思路,帮助你和 JWT 的链路做对比。",
learnMore: "适合续扩展成 Sa-Token 章节,比较登录态持久化、注解鉴权与刷新机制。" learnMore: "适合续扩展成 Sa-Token 章节,比较登录态持久化、注解鉴权和续期机制。"
}, },
filters: ["LearningJwtFilter", "AuthenticationEntryPoint", "AuthorizationFilter"], filters: ["LearningJwtFilter", "AuthenticationEntryPoint", "AuthorizationFilter"],
tokenTips: "当前实现会同时下发 Bearer Token 和认证 Cookie。这样既能继续学习 Header 鉴权,也能让普通页面跳转真正受登录保护。", tokenTips: "当前实现会同时下发 Bearer Token 和认证 Cookie。这样既能继续学习 Header 鉴权,也能让普通页面跳转真正受登录保护。",
timeline: [ timeline: [
{ stage: "1. 登录", detail: "POST /api/auth/login服务端同时返回 token 与认证 Cookie。" }, { stage: "1. 登录", detail: "POST /api/auth/login服务端同时返回 token 与认证 Cookie。" },
{ stage: "2. 访问页面", detail: "浏览器普通跳转自动携带 Cookie受保护 HTML 页面可直接放行。" }, { stage: "2. 访问页面", detail: "浏览器普通跳转自动携带 Cookie受保护 HTML 页面可直接放行。" },
{ stage: "3. 调接口", detail: "前端 fetch 默认带 Cookie如果本地存 token也会补上 Authorization 头。" }, { stage: "3. 调接口", detail: "前端 fetch 默认带 Cookie如果本地存 token也会补上 Authorization 头。" },
{ stage: "4. 失效处理", detail: "认证缺失或失效时,页面跳回 /access.html接口返回 401。" } { stage: "4. 失效处理", detail: "认证缺失或失效时,页面跳回 /access.html接口返回 401。" }
] ]
}, },

View File

@@ -82,7 +82,7 @@ class AuthFlowTest {
void protectedHtmlShouldLoadWithAuthCookie() throws Exception { void protectedHtmlShouldLoadWithAuthCookie() throws Exception {
mockMvc.perform(get("/index.html").cookie(authCookie()).accept(MediaType.TEXT_HTML)) mockMvc.perform(get("/index.html").cookie(authCookie()).accept(MediaType.TEXT_HTML))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(content().string(containsString("learning-menu-title"))); .andExpect(content().string(containsString("consoleOutput")));
} }
@Test @Test