跳到主要内容

快速开始

本章将帮助你在 Spring Boot 项目中快速集成 Spring Security,通过实际操作了解基本的安全配置。我们将从最简单的配置开始,逐步扩展功能。

环境准备

项目依赖

首先,创建一个 Spring Boot 项目并添加 Spring Security 依赖。如果你使用 Maven,在 pom.xml 中添加:

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

如果你使用 Gradle,在 build.gradle 中添加:

implementation 'org.springframework.boot:spring-boot-starter-security'

这个启动器依赖会自动引入 Spring Security 的核心组件以及相关配置。

版本说明

Spring Security 6.x 需要 Spring Boot 3.x 和 Java 17+。主要变化包括:

  • 全面采用 Lambda DSL 配置风格
  • 废弃 WebSecurityConfigurerAdapter,改用组件式配置
  • 默认启用 SecurityContextHolderFilter 替代 SecurityContextPersistenceFilter

最简配置

默认行为

添加依赖后,不做任何配置,Spring Security 已经为你的应用提供了基本的安全保护。启动应用后访问任何端点,你会被自动重定向到默认的登录页面。

默认配置包括:

  • 一个用户名为 user 的用户,密码在启动日志中随机生成
  • 表单登录和 HTTP Basic 认证
  • 所有请求都需要认证
  • CSRF 保护
  • Session 固定攻击防护

启动应用时,控制台会输出类似以下内容:

Using generated security password: 8e557730-4b8a-4f8a-9f5a-6c8b7d9e0f1a

这就是默认用户的密码。用户名固定为 user

查看默认密码

你可以在启动日志中找到生成的密码。或者,如果你想在配置文件中指定固定密码:

spring:
security:
user:
name: admin
password: admin123

这种方式适合开发和测试环境,生产环境绝不要这样配置。

基本安全配置

创建配置类

在 Spring Security 6 中,我们通过定义 SecurityFilterChain Bean 来配置安全规则:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 配置请求授权
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home", "/css/**", "/js/**").permitAll() // 公开访问
.requestMatchers("/admin/**").hasRole("ADMIN") // 需要 ADMIN 角色
.anyRequest().authenticated() // 其他请求需要认证
)
// 配置表单登录
.formLogin(form -> form
.loginPage("/login") // 自定义登录页面
.permitAll() // 登录页面允许公开访问
)
// 配置登出
.logout(logout -> logout
.permitAll()
)
// 启用 HTTP Basic 认证
.httpBasic(Customizer.withDefaults());

return http.build();
}
}

这个配置做了以下几件事:

  1. 设置 URL 访问权限:首页和静态资源公开访问,管理页面需要 ADMIN 角色,其他页面需要认证
  2. 配置表单登录:使用自定义登录页面
  3. 允许用户登出
  4. 启用 HTTP Basic 认证作为备选

配置用户存储

上面的配置只定义了访问规则,还需要配置用户信息。最简单的方式是使用内存用户存储:

@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password("{noop}password") // {noop} 表示明文密码
.roles("USER")
.build();

UserDetails admin = User.builder()
.username("admin")
.password("{noop}admin")
.roles("USER", "ADMIN")
.build();

return new InMemoryUserDetailsManager(user, admin);
}

{noop} 前缀表示密码以明文存储。这种方式仅用于演示,生产环境必须使用加密密码。

密码编码器配置

为什么需要密码编码器?

明文存储密码存在严重的安全隐患。即使数据库被攻破,使用安全的密码编码器也能保护用户密码不被轻易破解。Spring Security 提供了多种密码编码器实现。

推荐配置

import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

@Bean
public PasswordEncoder passwordEncoder() {
// 使用委托密码编码器,支持多种编码格式
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

委托编码器默认使用 BCrypt 加密新密码,同时支持验证旧格式的密码。密码存储时会带有编码类型前缀,如 {bcrypt}$2a$10$...

创建用户时使用编码器

@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password")) // 加密密码
.roles("USER")
.build();

return new InMemoryUserDetailsManager(user);
}

创建登录页面

简单登录表单

如果你使用 Thymeleaf,创建 login.html

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>登录</title>
</head>
<body>
<h1>登录</h1>

<!-- 登录错误提示 -->
<div th:if="${param.error}">
<p style="color: red;">用户名或密码错误</p>
</div>

<!-- 登出成功提示 -->
<div th:if="${param.logout}">
<p style="color: green;">已成功登出</p>
</div>

<form th:action="@{/login}" method="post">
<div>
<label>用户名:</label>
<input type="text" name="username" required />
</div>
<div>
<label>密码:</label>
<input type="password" name="password" required />
</div>

<!-- CSRF Token 会自动添加 -->
<button type="submit">登录</button>
</form>
</body>
</html>

控制器

创建控制器来处理登录页面:

@Controller
public class LoginController {

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

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

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

分层配置详解

HttpSecurity 常用配置项

HttpSecurity 提供了丰富的配置选项。以下是一个更完整的配置示例:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 1. 配置请求授权
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/user/**").hasRole("USER")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)

// 2. 配置异常处理
.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, authException) -> {
response.sendError(401, "未认证");
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.sendError(403, "无权限");
})
)

// 3. 配置表单登录
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/perform_login")
.defaultSuccessUrl("/home", true)
.failureUrl("/login?error=true")
)

// 4. 配置登出
.logout(logout -> logout
.logoutUrl("/perform_logout")
.logoutSuccessUrl("/login?logout=true")
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
)

// 5. 配置会话管理
.sessionManagement(session -> session
.maximumSessions(1) // 限制单点登录
.expiredUrl("/login?expired=true")
)

// 6. 配置 CSRF(开发环境可以禁用)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/**") // API 端点忽略 CSRF
)

// 7. 配置安全头
.headers(headers -> headers
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
);

return http.build();
}

Lambda DSL 风格说明

Spring Security 6 全面采用 Lambda DSL 配置风格,相比旧版本更加简洁直观:

// 旧版本写法(已废弃)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}

// 新版本写法
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> {});

return http.build();
}

新风格的优势:

  • 更好的代码可读性
  • 不需要链式调用 .and()
  • IDE 自动补全支持更好
  • 配置意图更清晰

多个 SecurityFilterChain

场景说明

在实际项目中,经常需要为不同的 URL 模式配置不同的安全策略。例如,API 端点使用无状态的 JWT 认证,而 Web 页面使用表单登录和 Session。

配置示例

@Configuration
@EnableWebSecurity
public class SecurityConfig {

// API 安全配置:无状态,使用 JWT
@Bean
@Order(1)
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());

return http.build();
}

// Web 安全配置:有状态,使用 Session
@Bean
@Order(2)
public SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/error").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
);

return http.build();
}
}

@Order 注解决定了哪个过滤器链先被评估。数字越小优先级越高。

测试安全配置

使用 MockMvc 测试

Spring Security 提供了测试支持,可以方便地测试安全配置:

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
class SecurityTests {

@Autowired
private MockMvc mockMvc;

@Test
void accessUnprotectedPage() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk());
}

@Test
void accessProtectedPageUnauthenticated() throws Exception {
mockMvc.perform(get("/admin"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("**/login"));
}

