Spring Security:认证、授权与过滤器链
认识 Spring Security:保护你的应用
在构建 Web 应用时,安全是永远绕不开的话题。Spring Security 是 Spring 家族中负责认证(Authentication)与授权(Authorization)的框架,同时完美融入了 Servlet 过滤器链(Filter Chain)机制。它可以帮助你轻松实现“谁可以访问”和“能访问什么”的控制。
本教程将从零开始,带你理解 Spring Security 的核心概念,并逐步搭建一个带有安全防护的 Spring Boot 应用。
环境准备与第一个安全项目
快速起步:引入依赖
创建一个标准的 Spring Boot + Spring Web 项目,并加入 Spring Security 起步依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
如果你使用的是 Spring Initializr,直接勾选 Spring Security 即可。
启动应用后,Spring Security 会自动配置一套默认的安全规则:所有请求都需要认证,并为你生成一个随机密码(控制台会打印 Using generated security password: ...),默认用户名为 user。访问任何 URL 都会跳转到默认的登录页面。
核心架构初探:过滤器链
在 Servlet 容器中,一个请求会经过一系列过滤器(Filter)的处理,然后才到达 Servlet。Spring Security 正是通过过滤器链来介入安全控制的。
最重要的一个过滤器是 FilterChainProxy,它内部维护了一个由多个安全过滤器组成的链条。这些过滤器各自负责不同的职责,例如:
- SecurityContextPersistenceFilter:负责在请求之间持久化安全上下文。
- UsernamePasswordAuthenticationFilter:处理表单登录的认证请求。
- ExceptionTranslationFilter:将安全相关异常转换为合适的 HTTP 响应(如重定向到登录页)。
- FilterSecurityInterceptor:执行最终的授权判断,决定是否允许访问。
你可以这样理解:过滤器链就是一道道安检门,只有持有有效“门票”(认证信息)且符合“入场规则”(授权)的请求才能到达最终目的地。
深入认证(Authentication)
认证要回答的核心问题是:“你是谁?”
认证的基本流程
- 用户提供凭证(用户名 / 密码)。
- 系统将凭证封装为
Authentication对象(通常未认证)。 AuthenticationManager委托给一个或多个AuthenticationProvider进行实际校验。- 校验通过,返回一个已认证的
Authentication对象(包含权限信息),存储到SecurityContextHolder中。
在 Spring Security 中,SecurityContextHolder 默认使用 ThreadLocal 存储,因此在同一线程的任何地方都可以获取当前用户信息。
示例:基于内存的认证配置
为了快速验证,我们可以使用基于内存的用户存储。新建一个配置类:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public UserDetailsService users() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("alice")
.password("12345")
.roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("bob")
.password("admin123")
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
注意:
withDefaultPasswordEncoder()仅适合演示或测试,生产环境必须使用BCryptPasswordEncoder等方式加密密码。
现在重启应用,就可以使用 alice 和 bob 的账号登录了。
获取当前登录用户
在控制器中,可以通过多种方式获取当前用户:
@GetMapping("/me")
public String currentUser(Authentication authentication) {
return "当前用户: " + authentication.getName();
}
// 或者使用 @AuthenticationPrincipal 注解
@GetMapping("/me2")
public String currentUser(@AuthenticationPrincipal UserDetails userDetails) {
return "当前用户: " + userDetails.getUsername();
}
核心授权(Authorization)
授权回答了问题:“你能做什么?”
角色与权限的声明
在基于内存的配置中,我们通过 .roles("USER") 定义了角色。Spring Security 中的角色通常以 ROLE_ 为前缀,因此 roles("USER") 实际存储的权限是 ROLE_USER。
授权可以基于角色,也可以基于更细粒度的权限(如 READ, WRITE)。使用 .authorities("READ_PROFILE") 可以设置自定义权限字符串。
示例:基于 URL 的访问控制
修改 SecurityConfig,添加授权规则:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/public/**").permitAll() // 公开访问
.requestMatchers("/user/**").hasRole("USER") // 需要 USER 角色
.requestMatchers("/admin/**").hasRole("ADMIN") // 需要 ADMIN 角色
.anyRequest().authenticated() // 其他都需要认证
)
.formLogin(Customizer.withDefaults()) // 开启表单登录
.logout(Customizer.withDefaults()); // 开启登出
return http.build();
}
- permitAll():完全开放。
- hasRole():检查
ROLE_前缀的角色。 - authenticated():只需要认证,不检查角色。
现在访问 /admin 时,如果登录用户是 alice(只有 USER 角色)会被拒绝(通常是 403 拒绝访问);而 bob 可以正常访问。
方法级安全:@PreAuthorize
除了基于 URL 的规则,Spring Security 还支持在方法上直接声明安全约束。首先,在配置类上添加 @EnableMethodSecurity 注解:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
// ...
}
然后在服务层或控制器方法上使用注解:
@RestController
public class AdminController {
@GetMapping("/admin/dashboard")
@PreAuthorize("hasRole('ADMIN')")
public String adminDashboard() {
return "管理后台仪表盘";
}
@GetMapping("/user/profile")
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public String userProfile() {
return "用户个人信息";
}
}
该方法级安全非常灵活,支持 Spring EL 表达式,可访问方法的参数等。
密码加密与用户存储进阶
BCryptPasswordEncoder
在生产环境中,密码必须加密存储。Spring Security 推荐使用 BCryptPasswordEncoder:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
然后在创建用户时使用该编码器加密密码。例如:
@Bean
public UserDetailsService users(PasswordEncoder encoder) {
UserDetails user = User.builder()
.username("alice")
.password(encoder.encode("12345"))
.roles("USER")
.build();
// ...
}
数据库用户认证
真实的应用通常从数据库加载用户。你需要实现 UserDetailsService 接口,并注入你的数据源。Spring Security 会自动使用它来匹配用户。
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserEntity entity = userRepository.findByUsername(username);
if (entity == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 将实体转换为 Spring Security 的 UserDetails
return org.springframework.security.core.userdetails.User
.withUsername(entity.getUsername())
.password(entity.getPassword())
.roles(entity.getRoles().split(","))
.build();
}
}
然后配置你的 AuthenticationProvider 或直接暴露该 UserDetailsService 即可。
自定义过滤器:深入过滤器链
有时默认的过滤器链不能满足需求,我们可以添加自己的过滤器。比如在请求中提取自定义 Token 并完成认证。
创建自定义过滤器
public class MyApiKeyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && apiKey.equals("my-secret-key")) {
// 构建认证对象,并设置为已认证
Authentication auth = new UsernamePasswordAuthenticationToken(
"api-user", null, List.of(new SimpleGrantedAuthority("ROLE_USER"))
);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response); // 一定要继续调用链
}
}
将过滤器插入到指定位置
在 SecurityFilterChain 配置中,使用 addFilterBefore 或 addFilterAfter 将自定义过滤器注册到默认的过滤器链中:
http.addFilterBefore(new MyApiKeyFilter(), UsernamePasswordAuthenticationFilter.class);
这样 MyApiKeyFilter 就会在表单登录过滤器之前执行。
总结
Spring Security 的强大之处在于它对过滤器链、认证和授权进行了高度抽象,同时又保持了极大的灵活性。本教程覆盖了:
- 过滤器链核心架构
- 认证流程与内存用户
- 基于 URL 和方法级的授权控制
- 密码加密与用户服务扩展
- 自定义过滤器
掌握这些内容后,你就能够构建出安全性较高的 Spring Boot 应用。下一步建议深入学习 OAuth2、JWT 以及 CSRF/CORS 等高级主题,进一步完善你的安全知识体系。