文章目录
- 引言
- [第一部分:Spring Security 核心概念与架构](#第一部分:Spring Security 核心概念与架构)
-
- [1.1 身份认证 (Authentication) 与授权 (Authorization)](#1.1 身份认证 (Authentication) 与授权 (Authorization))
- [1.2 核心组件深度解析](#1.2 核心组件深度解析)
- [1.3 基于 Servlet 过滤链 (Filter Chain) 的架构](#1.3 基于 Servlet 过滤链 (Filter Chain) 的架构)
- [第二部分:现代 Spring Boot 应用中的安全配置演进](#第二部分:现代 Spring Boot 应用中的安全配置演进)
-
- [2.1 从 WebSecurityConfigurerAdapter 到 SecurityFilterChain 的演进](#2.1 从 WebSecurityConfigurerAdapter 到 SecurityFilterChain 的演进)
- 第三部分:主流认证与授权方案实现
-
- [3.1 基于 JWT 的认证与授权](#3.1 基于 JWT 的认证与授权)
- [3.2 OAuth2 登录与资源服务器配置](#3.2 OAuth2 登录与资源服务器配置)
- 第四部分:高级安全特性与细粒度控制
-
- [4.1 方法级别安全:@PreAuthorize 与 @PostAuthorize](#4.1 方法级别安全:@PreAuthorize 与 @PostAuthorize)
- [4.2 角色继承与权限分层 (RoleHierarchy)](#4.2 角色继承与权限分层 (RoleHierarchy))
- [4.3 密码管理:从 BCrypt 到 DelegatingPasswordEncoder](#4.3 密码管理:从 BCrypt 到 DelegatingPasswordEncoder)
- [4.4 CSRF 防护与 CORS 跨域配置](#4.4 CSRF 防护与 CORS 跨域配置)
- [4.5 保护 Actuator 端点](#4.5 保护 Actuator 端点)
- [4.6 WebSocket 安全](#4.6 WebSocket 安全)
引言
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。作为 Spring 生态系统的事实安全标准,它在保护基于 Spring Boot 构建的现代企业级 Java 应用中扮演着至关重要的角色。随着 Spring Boot 3 和 Spring Security 6 的发布,其配置方式、核心理念和最佳实践均发生了显著演进,向着更组件化、更灵活、更安全的方向发展。
第一部分:Spring Security 核心概念与架构
要精通 Spring Security,首先必须理解其底层的设计哲学和核心组件。它并非一个单一的黑盒,而是一套由多个协同工作的组件构成的精密系统。
1.1 身份认证 (Authentication) 与授权 (Authorization)
Spring Security 的两大核心功能是身份认证和授权。
- 身份认证 (Authentication) :此过程旨在验证"你是谁?"。当用户尝试访问受保护资源时,系统需要确认其身份的真实性。这通常通过用户名和密码、令牌 (Token)、生物特征等方式完成。
- 授权 (Authorization) :此过程发生在身份认证成功之后,旨在决定"你能做什么?"。即使用户身份合法,系统也需要根据其被授予的权限(如角色、操作许可),来判断其是否有权访问特定的资源或执行某个操作。
1.2 核心组件深度解析
Spring Security 的功能由一系列核心对象协作完成,它们共同构成了安全管理的骨架。
- SecurityContextHolder: 这是 Spring Security 的核心入口。它内部持有一个 SecurityContext 对象,而 SecurityContext 则包含了当前用户的 Authentication 信息。默认情况下,SecurityContextHolder 使用 ThreadLocal 策略来存储安全上下文,从而确保在同一线程内的任何位置都能获取到当前用户的认证信息。
- Authentication: 这是一个接口,代表了一次认证请求或一个已认证的用户主体 (Principal)。它包含了用户凭证 (Credentials)、主体信息 (Principal) 以及授予的权限 (Authorities)。
- AuthenticationManager: 它是认证流程的入口点,负责处理认证请求。它本身不执行具体的认证逻辑,而是将请求委托给一个或多个配置好的 AuthenticationProvider。
- AuthenticationProvider: 这是执行具体认证逻辑的组件。例如,DaoAuthenticationProvider 会与 UserDetailsService 和 PasswordEncoder 协作,通过比对数据库中的用户信息和用户提交的凭证来完成认证。
- UserDetailsService: 此接口的核心职责是根据用户名加载用户的数据。它的实现类通常会连接数据库、LDAP 或其他用户存储,返回一个 UserDetails 对象。
- UserDetails: 此接口封装了用户的核心信息,包括用户名、密码、账户是否启用、是否锁定以及最重要的------用户所拥有的权限集合 (GrantedAuthority)。
- GrantedAuthority: 此接口代表了授予给用户的权限。最常见的实现是 SimpleGrantedAuthority,它通常封装一个代表角色或权限的字符串(如 ROLE_ADMIN 或 READ_PRIVILEGE)。
- PasswordEncoder: 用于密码的单向加密和匹配。Spring Security 强烈建议不要以明文形式存储密码。BCryptPasswordEncoder 是当前广泛使用的强大加密器。
1.3 基于 Servlet 过滤链 (Filter Chain) 的架构
Spring Security 的 Web 安全功能是构建在一系列 Servlet 过滤器之上的。当一个 HTTP 请求到达应用时,它会首先穿过一个由 Spring Security 管理的过滤器链。每个过滤器都承担着特定的安全职责,例如:
- UsernamePasswordAuthenticationFilter: 处理基于表单提交的用户名和密码认证。
- BasicAuthenticationFilter: 处理 HTTP Basic 认证。
- CsrfFilter: 提供跨站请求伪造 (CSRF) 防护。
- BearerTokenAuthenticationFilter: 处理基于 JWT 或其他不透明令牌的 Bearer Token 认证。
- FilterSecurityInterceptor: 位于过滤链的末端,根据 Authentication 对象和请求的资源,利用 AccessDecisionManager 做出最终的授权决策。
在 Spring Boot 应用中,这个过滤器链被注册为一个名为 springSecurityFilterChain 的 Bean,它由 Spring Boot 根据你的配置自动创建和管理。
第二部分:现代 Spring Boot 应用中的安全配置演进
随着 Spring Security 版本的迭代,其配置方式也从传统的继承类模式转向了更现代、更灵活的组件化配置模式。
2.1 从 WebSecurityConfigurerAdapter 到 SecurityFilterChain 的演进
在 Spring Security 5.7 之前,自定义安全配置的通用做法是继承 WebSecurityConfigurerAdapter 类并重写其 configure(HttpSecurity http) 方法。然而,这种方式存在一些局限,例如配置耦合度高、难以实现多个独立的、有条件的配置链。
为了推动更模块化和组件化的配置,WebSecurityConfigurerAdapter 在 Spring Security 5.7.0-M2 中被标记为已弃用,并在 Spring Security 6.0 中被完全移除。
新的推荐方式是直接在 @Configuration 配置类中定义一个或多个 SecurityFilterChain 类型的 @Bean 。这种转变的好处是:
- 解耦:每个 SecurityFilterChain Bean 都是一个独立的、自包含的安全配置单元。
- 灵活性:可以根据不同的请求路径(如 https://files.metaso.cn/api/\*\* 和 /admin/**)定义不同的安全过滤链,并使用 @Order 注解来控制它们的优先级。
- 清晰性:配置逻辑更加清晰,不再隐藏于父类的方法重写中。
迁移示例:
旧方式 (继承 WebSecurityConfigurerAdapter)
java
// @Configuration
// @EnableWebSecurity
public class OldSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
}
新方式 (定义 SecurityFilterChain Bean)
java
@Configuration
@EnableWebSecurity
public class NewSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults()); // 使用 lambda DSL 和默认配置器
return http.build();
}
}
值得注意的是,一些配置方法的名称也发生了变化,例如 authorizeRequests() 被 authorizeHttpRequests() 替代,并且新的配置大量采用 Lambda DSL 风格,使代码更加简洁易读。
第三部分:主流认证与授权方案实现
在现代 Web 应用中,基于 Token 的无状态认证和 OAuth2 是最主流的安全方案。
3.1 基于 JWT 的认证与授权
JSON Web Token (JWT) 是一种紧凑且自包含的令牌格式,非常适合用于分布式、无状态的认证场景。在 Spring Security 中集成 JWT 认证通常涉及以下步骤:
-
添加依赖:引入 spring-boot-starter-security 和 JWT 相关的库(如 io.jsonwebtoken:jjwt-api, jjwt-impl, jjwt-jackson)。
-
创建 JWT 工具类:编写一个 JwtUtil 或类似的服务,负责生成、解析和验证 JWT。该工具类会处理签名、过期时间判断、从 Token 中提取声明 (claims) 等逻辑。
-
配置 SecurityFilterChain:
- 禁用 CSRF:由于 JWT 认证是无状态的,且通常用于 API,因此可以禁用 CSRF 防护:http.csrf(csrf -> csrf.disable()) 。
- 禁用 Session:将 Session 管理策略设置为无状态 STATELESS,因为服务器不再需要维护用户会话:http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) 。
- 配置授权规则:定义哪些端点是公开的(如登录 /auth/login、注册 /auth/register),哪些需要认证。
-
实现自定义认证过滤器:创建一个继承自 OncePerRequestFilter 的自定义过滤器(例如 JwtAuthenticationFilter)。此过滤器的核心逻辑是:
- 从请求头 (Authorization: Bearer ) 中提取 JWT。
- 使用 JwtUtil 验证 Token 的有效性。
- 如果 Token 有效,从中解析出用户信息(如用户名、权限)。
- 根据用户信息创建一个 UsernamePasswordAuthenticationToken 对象。
- 将此 Authentication 对象设置到 SecurityContextHolder 中:SecurityContextHolder.getContext().setAuthentication(authentication)。
- 最后,将该自定义过滤器添加到 Spring Security 的过滤器链中,通常是在 UsernamePasswordAuthenticationFilter 之前。
-
提供登录接口:创建一个 Controller 端点用于用户登录。该接口接收用户名和密码,通过 AuthenticationManager 进行认证,认证成功后使用 JwtUtil 生成 JWT 并返回给客户端。
3.2 OAuth2 登录与资源服务器配置
OAuth2 是一个行业标准的授权框架,允许第三方应用在用户授权下访问其在另一服务上的资源。Spring Security 对 OAuth2 提供了全面的支持,主要分为两大场景:OAuth2 客户端和资源服务器。
作为 OAuth2 客户端(例如,实现"使用 Google/GitHub 登录"):
- 添加依赖:引入 spring-boot-starter-oauth2-client。
- 配置 application.yml:配置认证提供商(如 Google, GitHub)的 client-id、client-secret、redirect-uri、scope 等信息。
- 配置 SecurityFilterChain:在安全配置中启用 OAuth2 登录:http.oauth2Login(Customizer.withDefaults())。Spring Boot 会根据你的配置自动处理重定向到认证服务器、处理回调、获取令牌和用户信息等流程。
- 自定义用户映射:有时需要将从 OAuth2 提供商获取的用户信息映射到应用内部的用户模型,或在首次登录时自动注册用户。这可以通过自定义 OAuth2UserService 来实现。
作为资源服务器(保护你的 API):
- 添加依赖:引入 spring-boot-starter-oauth2-resource-server。
- 配置 application.yml:配置 JWT 签发者的 URI (spring.security.oauth2.resourceserver.jwt.issuer-uri)。Spring Security 会自动从该 URI 下载 JWK Set(公钥集)用于验证 JWT 签名。
- 配置 SecurityFilterChain :
- 启用资源服务器支持:http.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) 。
- 配置授权规则,例如要求访问者拥有特定的范围 (scope) 或角色。
通过这些配置,当一个带有 Bearer 令牌的请求访问你的 API 时,Spring Security 会自动验证令牌的签名、过期时间和签发者,并从令牌中提取权限信息用于授权决策。
第四部分:高级安全特性与细粒度控制
除了基础的认证和授权,Spring Security 还提供了一系列高级功能,以满足复杂的业务需求。
4.1 方法级别安全:@PreAuthorize 与 @PostAuthorize
除了通过 HttpSecurity 配置 URL 级别的访问控制,Spring Security 还支持在方法级别进行更细粒度的授权。这对于保护 Service 层的业务方法非常有用。
-
启用方法安全:在 Spring Boot 3 / Spring Security 6 中,需要在你的配置类上添加 @EnableMethodSecurity 注解。在旧版本中,使用的是 @EnableGlobalMethodSecurity(prePostEnabled = true) 。
-
使用注解:
- @PreAuthorize: 在方法执行前进行权限检查。它支持强大的 Spring 表达式语言 (SpEL),可以实现非常复杂的逻辑。
java// 只有 ADMIN 角色或拥有 'user:write' 权限的用户才能调用 @PreAuthorize("hasRole('ADMIN') or hasAuthority('user:write')") public void updateUser(User user) { ... } // 只有当传入的 username 与当前登录用户的用户名相同时才能调用 @PreAuthorize("#username == authentication.principal.username") public UserDetails findUserByUsername(String username) { ... }- @PostAuthorize: 在方法执行后进行权限检查。这通常用于基于方法返回值进行决策。
java// 只有当返回的订单属于当前用户时,才允许方法成功返回 @PostAuthorize("returnObject.owner == authentication.principal.username") public Order findOrderById(Long id) { ... }
4.2 角色继承与权限分层 (RoleHierarchy)
在复杂的应用中,角色之间往往存在继承关系,例如,ROLE_ADMIN 应该拥有 ROLE_USER 的所有权限。手动为 ADMIN 添加所有 USER 的权限非常繁琐且难以维护。Spring Security 提供了 RoleHierarchy 来解决这个问题。
-
定义 RoleHierarchy Bean:在配置类中创建一个 RoleHierarchy Bean,并定义角色层级。符号 > 表示继承关系。
java@Bean public RoleHierarchy roleHierarchy() { RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); // 定义 DBA > ADMIN > USER String hierarchy = "ROLE_DBA > ROLE_ADMIN \n ROLE_ADMIN > ROLE_USER"; roleHierarchy.setHierarchy(hierarchy); return roleHierarchy; } -
集成到表达式处理器:为了让 @PreAuthorize 等方法安全注解能够识别这种继承关系,需要将 RoleHierarchy 注入到 MethodSecurityExpressionHandler 中。
java@Bean public MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) { DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); expressionHandler.setRoleHierarchy(roleHierarchy); return expressionHandler; }
配置完成后,一个拥有 ROLE_ADMIN 角色的用户将自动被认为也拥有 ROLE_USER 角色。因此,当他访问一个标注了 @PreAuthorize("hasRole('USER')") 的方法时,访问将被允许。
4.3 密码管理:从 BCrypt 到 DelegatingPasswordEncoder
Spring Security 5.0 之后,推荐使用 DelegatingPasswordEncoder 作为默认的密码编码器。它不是一种新的加密算法,而是一个代理,可以支持多种加密算法,并根据存储的密码前缀(如 {bcrypt}、{sha256})来选择合适的编码器进行匹配。
好处:
- 平滑迁移:当需要升级密码加密算法时(例如从 MD5 升级到 Bcrypt),可以无缝地支持新旧两种格式的密码。用户使用旧密码登录时,系统验证通过后,可以将其密码重新编码为新格式再存储。
- 明确性:密码存储格式 {id}encodedPassword 清晰地表明了使用了哪种算法。
配置:推荐使用 PasswordEncoderFactories 来创建实例。
java
@Bean
public PasswordEncoder passwordEncoder() {
// 默认使用 Bcrypt,并支持多种其他算法
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
当你调用 passwordEncoder.encode("password") 时,它会返回一个类似 {bcrypt}$2a 10 10 10dXJ3SW6G7P50lGmMkkmwe.20cQQPBEa7jsNN7k2AU.AetU... 的字符串。在验证时,DelegatingPasswordEncoder 会解析 {bcrypt} 前缀,并将真正的验证工作委托给内部的 BCryptPasswordEncoder。
4.4 CSRF 防护与 CORS 跨域配置
-
CSRF (Cross-Site Request Forgery): Spring Security 默认对所有会改变状态的请求(POST, PUT, DELETE 等)启用 CSRF 防护 。它通过在表单或请求头中加入一个同步令牌来实现。对于无状态的 REST API,通常会禁用 CSRF (http.csrf(c -> c.disable()))。如果需要细粒度控制,可以使用 csrf.ignoringRequestMatchers("https://files.metaso.cn/api/public/\*\*") 来忽略特定路径。
-
CORS (Cross-Origin Resource Sharing): 当你的前端应用和后端 API 部署在不同源时,需要进行 CORS 配置。Spring Security 提供了强大的 CORS 支持。最佳实践是在 SecurityFilterChain 中通过 .cors() 启用,并提供一个 CorsConfigurationSource Bean。
java@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of("https://your-frontend.com")); // 严格指定允许的源 configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(List.of("Authorization", "Content-Type")); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); // 对所有路径应用此配置 return source; }
在 SecurityFilterChain 中启用它:http.cors(c -> c.configurationSource(corsConfigurationSource()))。
4.5 保护 Actuator 端点
Spring Boot Actuator 提供了许多有用的生产就绪端点(如 /health, /metrics, /env)。当引入 Spring Security 后,这些端点默认会受到保护。
为了进行细粒度控制,Spring Security 提供了 EndpointRequest 工具类,可以方便地匹配 Actuator 端点。
示例:
- 在 application.properties 中,通过 management.endpoints.web.exposure.include 显式暴露你需要的端点,而不是全部暴露。
- 在 SecurityFilterChain 中,使用 EndpointRequest 配置访问规则。例如,允许匿名访问健康检查端点,但其他端点需要特定角色。
java
// 在 authorizeHttpRequests 中配置
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ACTUATOR_ADMIN")
4.6 WebSocket 安全
使用 Spring Security 保护基于 STOMP 的 WebSocket 通信需要额外的配置。
-
启用 WebSocket 消息代理:通过 @EnableWebSocketMessageBroker 开启 WebSocket 功能。
-
配置入站消息安全:Spring Security 提供了专门的 SecuritySocketAccessDecider 来保护 WebSocket 消息。你需要在安全配置中重写 configureMessageBroker(MessageSecurityMetadataSourceRegistry messages) 方法。
java
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
// 发送到 /app/** 的消息需要认证
.simpDestMatchers("/app/**").authenticated()
// 发送到 /user/** 的消息需要认证
.simpDestMatchers("/user/**").authenticated()
// 其他任何消息都需要认证
.anyMessage().authenticated();
}
// 必须禁用同源策略,否则 WebSocket 连接可能失败
@Override
protected boolean sameOriginDisabled() {
return true;
}
}
- 身份验证:WebSocket 的身份验证通常发生在 HTTP 握手阶段。一旦用户通过常规的 HTTP 方式(如表单登录、JWT)认证,其 Authentication 对象就会被关联到 WebSocket 会话中,后续的 STOMP 消息将携带此身份信息。