@Test
@WithMockUser(username = "user", roles = "USER")
void accessProtectedPageAsUser() throws Exception {
mockMvc.perform(get("/home"))
.andExpect(status().isOk());
}

@Test
@WithMockUser(username = "user", roles = "USER")
void accessAdminPageAsUserDenied() throws Exception {
mockMvc.perform(get("/admin"))
.andExpect(status().isForbidden());
}

@Test
@WithMockUser(username = "admin", roles = "ADMIN")
void accessAdminPageAsAdmin() throws Exception {
mockMvc.perform(get("/admin"))
.andExpect(status().isOk());
}

@Test
void loginWithValidCredentials() throws Exception {
mockMvc.perform(formLogin("/login")
.user("user")
.password("password"))
.andExpect(authenticated().withUsername("user"));
}
}

测试注解说明

  • @WithMockUser:模拟一个已认证用户
  • @WithAnonymousUser:模拟匿名用户
  • @WithUserDetails:使用 UserDetailsService 中配置的真实用户

常见问题排查

启用调试日志

application.yml 中添加:

logging:
level:
org.springframework.security: DEBUG

这会输出详细的过滤器链信息和认证过程日志。

常见错误

1. 403 Forbidden 错误

可能原因:

  • CSRF Token 缺失或无效
  • 用户权限不足
  • CORS 配置问题

排查方法:查看日志中的 AccessDeniedException 堆栈信息。

2. 重定向循环

可能原因:

  • 登录页面被配置为需要认证
  • 安全配置和控制器配置冲突

排查方法:检查登录页面 URL 是否在 permitAll() 中。

3. Session 失效

可能原因:

  • 并发登录限制
  • Session 超时配置
  • 集群环境 Session 不同步

排查方法:检查 Session 管理配置。

小结

本章介绍了 Spring Security 的基本配置方法:

  • 添加依赖后 Spring Security 自动提供基本安全保护
  • 通过 SecurityFilterChain Bean 配置安全规则
  • 使用 UserDetailsService 配置用户信息
  • 密码编码器保护用户密码安全
  • 多个 SecurityFilterChain 支持不同安全策略
  • 测试支持帮助验证安全配置

接下来,我们将深入学习认证机制的详细配置,包括数据库用户存储、自定义认证逻辑等高级主题。