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 FilterChainProxy Bean。如果改成 mySecurityFilter ,就会报 NoSuchBeanDefinitionException ContextLoaderListener 加载的是 Root Context,存放 SecurityConfig DispatcherServlet 加载的是 Child Context,存放 @Controller 。这种父子上下文隔离,保证了 Security 的 AuthenticationManager 不会被 MVC 的 @Service Bean 覆盖。

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>

实操验证步骤:

  1. 启动 Tomcat,访问 http://localhost:8080/your-app/login ,确认登录页正常显示;
  2. 输入 admin/123456 ,点击登录,观察 URL 是否跳转到 /home ,页面是否显示用户名;
  3. 打开浏览器开发者工具,查看 Network 标签,确认 /login POST 请求返回 302 重定向, /home GET 返回 200;
  4. 修改 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 拦截失败。常见原因有三:

  1. 表单 action 地址错误 :HTML 中 <form action="/login"> /login 必须和 spring-security.xml login-processing-url 完全一致(包括斜杠);
  2. 参数名不匹配 username-parameter="username" 必须和 <input name="username"> name 属性一致,大小写敏感;
  3. 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 配置问题。按顺序排查:

  1. pom.xml spring-security-taglibs 依赖是否声明,且版本与 spring-security-web 一致;
  2. login.jsp home.jsp 顶部是否都有 <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
  3. web.xml 中是否遗漏了 taglib 声明(Servlet 3.0+ 不需要,但低版本需添加);
  4. 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

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