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

理解这个机制可以避免只抄配置却不知道为何生效。默认情况下,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 中通过 usernameParameter 和 passwordParameter 修改。
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