导读:本期聚焦于孙志远创作的《Spring Boot 如何整合 Spring Security 实现表单认证?》,敬请观看详情。表单认证的本质是把用户提交的账号密码与后端存储的凭证进行比对,通过后创建会话并重定向到受保护资源。Spring Boot 整合 Spring Security 并不需要手写过滤器,核心在于配置一个 SecurityFilterChain,指定登录页、登录处理地址、成功和失败跳转规则。本文从认证流程切入,先说明默认行为为何会拦截所有请求,再逐步构建自定义 UserDetailsService 和 PasswordEncoder,并结合内存用户演示完整登录链路。过程中会重点处理 CSRF 保护、会话固定攻击、登出清理以及常见配置错误,比如密码未加密导致 500 或登录后无限重定向。读完后你可以直接在 Spring Boot 项目中复现这套表单登录方案。

一、表单认证的运行机制与项目初始化

Spring Security 的表单认证并不是一个独立框架,而是基于 Filter 链构建的一套完整认证流程。当浏览器访问一个受保护资源时,请求会先经过 SecurityFilterChain 中的多个过滤器,其中 UsernamePasswordAuthenticationFilter 专门处理登录提交请求。它的核心逻辑是从请求参数中读取用户名和密码,封装成 Authentication 对象,再交给 AuthenticationManager 完成身份比对。比对通过后,认证信息会被写入 SecurityContext 并保存到 HttpSession,之后同一个会话内的请求都能携带已认证身份继续访问。

Spring Boot 如何整合 Spring Security 实现表单认证?

理解这个机制可以避免只抄配置却不知道为何生效。默认情况下,Spring Boot 引入 spring-boot-starter-security 后,所有端点都会要求认证,未登录用户会被重定向到 /login 页面。该默认登录页由框架生成,用户名固定为 user,密码在启动日志中随机打印。这种方式只适合快速体验,无法对接业务用户表,也无法控制登录页样式。

要替换默认行为,第一步是创建项目并添加依赖。通常需要 spring-boot-starter-web、spring-boot-starter-security 以及 Thymeleaf 模板引擎。本文使用 Thymeleaf 渲染登录页,但思路同样适用于 JSP 或纯静态资源。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
</dependencies>

添加依赖后启动应用,Spring Boot 会自动装配默认的 SecurityFilterChain。此时访问 http://localhost:8080/index 会看到浏览器跳转到 /login。如果直接使用默认账号登录,只能验证环境是否可用,无法满足真实业务需求,因此需要自定义安全配置。

二、配置 SecurityFilterChain 与自定义登录页

