快速开始
本章将帮助你在 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();
}
}
这个配置做了以下几件事:
- 设置 URL 访问权限:首页和静态资源公开访问,管理页面需要 ADMIN 角色,其他页面需要认证
- 配置表单登录:使用自定义登录页面
- 允许用户登出
- 启用 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 自动提供基本安全保护
- 通过
SecurityFilterChainBean 配置安全规则 - 使用
UserDetailsService配置用户信息 - 密码编码器保护用户密码安全
- 多个
SecurityFilterChain支持不同安全策略 - 测试支持帮助验证安全配置
接下来,我们将深入学习认证机制的详细配置,包括数据库用户存储、自定义认证逻辑等高级主题。