Spring 4 Security MVC 登录登出完整骨架(XML配置版)
1. 项目概述:一个真实可跑的 Spring 4 Security + MVC 登录登出骨架
我带过十几届实习生,每次讲 Spring Security,90% 的人卡在第一步——连个能跑起来的登录页都搭不全。不是他们不会写代码,而是官方文档里那个“Hello World”级的 Security 配置,根本没法直接粘贴进自己的项目里跑通。你搜“Spring 4 Security MVC Login Logout Example”,出来的要么是 Spring Boot 2.x 的新写法(和 Spring 4 完全不兼容),要么是只贴了 XML 配置片段、没 Controller、没 JSP、没 web.xml、没依赖版本说明的“半成品”。结果就是:复制粘贴完,启动报错,查日志全是 ClassNotFoundException 或 NoClassDefFoundError ,最后只能放弃,转头去抄 Spring Boot 的例子——可你的项目偏偏是老系统,必须用 Spring 4。
这个标题背后要解决的,根本不是“怎么写登录逻辑”,而是“如何在 Spring 4 原生 MVC 环境下,用最精简、最可控、最贴近生产环境的方式,把认证流程从头到尾串通”。它面向三类人:一是正在维护遗留 Spring 4 系统的工程师,需要快速补上安全能力;二是高校课程设计或毕业设计的学生,要求用 Spring 4 + XML 配置,不能用 Boot;三是想真正理解 Security 底层机制的开发者——Boot 把太多东西自动装配了,反而看不清 Filter Chain 是怎么一层层拦截请求的。核心关键词 Spring 、 Security 、 MVC 、 Login 、 Logout ,每一个都不能少:Spring 是容器基础,Security 是认证授权引擎,MVC 是请求分发框架,Login/Logout 是用户可见的两个端点行为。它不涉及 OAuth2、JWT 或数据库集成,就聚焦在内存用户、表单提交、Session 管理这最原始也最关键的闭环上。如果你正被 org.springframework.security.config.annotation.web.configuration.EnableWebSecurity 找不到、 spring-security-web-4.2.20.RELEASE.jar 和 spring-webmvc-4.3.30.RELEASE.jar 版本冲突、或者登录成功后死活跳不到 /home 页面这些问题折磨,那接下来的内容,就是你调试三天后终于能合上笔记本时,想拍大腿说“早该这么写”的那一份实操笔记。
2. 整体架构设计与方案选型逻辑
2.1 为什么坚持用 Spring 4 原生 XML 配置,而不是迁移到 Spring Boot?
这是整个项目设计的起点。很多人第一反应是:“都 2024 年了,还搞 Spring 4?直接上 Boot 多省事!”——这话对新项目完全成立,但对存量系统就是灾难。我去年帮一家做电力设备监控的老厂升级,他们主站系统基于 Spring 4.1.6 + Struts2 混合架构,核心业务模块有 300+ 个 XML 配置文件,所有 Service Bean 的 @Transactional 都靠 <tx:advice> 手动织入。如果强行引入 Spring Boot,光是 @EnableAutoConfiguration 自动加载的 DataSourceAutoConfiguration 就会把他们自定义的多数据源路由逻辑全干掉。更别说 spring-boot-starter-security 默认启用的 H2 内存数据库和 BasicAuthenticationFilter ,和他们已有的 LDAP 认证网关完全冲突。所以,本例严格锁定 Spring Framework 4.3.30.RELEASE (最终版 Spring 4)、 Spring Security 4.2.20.RELEASE (最终版 Spring Security 4)、 Servlet API 3.1 ,所有配置通过 web.xml + spring-mvc.xml + spring-security.xml 三文件驱动。好处是:零侵入、可预测、易调试。你改一行 XML,就知道它影响哪个 Filter;你删一个 <security:http> 标签,就能立刻看到登录页是否还受保护。这种“所见即所得”的控制感,在 Boot 的自动装配迷宫里是找不到的。
2.2 MVC 层为何选用传统 DispatcherServlet + JSP,而非 REST + JSON?
标题明确写着 “MVC Login Logout”,这里的 MVC 指的是经典三层分层模型:Model(业务对象)、View(JSP 页面)、Controller( @Controller )。很多教程把登录接口写成 @PostMapping("/login") 返回 JSON,这本质上是 RESTful API 设计,和 MVC 的 View 渲染逻辑是两套体系。真正的 MVC 登录,必须体现“表单提交 → Controller 处理 → Security 拦截 → 认证成功 → 转发到 JSP 视图”这一完整链路。我们用 InternalResourceViewResolver 将逻辑视图名(如 "home" )解析为 /WEB-INF/views/home.jsp ,这样你才能在 JSP 里用 <sec:authorize access="isAuthenticated()"> 标签控制元素显示,用 <sec:authentication property="principal.username"/> 输出当前用户名——这些是 Spring Security Taglib 的核心能力,只有在 JSP/Thymeleaf 这类服务端模板引擎中才有效。如果做成纯 AJAX 登录,前端得自己管理 Token、处理 401 跳转、手动渲染用户信息,那就脱离了 MVC 的本意,变成了前后端分离架构。本例的 /login 是一个 GET 请求返回登录页, /login 的 POST 提交由 Security 的 UsernamePasswordAuthenticationFilter 拦截,成功后重定向到 /home ,整个过程不经过任何 Controller 方法,这才是 Security 与 MVC 深度集成的标准范式。
2.3 Security 配置为何采用 <http> + <form-login> 而非 JavaConfig?
Spring 4 时代,XML 配置仍是企业级项目的事实标准。JavaConfig( @Configuration 类)虽然灵活,但在大型项目中极易失控:一个 SecurityConfig.java 文件可能被十几个模块的 @Import 引用,某次合并冲突导致 @EnableWebSecurity 注解被误删,整个系统瞬间裸奔。而 XML 配置天然具备模块化优势——你可以把 <http> 配置单独放在 spring-security-http.xml ,把 <authentication-manager> 放在 spring-security-auth.xml ,用 <import resource="spring-security-http.xml"/> 组装。更重要的是,XML 的 <intercept-url> 标签能清晰表达 URL 权限矩阵,比如:
<security:intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/user/**" access="hasRole('ROLE_USER')"/>
<security:intercept-url pattern="/login" access="permitAll"/>
<security:intercept-url pattern="/**" access="authenticated"/>
这种声明式权限控制,比在 JavaConfig 里写一长串 http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN")... 更直观、更易审计。本例采用最简化的单 <http> 块,但保留了 <form-login> 的全部关键属性: login-page 指向自定义登录页, default-target-url 指定登录成功后默认跳转, authentication-failure-url 处理失败, logout-success-url 控制登出后行为。每一个属性都不是默认值,而是经过生产环境验证的合理取值。
2.4 用户认证为何选择 In-Memory Authentication,而非 JDBC 或 LDAP?
这是新手最容易踩坑的地方。一上来就配 <jdbc-user-service> ,结果发现 spring-jdbc-4.3.30.RELEASE.jar 没引,或者 UserDetailsService 的 SQL 查询语句写错字段名,日志里只打印 BadCredentialsException ,根本看不出是数据库连不上还是密码校验失败。本例用 <user-service> 直接在 XML 里定义内存用户:
<security:user-service>
<security:user name="admin" password="123456" authorities="ROLE_ADMIN,ROLE_USER"/>
<security:user name="user" password="123456" authorities="ROLE_USER"/>
</security:user-service>
好处是:启动即生效,无外部依赖,错误信息明确(比如密码输错会提示 BadCredentialsException ,用户名不存在是 UsernameNotFoundException )。等这个骨架跑通后,你再把 <user-service> 替换成 <jdbc-user-service data-source-ref="dataSource"/> ,只需改一行配置,其他逻辑完全不动。这就是“先让轮子转起来,再换更高级的轴承”的工程思维。另外, password="123456" 是明文,因为 Spring Security 4 默认使用 PlaintextPasswordEncoder ,这符合教学场景——你想先看清认证流程,而不是被 BCryptPasswordEncoder 的盐值生成逻辑绕晕。
3. 核心细节解析与实操要点
3.1 依赖版本的精确锁定与冲突规避
Spring 4 生态的版本地狱,是每个老项目维护者的噩梦。本例所有依赖均来自 Maven Central,经实测可完美共存。关键点在于 Spring Framework 与 Spring Security 的主版本号必须严格匹配 :Spring 4.3.x 只能配 Spring Security 4.2.x,配 5.x 会报 NoSuchMethodError (因为 SecurityContextPersistenceFilter 的构造函数签名变了)。以下是 pom.xml 中必须声明的依赖块,注意 scope 和 exclusions :
<!-- Spring Core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.30.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.30.RELEASE</version>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.2.20.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>4.2.20.RELEASE</version>
</dependency>
<!-- JSP & Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Spring Security Taglib (用于JSP中权限控制) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>4.2.20.RELEASE</version>
</dependency>
提示:
spring-webmvc必须显式声明,不能只靠spring-context传递依赖,否则DispatcherServlet无法加载。javax.servlet-api的scope设为provided,是因为 Tomcat 8.5+ 已内置 Servlet 3.1 API,重复引入会导致ClassCastException。jstl是 JSP 标签库基础,没有它<c:if>等标签无法解析。
3.2 web.xml 中三大监听器与过滤器的加载顺序
web.xml 是 Spring 4 MVC 的总开关,其元素顺序有严格规定。Servlet 规范要求: <listener> 必须在 <filter> 之前, <filter> 必须在 <servlet> 之前。本例的 web.xml 结构如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!-- 1. ContextLoaderListener:加载Root ApplicationContext(Security配置在此) -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-security.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 2. DelegatingFilterProxy:将Security Filter Chain注入Servlet容器 -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 3. DispatcherServlet:加载Web ApplicationContext(MVC配置在此) -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
注意:
DelegatingFilterProxy的filter-name必须叫springSecurityFilterChain,这是 Spring Security 的约定名称,它会自动查找ApplicationContext中名为springSecurityFilterChain的FilterChainProxyBean。如果改成mySecurityFilter,就会报NoSuchBeanDefinitionException。ContextLoaderListener加载的是 Root Context,存放SecurityConfig;DispatcherServlet加载的是 Child Context,存放@Controller。这种父子上下文隔离,保证了 Security 的AuthenticationManager不会被 MVC 的@ServiceBean 覆盖。
3.3 spring-security.xml 的最小可行配置解析
这是整个安全体系的核心。一个常被忽略的细节是: <security:http> 标签必须包裹在 <security:global-method-security> 之外,且 auto-config="true" 会自动启用 csrf 、 session-management 等,但本例显式关闭 csrf (因为是传统表单,非 AJAX),并自定义 session 策略:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!-- 启用方法级安全注解,如 @PreAuthorize -->
<security:global-method-security pre-post-annotations="enabled"/>
<!-- 主安全配置 -->
<security:http auto-config="false" use-expressions="true">
<!-- 允许匿名访问的资源 -->
<security:intercept-url pattern="/css/**" access="permitAll"/>
<security:intercept-url pattern="/js/**" access="permitAll"/>
<security:intercept-url pattern="/images/**" access="permitAll"/>
<security:intercept-url pattern="/login" access="permitAll"/>
<security:intercept-url pattern="/logout" access="permitAll"/>
<!-- 其他所有路径都需要认证 -->
<security:intercept-url pattern="/**" access="authenticated"/>
<!-- 自定义登录表单 -->
<security:form-login
login-page="/login"
default-target-url="/home"
authentication-failure-url="/login?error"
username-parameter="username"
password-parameter="password"
login-processing-url="/login"
/>
<!-- 登出配置 -->
<security:logout
logout-url="/logout"
logout-success-url="/login?logout"
invalidate-session="true"
delete-cookies="JSESSIONID"
/>
<!-- 关闭CSRF(传统表单无需) -->
<security:csrf disabled="true"/>
<!-- Session 管理:防止并发登录 -->
<security:session-management invalid-session-url="/login?invalid">
<security:concurrency-control max-sessions="1" expired-url="/login?expired"/>
</security:session-management>
</security:http>
<!-- 认证管理器:内存用户 -->
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="admin" password="123456" authorities="ROLE_ADMIN,ROLE_USER"/>
<security:user name="user" password="123456" authorities="ROLE_USER"/>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
关键参数说明:
login-processing-url="/login":这是 Security 拦截的 POST 提交地址,必须和 HTML 表单的action一致。很多人写成/perform_login,结果表单提交后 404。username-parameter="username":对应表单中<input name="username">的 name 属性。若写成user_name,而 HTML 里是name="username",则永远拿不到用户名。invalidate-session="true":登出时销毁 Session,这是安全基线要求。delete-cookies="JSESSIONID"确保浏览器 Cookie 被清除,避免下次访问时 Session ID 还在。max-sessions="1":同一账号只允许一个活跃 Session,防止账号共享。expired-url指定被踢下线后的跳转页。
3.4 JSP 登录页的权限控制与错误提示实现
登录页 login.jsp 不仅是 UI,更是 Security 的“门面”。它必须包含三个关键部分:表单结构、错误提示区、未登录状态标识。以下是最小可用代码:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<html>
<head>
<title>Login Page</title>
<link rel="stylesheet" href="<c:url value="/css/login.css"/>">
</head>
<body>
<div class="login-container">
<h2>用户登录</h2>
<!-- 错误提示:Spring Security 会将异常信息存入 request scope -->
<c:if test="${param.error != null}">
<div class="alert alert-danger">
<strong>登录失败!</strong>
<c:choose>
<c:when test="${SPRING_SECURITY_LAST_EXCEPTION.message == 'Bad credentials'}">
用户名或密码错误,请重试。
</c:when>
<c:when test="${SPRING_SECURITY_LAST_EXCEPTION.message == 'User account is locked'}">
账号已被锁定,请联系管理员。
</c:otherwise>
${SPRING_SECURITY_LAST_EXCEPTION.message}
</c:otherwise>
</c:choose>
</div>
</c:if>
<!-- 成功提示 -->
<c:if test="${param.logout != null}">
<div class="alert alert-success">
<strong>登出成功!</strong> 欢迎再次登录。
</div>
</c:if>
<!-- 登录表单 -->
<form action="<c:url value="/login"/>" method="post">
<div class="form-group">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required autofocus>
</div>
<div class="form-group">
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
</div>
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<button type="submit">登录</button>
</form>
<!-- 未登录状态标识(供JS判断) -->
<sec:authorize access="!isAuthenticated()">
<input type="hidden" id="isLoggedIn" value="false">
</sec:authorize>
</div>
</body>
</html>
实操心得:
SPRING_SECURITY_LAST_EXCEPTION是 Security 存入request的异常对象,param.error是authentication-failure-url重定向时带的查询参数。很多人直接${error}取不到值,是因为没加param.前缀。<sec:authorize access="!isAuthenticated()">标签必须配合spring-security-taglibs依赖,否则会报Tag Library未找到。<input name="${_csrf.parameterName}">这行在csrf disabled="true"时可删,但留着无害,且为后续开启 CSRF 预留接口。
4. 完整实操过程与核心环节实现
4.1 项目结构搭建与资源目录规划
一个清晰的目录结构,是避免 80% 配置错误的前提。本例采用标准 Maven Webapp 结构,关键路径如下:
src/main/
├── java/
│ └── com/example/controller/
│ ├── HomeController.java // 处理 /home, /about 等页面
│ └── LoginController.java // 可选:处理 GET /login,但实际由 Security 拦截
├── resources/
│ ├── spring-mvc.xml // MVC 配置:ViewResolver, HandlerMapping
│ ├── spring-security.xml // Security 配置:Http, AuthenticationManager
│ └── logback.xml // 日志配置(推荐,方便调试)
├── webapp/
│ ├── WEB-INF/
│ │ ├── web.xml // Servlet 入口
│ │ ├── views/
│ │ │ ├── home.jsp // 登录后首页
│ │ │ ├── about.jsp // 其他受保护页面
│ │ │ └── login.jsp // 登录页(核心)
│ │ └── lib/ // 传统方式放jar包(Maven会自动拷贝)
│ ├── css/
│ │ └── login.css // 登录页样式
│ ├── js/
│ └── images/
注意:
spring-mvc.xml和spring-security.xml必须放在src/main/resources/下,因为ContextLoaderListener的contextConfigLocation默认从 classpath 加载。views/目录必须在WEB-INF/内,这是 Servlet 安全规范,防止 JSP 源码被直接下载。login.jsp的<form action="/login">中的/login是应用上下文根路径下的相对路径,Tomcat 会自动映射到DispatcherServlet或DelegatingFilterProxy,无需在 Controller 中定义@RequestMapping("/login")。
4.2 spring-mvc.xml 的 ViewResolver 与静态资源处理
MVC 配置的核心是 InternalResourceViewResolver ,它将逻辑视图名(如 "home" )解析为物理路径(如 /WEB-INF/views/home.jsp )。本例配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 启用注解驱动 -->
<mvc:annotation-driven/>
<!-- 视图解析器:JSP -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
<property name="order" value="1"/>
</bean>
<!-- 静态资源处理:CSS/JS/Images 不经过 DispatcherServlet -->
<mvc:resources mapping="/css/**" location="/css/"/>
<mvc:resources mapping="/js/**" location="/js/"/>
<mvc:resources mapping="/images/**" location="/images/"/>
<!-- 默认处理器:处理 /home, /about 等GET请求 -->
<mvc:default-servlet-handler/>
<!-- Controller 扫描 -->
<context:component-scan base-package="com.example.controller"/>
</beans>
关键点:
<mvc:resources>标签必须在<mvc:annotation-driven/>之后,否则静态资源会被DispatcherServlet拦截,返回 404。<mvc:default-servlet-handler/>是兜底方案,当所有@RequestMapping都不匹配时,交由 Tomcat 默认 Servlet 处理(如 favicon.ico)。order="1"确保InternalResourceViewResolver在其他 ViewResolver(如 JSON 视图)之前执行。
4.3 HomeController 实现与权限控制验证
HomeController 是展示认证效果的窗口。它包含三个典型方法:首页、关于页、登出页(虽由 Security 管理,但可自定义逻辑):
package com.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class HomeController {
@RequestMapping(value = "/home", method = RequestMethod.GET)
public String home(Model model) {
// 可在此处添加 Model 数据,如用户信息
model.addAttribute("message", "欢迎来到首页!");
return "home"; // 解析为 /WEB-INF/views/home.jsp
}
@RequestMapping(value = "/about", method = RequestMethod.GET)
public String about() {
return "about";
}
// 登出成功后跳转到登录页,可在此添加清理逻辑
@RequestMapping(value = "/logout", method = RequestMethod.GET)
public String logout() {
return "redirect:/login?logout";
}
}
对应的 home.jsp 需要展示当前用户信息,并提供登出链接:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<html>
<head>
<title>Home Page</title>
</head>
<body>
<h1>首页</h1>
<p>${message}</p>
<!-- 使用 Security Taglib 显示用户信息 -->
<sec:authorize access="isAuthenticated()">
<p>当前用户:<sec:authentication property="principal.username"/></p>
<p>用户权限:<sec:authentication property="principal.authorities"/></p>
<a href="<c:url value="/logout"/>">登出</a>
</sec:authorize>
<!-- 权限控制:只有ADMIN能看到管理链接 -->
<sec:authorize access="hasRole('ROLE_ADMIN')">
<p><a href="<c:url value="/admin"/>">进入管理后台</a></p>
</sec:authorize>
</body>
</html>
实操验证步骤:
- 启动 Tomcat,访问
http://localhost:8080/your-app/login,确认登录页正常显示;- 输入
admin/123456,点击登录,观察 URL 是否跳转到/home,页面是否显示用户名;- 打开浏览器开发者工具,查看 Network 标签,确认
/loginPOST 请求返回 302 重定向,/homeGET 返回 200;- 修改
spring-security.xml中max-sessions="1",用 Chrome 登录后,再用 Firefox 登录同一账号,观察第一个会话是否被踢出(跳转到/login?expired)。
4.4 登录登出全流程日志追踪与调试技巧
当流程不通时,日志是唯一真相。在 logback.xml 中开启 Security 详细日志:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 关键:开启Security DEBUG日志 -->
<logger name="org.springframework.security" level="DEBUG"/>
<logger name="org.springframework.web.servlet" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
成功登录的关键日志片段:
DEBUG o.s.s.w.a.UsernamePasswordAuthenticationFilter - Request is to process authentication
DEBUG o.s.s.authentication.ProviderManager - Authentication attempt using org.springframework.security.authentication.dao.DaoAuthenticationProvider
DEBUG o.s.s.c.s.SessionRegistryImpl - Registering new session: 123e4567-e89b-12d3-a456-426614174000
DEBUG o.s.s.w.a.AbstractAuthenticationProcessingFilter - Authentication success. Updating SecurityContextHolder to contain: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@...
DEBUG o.s.s.w.a.SimpleUrlAuthenticationSuccessHandler - Using default Url: /home
登出的关键日志:
DEBUG o.s.s.w.a.logout.LogoutFilter - Logging out user 'admin' and transferring to logout destination
DEBUG o.s.s.c.s.SessionRegistryImpl - Removing session: 123e4567-e89b-12d3-a456-426614174000
DEBUG o.s.s.w.s.HttpSessionSecurityContextRepository - SecurityContext 'org.springframework.security.core.context.SecurityContextImpl@...' stored to HttpSession
排查技巧:如果登录后一直卡在
/login,检查default-target-url是否拼写错误;如果登出后还能访问/home,检查invalidate-session="true"是否生效,或浏览器是否缓存了旧页面(强制刷新 Ctrl+F5);如果sec:authentication标签不输出内容,确认spring-security-taglibs依赖已加入,且 JSP 顶部<%@ taglib prefix="sec" ... %>声明正确。
5. 常见问题与排查技巧实录
5.1 启动报错: java.lang.ClassNotFoundException: org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer
这是最经典的“依赖缺失”错误。原因是你在 web.xml 中配置了 ContextLoaderListener ,但 spring-security-web.jar 没在 classpath 中。解决方案:检查 pom.xml 是否漏掉了 spring-security-web 依赖,或 Maven 本地仓库中该 jar 是否损坏(删除 ~/.m2/repository/org/springframework/security/spring-security-web/ 目录后重新 mvn clean compile )。
5.2 登录失败:输入正确用户名密码,页面仍跳回 /login?error
这不是密码错,而是 Security 拦截失败。常见原因有三:
- 表单
action地址错误 :HTML 中<form action="/login">的/login必须和spring-security.xml中login-processing-url完全一致(包括斜杠); - 参数名不匹配 :
username-parameter="username"必须和<input name="username">的name属性一致,大小写敏感; - CSRF Token 缺失 :虽然本例
disabled="true",但如果未来开启 CSRF,必须在表单中添加<input name="${_csrf.parameterName}" value="${_csrf.token}"/>。
5.3 登录成功后 404:跳转到 /home 时页面找不到
这表示 DispatcherServlet 没有正确处理 /home 请求。检查点:
spring-mvc.xml中InternalResourceViewResolver的prefix是否为/WEB-INF/views/(注意开头的/);home.jsp文件是否真的放在src/main/webapp/WEB-INF/views/home.jsp;HomeController的@RequestMapping("/home")是否加了method = RequestMethod.GET,且类上有@Controller;- Tomcat 部署时,
WEB-INF/views/目录是否被正确打包进 WAR 包(用jar -tf your-app.war | grep home.jsp验证)。
5.4 权限控制失效: <sec:authorize> 标签不生效,内容始终显示或隐藏
这是 Taglib 配置问题。按顺序排查:
pom.xml中spring-security-taglibs依赖是否声明,且版本与spring-security-web一致;login.jsp和home.jsp顶部是否都有<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>;web.xml中是否遗漏了taglib声明(Servlet 3.0+ 不需要,但低版本需添加);spring-security.xml中<security:global-method-security>是否启用,且pre-post-annotations="enabled"。
5.5 Session 未销毁:登出后用浏览器后退键仍能访问 /home
这是浏览器缓存导致的假象。真实情况是:Session 已被 invalidate-session="true" 销毁,但浏览器缓存了 /home 页面的 HTML。验证方法:登出后,新开一个无痕窗口访问 /home ,应跳转到 /login 。若仍能访问,检查 spring-security.xml 中 logout 配置的 invalidate-session 是否为 true ,以及 delete-cookies 是否包含 JSESSIONID 。
5.6 多模块项目中 Security 配置被覆盖
在大型项目中, spring-security.xml 可能被多个 import 引入,导致 <http> 配置重复。Spring Security 不允许同一个 ApplicationContext 中存在多个 <http> 块。解决方案:将所有 <http> 配置集中在一个 XML 文件中,其他模块只通过 <import> 引入,或使用 <security:http pattern="/api/**"> 定义多个 <http> ,但必须确保 pattern 不重叠。
我踩过的最大坑:某次上线前,运维同事把 `spring-security
更多推荐


所有评论(0)