自定义表单认证的第一步是提供 SecurityFilterChain Bean。在 Spring Security 较新版本中,推荐使用 HttpSecurity 的 authorizeHttpRequests 和 formLogin 方法,而不是已经过时的 authorizeRequests。下面这个配置类声明了登录页地址、登录处理地址、成功跳转地址以及放行的静态资源路径。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login", "/css/**", "/js/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/home", true)
                .failureUrl("/login?error=true")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout=true")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
            );
        return http.build();
    }
}

这里有几个关键点需要说明。loginPage("/login") 指定的是登录页的 GET 地址,而 loginProcessingUrl("/doLogin") 指定的是登录表单 POST 提交给 Spring Security 的地址,不需要自己写该地址对应的控制器。defaultSuccessUrl("/home", true) 的第二个参数为 true,表示登录成功后总是跳转到 /home,即使登录前用户访问的是其他受保护页面。如果设为 false,则会优先跳回登录前试图访问的地址。

由于我们指定了自定义登录页地址 /login,就必须提供一个返回登录视图的控制器。Spring Security 不会替你渲染自定义页面,它只负责在未认证时重定向到该地址。

@Controller
public class LoginController {

    @GetMapping("/login")
    public String loginPage() {
        return "login";
    }

    @GetMapping("/home")
    public String homePage() {
        return "home";
    }
}

在 src/main/resources/templates 目录下创建 login.html。表单必须使用 POST 提交到 /doLogin,并且携带 username 和 password 两个字段。Spring Security 默认从名为 username 和 password 的参数中读取账号密码,若前端字段名不同,需要在 formLogin 中通过 usernameParameterpasswordParameter 修改。

Spring Security 默认开启 CSRF 防护,登录表单必须携带 CSRF token,否则提交后会返回 403。使用 Thymeleaf 时,可以通过模板属性手动渲染隐藏字段。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
    <h2>用户登录</h2>
    <form th:action="@{/doLogin}" method="post">
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
        <div>
            <label>用户名:</label>
            <input type="text" name="username"/>
        </div>
        <div>
            <label>密码:</label>
            <input type="password" name="password"/>
        </div>
        <button type="submit">登录</button>
    </form>
    <p th:if="${param.error}">用户名或密码错误</p>
    <p th:if="${param.logout}">已退出登录</p>
</body>
</html>

如果登录页不是由 Thymeleaf 渲染,而是纯静态 HTML,就需要额外处理 CSRF token 的获取。可以直接关闭 CSRF,但在表单认证场景下不建议这样做,因为登录接口被跨站请求伪造的风险较高。更稳妥的做法是前后端分离时改用 CookieCsrfTokenRepository 或通过接口读取 token。配置完成后,启动应用访问任意受保护资源都会跳转到自定义登录页,样式已经可以完全由自己控制。

三、构建用户来源与密码加密

当用户提交账号密码后,Spring Security 需要从某个地方查询用户。这个动作由 AuthenticationManager 委托给 UserDetailsService 完成。UserDetailsService 只负责根据用户名加载用户信息,如果找不到则抛出 UsernameNotFoundException。最简单的方式是在内存中定义用户,适合学习和测试。

@Bean
public UserDetailsService userDetailsService() {
    UserDetails admin = User.withUsername("admin")
            .password(passwordEncoder().encode("123456"))
            .roles("ADMIN")
            .build();
    UserDetails normal = User.withUsername("user")
            .password(passwordEncoder().encode("123456"))
            .roles("USER")
            .build();
    return new InMemoryUserDetailsManager(admin, normal);
}

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

上面的配置中存在一个容易忽略的细节:密码必须经过 PasswordEncoder 加密后再存入 UserDetails。Spring Security 5 以后强制要求密码具有编码格式,如果直接使用明文,登录时会抛出 There is no PasswordEncoder mapped for the id "null"。如果数据库中已经有历史明文密码,可以在密码前加 {noop} 前缀临时兼容,但生产环境必须迁移为 BCrypt 等强哈希算法。

真实项目中用户通常存储在数据库中,此时需要自定义 UserDetailsService 实现类。下面是一个基于 JdbcTemplate 的简单示例,演示如何从 users 表加载用户并返回 UserDetails 对象。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final JdbcTemplate jdbcTemplate;

    public UserDetailsServiceImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String sql = "SELECT username, password, enabled FROM users WHERE username = ?";
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, username);
        if (rows.isEmpty()) {
            throw new UsernameNotFoundException("用户不存在");
        }
        Map<String, Object> row = rows.get(0);
        String password = (String) row.get("password");
        return User.withUsername(username)
                .password(password)
                .roles("USER")
                .build();
    }
}

该实现返回的密码应当是数据库中已经 BCrypt 加密后的值。如果你使用的是 JPA 或 MyBatis,只需把数据访问层替换掉,核心逻辑不变。还需要注意,UserDetails 接口中包含账户是否启用、是否过期、是否锁定等状态字段,这些字段会参与认证前的预检查。如果用户被禁用,Spring Security 不会继续比对密码,而是直接抛出 DisabledException。

密码加密器可以统一使用 BCryptPasswordEncoder,它的特点是每次加密同一个明文会生成不同的密文,因此无法直接通过数据库比对,只能使用 matches 方法验证。配置为 Bean 后,Spring Security 会在登录比对时自动调用该加密器,无需手动处理密码验证逻辑。

四、处理会话、登出与常见错误排查

表单认证成功后,Spring Security 会创建 SecurityContext 并保存到 HttpSession 中。默认情况下,同一个浏览器会话内访问其他受保护资源不再要求重新登录。为了保护会话,可以配置 sessionManagement 来限制并发会话,避免同一账号在多个终端同时在线造成安全问题。

http
    .sessionManagement(session -> session
        .maximumSessions(1)
        .expiredUrl("/login?expired=true")
    );

这段配置表示同一个用户名最多允许一个有效会话。如果第二个登录请求成功,第一个会话会被标记为过期,之后的请求会被重定向到 /login?expired=true。需要注意,这个功能默认使用 SessionRegistry 实现,如果应用部署在多个实例上,需要配合共享会话存储,否则无法跨节点判断会话数量。

登出操作通过内置的 /logout 过滤器完成。当用户访问 /logout 时,Spring Security 会清理 SecurityContext、使 HttpSession 失效,并删除 JSESSIONID Cookie。配置 logoutSuccessUrl 可以决定登出后跳转到哪个页面。实际项目中通常会在页面右上角放置退出链接,指向 /logout,并建议使用 POST 方式提交以配合 CSRF 防护。

最后总结几个表单认证中常见的错误。第一,登录后一直返回 403,通常是 CSRF token 缺失或过期,检查登录页是否包含隐藏的 _csrf 字段。第二,登录时出现 There is no PasswordEncoder mapped for the id "null",说明密码没有使用标准编码格式,需要在 PasswordEncoder 中统一加密。第三,登录成功后无限重定向,多是因为 defaultSuccessUrl 的第二个参数为 false,且登录前访问的地址是 /login 本身,建议改为 true 或明确处理根路径。第四,静态资源被安全策略拦截,导致登录页样式丢失,需要在 authorizeHttpRequests 中显式放行 /css、/js 等目录。理解这些细节之后,Spring Boot 表单认证才算真正落地可控。

Spring BootSpring Security表单认证修改时间:2026-08-26 12:18:03

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。