Article
第一章:Spring Security 入门基础
1.1 什么是 Spring Security(概念)
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| Spring Security | Spring 生态中用于为 Java 应用提供全面安全服务的框架,支持认证(Authentication)和授权(Authorization)功能,适用于基于 Spring 的 Web 应用和方法级安全控制。 | 是 Spring 官方项目,与 Spring Boot、Spring MVC 深度集成;不仅限于 Web 层,也可用于方法调用、域对象安全等场景 |
| 认证(Authentication) | 确认用户身份的过程,即”你是谁”。例如通过用户名密码登录系统。 | 是访问受保护资源的前提步骤;成功后会生成一个包含用户信息的 Authentication 对象 |
| 授权(Authorization) | 在用户身份确认后,判断其是否有权限执行某项操作或访问某个资源,即”你能做什么”。 | 基于角色(Role)或权限(Authority)进行控制;可在 URL、方法、视图等多个层级进行配置 |
| 过滤器链(Filter Chain) | Spring Security 通过一组 Servlet Filter 构成的安全过滤器链来拦截请求并执行安全逻辑。 | 默认包含十几种过滤器,如认证、授权、CSRF 防护等;请求必须通过整个链才能访问目标资源 |
1.2 Spring Security 的核心功能与优势
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| 统一安全模型 | 提供一致的认证与授权机制,支持多种安全协议和模式。 | 核心接口如 AuthenticationManager、UserDetailsService 具有高度可扩展性;支持自定义实现以适配不同业务需求 |
| Web 请求安全 | 可对 HTTP 请求路径进行细粒度访问控制,支持基于表达式的权限判断。 | 使用 HttpSecurity DSL 进行配置;支持 antMatcher、mvcMatcher 等多种匹配方式 |
| 方法级安全 | 支持在服务层方法上添加安全注解(如 @PreAuthorize),实现更精细的权限控制。 | 需启用 @EnableMethodSecurity 或 @EnableGlobalMethodSecurity;适用于复杂业务逻辑中的权限校验 |
| 防御常见攻击 | 内置对 CSRF、CORS、Session Fixation、Clickjacking 等常见安全威胁的防护机制。 | 多数防护默认开启(如 CSRF 在表单登录时自动启用);可根据需要显式关闭或调整策略 |
| 多种认证方式支持 | 支持表单登录、HTTP Basic、OAuth2、JWT、LDAP、Remember-Me 等多种认证方式。 | 可组合使用多种认证机制;扩展性强,支持自定义 AuthenticationProvider |
| 与 Spring 生态无缝集成 | 与 Spring Boot、Spring MVC、Spring Data、Spring Cloud 等组件天然兼容。 | 使用 starter 可快速引入并自动配置;支持条件化配置(@ConditionalOnMissingBean) |
1.3 安全框架对比:Spring Security vs Shiro
| 对比维度 | Spring Security | Apache Shiro |
|---|---|---|
| 所属生态 | Spring 官方项目,深度集成 Spring/Spring Boot | 独立框架,不依赖任何特定生态 |
| 学习曲线 | 较陡峭,概念抽象,配置复杂 | 相对简单直观,API 易懂 |
| 功能丰富性 | 功能极其全面,支持 OAuth2、JWT、SAML、LDAP、OpenID Connect 等 | 核心功能完整,但高级协议支持较弱 |
| 方法级安全 | 原生支持 @PreAuthorize 等 SpEL 表达式注解 | 支持注解,但表达式能力有限 |
| 过滤器机制 | 基于 Servlet Filter,与 Spring Web 深度绑定 | 同样基于 Filter,但更轻量,可用于非 Spring 环境 |
| 配置方式 | Java Config / Lambda DSL / XML,推荐使用 Java 配置 | Ini 文件 / Java API,灵活性高 |
| 社区与维护 | 活跃,持续更新,紧跟 Spring 发展 | 社区相对较小,更新频率较低 |
| 适用场景 | Spring/Spring Boot 项目,尤其是微服务架构 | 老旧系统、非 Spring 项目、小型应用 |
1.4 环境搭建与基本依赖配置(Maven/Gradle)
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| spring-boot-starter-security | Maven: spring-boot-starter-security / Gradle: implementation 'org.springframework.boot:spring-boot-starter-security' | 引入 Spring Security 核心模块,自动配置默认安全行为 | 见下方代码块 | 使用 Spring Boot 时只需此依赖即可启用安全功能;自动保护所有端点,要求用户登录 |
| spring-security-web | <groupId>org.springframework.security</groupId><artifactId>spring-security-web</artifactId> | 提供 Web 层安全支持,如 Filter、SecurityContextPersistenceFilter 等 | 见下方代码块 | 非 Spring Boot 项目需手动引入;包含核心 Web 安全过滤器 |
| spring-security-config | <groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId> | 提供安全配置支持,如 @EnableWebSecurity、Java Config DSL | 见下方代码块 | 必须引入才能使用 Java 配置类;包含 AuthenticationManagerBuilder 等配置工具 |
Maven 依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<!-- 非 Spring Boot 项目需额外引入 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>6.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>6.2.0</version>
</dependency>
Gradle 依赖:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
}
1.5 第一个 Spring Security 应用:默认安全配置体验
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| @EnableWebSecurity | @EnableWebSecurity | 启用 Spring Security 的 Web 安全配置,注册默认过滤器链 | 见下方代码块 | 在 Spring Boot 中通常不需要显式添加,因为自动配置已包含;若自定义配置类,建议使用 |
| SecurityFilterChain Bean | @Bean SecurityFilterChain filterChain(HttpSecurity http) | 定义请求过滤规则和安全行为,替代旧版 WebSecurityConfigurerAdapter | 见下方代码块 | Spring Security 6 推荐方式;必须定义至少一个 SecurityFilterChain Bean 来避免默认锁定 |
| 默认用户名 | user | 当未配置用户时,Spring Boot 自动生成一个随机密码并输出到控制台,用户名固定为 “user” | 控制台输出:Using generated security password: 8a2d7b1e-5f3c-4d8a-b9c0-... | 仅用于开发测试;生产环境必须自定义用户管理 |
| 默认登录页 | /login | Spring Security 自动生成的登录页面路径 | 访问 /hello(受保护资源)→ 跳转至 /login | 页面由框架内置生成;可通过 formLogin().loginPage("/custom-login") 自定义 |
| 默认登出 | /logout | 默认登出端点,发送 POST 请求即可退出登录 | 发送 POST 请求到 /logout | 必须是 POST 请求(防止 CSRF 滥用);可通过 logout().logoutUrl("/sign-out") 修改 |
| 自动生成密码 | N/A | 应用启动时,若无用户配置,控制台打印随机生成的临时密码 | Using generated security password: abcdef123456 | 每次重启都会变化;仅适用于开发环境 |
基本安全配置示例(Spring Security 6 推荐方式):
@Configuration
@EnableWebSecurity
public class DefaultSecurityConfig {
}
@Configuration
@EnableWebSecurity
public class BasicSecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}
}
第二章:认证(Authentication)机制详解
2.1 认证流程核心组件:AuthenticationManager、ProviderManager、AuthenticationProvider
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| AuthenticationManager | Authentication authenticate(Authentication authentication) throws AuthenticationException | 认证管理顶层接口,负责接收认证请求并返回认证结果 | 见下方代码块 | 是认证流程的入口;实际常用实现是 ProviderManager |
| ProviderManager | public class ProviderManager implements AuthenticationManager { ... } | AuthenticationManager 的默认实现,委托给多个 AuthenticationProvider 进行认证 | 默认由 Spring Security 自动配置,通常无需手动创建实例 | 支持多个 AuthenticationProvider,按顺序尝试;可设置 eraseCredentialsAfterAuthentication 控制凭证清除行为 |
| AuthenticationProvider | public interface AuthenticationProvider { ... } | 提供具体的认证逻辑实现,如用户名密码、LDAP、OAuth2 等 | 见下方代码块 | 必须实现 supports() 方法以声明支持的认证类型;若认证失败应抛出对应 AuthenticationException |
AuthenticationManager Bean 配置:
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.build();
}
自定义 AuthenticationProvider:
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
@Override
public Authentication authenticate(Authentication authentication) {
// 自定义认证逻辑
return new UsernamePasswordAuthenticationToken(...);
}
}
2.2 用户详情服务:UserDetailsService 与 InMemoryUserDetailsManager
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| UserDetailsService | UserDetails loadUserByUsername(String username) throws UsernameNotFoundException | 定义加载用户信息的标准接口,用于认证时获取用户凭证 | 见下方代码块 | 是认证流程中获取用户信息的关键接口;方法名易误导:实际可基于邮箱、手机号等非用户名字段查询 |
| InMemoryUserDetailsManager | public class InMemoryUserDetailsManager implements UserDetailsService, UserDetailsManager { ... } | 基于内存存储的 UserDetailsService 实现,用于测试或简单场景 | 见下方代码块 | 仅适用于开发/测试环境;密码明文存储不安全,生产环境禁用 |
自定义 UserDetailsService:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) {
// 从数据库加载用户
throw new UsernameNotFoundException("User not found");
}
}
InMemoryUserDetailsManager 配置:
@Bean
public UserDetailsService userDetailsService() {
User.UserBuilder users = User.withDefaultPasswordEncoder();
return new InMemoryUserDetailsManager(
users.username("user").password("password").roles("USER").build(),
users.username("admin").password("admin").roles("ADMIN").build()
);
}
2.3 自定义用户信息加载:实现 UserDetailsService 接口
loadUserByUsername 是加载用户详细信息的核心方法,被 AuthenticationProvider 调用。
实现示例(集成数据库):
@Service
public class DatabaseUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) {
UserEntity user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User
.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(user.getRoles().split(","))
.accountExpired(!user.isActive())
.credentialsExpired(false)
.disabled(!user.isActive())
.build();
}
}
注意事项: 必须处理用户不存在情况,抛出 UsernameNotFoundException;可集成 JPA、MyBatis 等持久层框架。
2.4 用户凭证模型:UserDetails 与 GrantedAuthority
| 名称 | 说明 | 代码示例 | 注意事项 |
|---|---|---|---|
| UserDetails | 表示用户的完整安全信息,包含权限、状态等 | 见下方代码块 | 所有 getter 方法都会被认证流程检查;任一状态为 false 将导致认证失败 |
| GrantedAuthority | 表示用户所拥有的权限或角色,通常以字符串形式返回(如 “ROLE_ADMIN”) | SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_ADMIN"); | 角色建议以 “ROLE_” 开头;权限可自定义命名,如 “user:read”、“order:write” |
| User (builder) | Spring Security 提供的 UserDetails 实现类,支持链式构建 | 见下方代码块 | 生产环境可自定义实现 UserDetails;builder() 方式更安全(自动编码密码) |
UserDetails 接口定义:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
使用 Builder 构建 User 实例:
User.builder()
.username("john")
.password("$2a$10$...")
.authorities("ROLE_USER", "read")
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(false)
.build();
2.5 密码编码器:PasswordEncoder 接口与常用实现
| 名称 | 说明 | 代码示例 | 注意事项 |
|---|---|---|---|
| PasswordEncoder | 定义密码编码与验证的标准接口 | 见下方代码块 | 必须注册为 Bean 供框架使用;不应使用 NoOpPasswordEncoder(明文存储) |
| BCryptPasswordEncoder | 基于 BCrypt 算法的强哈希编码器,推荐使用 | 见下方代码块 | 每次编码结果不同(因盐值随机);支持 strength 参数控制计算强度(默认 10) |
| NoOpPasswordEncoder | 明文编码器,不进行任何加密处理 | NoOpPasswordEncoder.getInstance() | 已废弃,仅用于测试;生产环境绝对禁止使用 |
| DelegatingPasswordEncoder | 支持多种编码算法的代理编码器,能识别前缀(如 {bcrypt}、{noop})自动选择策略 | 自动配置 | Spring Security 5+ 默认使用;提升迁移和兼容性 |
PasswordEncoder Bean 配置:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
使用示例:
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encoded = encoder.encode("password");
boolean matches = encoder.matches("password", encoded);
2.6 认证异常处理:AuthenticationException 及其子类
| 名称 | 说明 | 注意事项 |
|---|---|---|
| AuthenticationException | 认证失败的顶层异常,所有认证相关异常的父类 | 必须在认证流程中捕获处理;可用于自定义异常响应 |
| BadCredentialsException | 用户名或密码错误 | 最常见的认证异常;建议统一处理避免暴露具体错误 |
| UsernameNotFoundException | 用户名不存在 | 由 UserDetailsService 抛出;实际应返回通用错误防止用户名探测 |
| AccountExpiredException | 用户账户已过期 | 由 UserDetails.isAccountNonExpired() 返回 false 触发;可引导用户联系管理员 |
| LockedException | 用户账户被锁定 | 多次失败后可手动或自动锁定;可结合失败次数计数器实现 |
| CredentialsExpiredException | 用户凭证(密码)已过期,需更换 | 强制密码定期更换策略的实现基础;可跳转至修改密码页面 |
| DisabledException | 用户账户被禁用 | 用于软删除或临时停用用户;与锁定不同,禁用通常是长期状态 |
第三章:授权(Authorization)控制
3.1 基于角色与权限的访问控制:hasRole()、hasAuthority() 等表达式
| 名称 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|
hasRole() | 判断用户是否具有指定角色,自动添加 “ROLE_” 前缀 | hasRole('ADMIN') | 参数无需写 “ROLE_“;实际检查的是 “ROLE_ADMIN” |
hasAnyRole() | 判断用户是否具有任意一个指定角色 | hasAnyRole('USER', 'ADMIN') | 多角色用逗号分隔;满足其一即可访问 |
hasAuthority() | 判断用户是否具有指定权限(精确匹配) | hasAuthority('read') | 不自动添加前缀;区分大小写 |
hasAnyAuthority() | 判断用户是否具有任意一个指定权限 | hasAnyAuthority('read', 'write') | 权限列表可动态配置;常用于细粒度权限控制 |
permitAll() | 允许所有用户访问,无需认证 | permitAll() | 用于静态资源、登录页等公开路径 |
denyAll() | 拒绝所有用户访问 | denyAll() | 调试或临时关闭功能时使用 |
HttpSecurity 配置示例:
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/dashboard").hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/data").hasAuthority("data:read")
.requestMatchers("/api/edit").hasAnyAuthority("post:write", "comment:write")
.requestMatchers("/login", "/css/**").permitAll()
.requestMatchers("/private").denyAll()
.anyRequest().authenticated()
);
3.2 方法级安全注解:@PreAuthorize、@PostAuthorize、@Secured
| 名称 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|
@PreAuthorize | 在方法执行前进行权限检查 | @PreAuthorize("hasRole('ADMIN')") | 使用 SpEL 表达式;支持方法参数引用(如 #id) |
@PostAuthorize | 在方法执行后进行权限检查,可用于基于返回值的授权 | @PostAuthorize("returnObject.owner == authentication.name") | 性能开销大,因方法已执行;少用,主要用于数据过滤 |
@Secured | 基于角色的简单方法安全控制 | @Secured("ROLE_ADMIN") | 不支持 SpEL;角色必须带 “ROLE_” 前缀 |
@PreFilter | 在方法执行前对集合参数进行过滤 | @PreFilter("filterObject.owner == authentication.name") | filterObject 代表集合中每个元素;用于参数预处理 |
@PostFilter | 在方法执行后对返回的集合进行过滤 | @PostFilter("filterObject.owner == authentication.name") | filterObject 代表返回集合中每个元素;常用于多租户数据隔离 |
使用示例:
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) { ... }
@PostAuthorize("returnObject.user == authentication.principal")
public Document getDocument(Long id) { ... }
@Secured("ROLE_USER")
public void userOperation() { ... }
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void adminOrManager() { ... }
public void processOrders(@PreFilter("filterObject.status == 'ACTIVE'") Collection orders) { ... }
@PostFilter("hasRole('USER') or filterObject.public")
public List getAllDocuments() { ... }
}
3.3 启用方法安全:@EnableMethodSecurity 与 @EnableGlobalMethodSecurity
| 名称 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|
@EnableMethodSecurity | Spring Security 6+ 推荐的方式启用方法级安全 | @EnableMethodSecurity | 默认启用 @PreAuthorize、@PostAuthorize、@Secured;支持 JSR-250 注解(需设置 jsr250Enabled = true) |
@EnableGlobalMethodSecurity | 旧版方式(Spring Security 5.x),仍可用 | @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) | prePostEnabled: 启用 @PreAuthorize / @PostAuthorize;securedEnabled: 启用 @Secured;jsr250Enabled: 启用 @RolesAllowed |
配置示例:
// Spring Security 6+ 推荐
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig { }
// Spring Security 5.x 旧版
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig { }
3.4 安全表达式语言(SpEL)在授权中的应用
| 名称 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|
authentication | 获取当前 Authentication 对象 | @PreAuthorize("authentication.principal.username == #username") | 可访问 principal、credentials、authorities 等属性 |
principal | 获取当前用户主体(通常是 UserDetails) | @PreAuthorize("principal.username == #username") | 等价于 authentication.principal |
hasRole() | SpEL 内置表达式,检查角色 | @PreAuthorize("hasRole('ADMIN')") | 自动处理 “ROLE_” 前缀 |
hasAnyRole() | 检查是否具有任意一个角色 | @PreAuthorize("hasAnyRole('USER', 'ADMIN')") | 参数为字符串数组 |
hasAuthority() | 检查权限 | @PreAuthorize("hasAuthority('order:write')") | 精确匹配权限字符串 |
hasAnyAuthority() | 检查是否具有任意一个权限 | @PreAuthorize("hasAnyAuthority('data:read', 'data:write')") | 常用于细粒度控制 |
#parameter | 引用方法参数 | @PreAuthorize("#id == authentication.principal.id") | 参数名需与方法定义一致;支持复杂表达式 |
T(Class) | 调用静态方法或访问常量 | @PreAuthorize("hasRole(T(com.example.Role).ADMIN.name())") | 用于动态引用类常量 |
参数引用示例:
@PreAuthorize("#id == authentication.principal.id")
public User getUser(Long id) { ... }
3.5 匿名用户与未认证访问控制(概念)
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| 匿名认证(Anonymous Authentication) | Spring Security 会为未登录用户创建一个 AnonymousAuthenticationToken,使其在安全上下文中仍有一个 Authentication 对象 | 默认启用;角色为 ROLE_ANONYMOUS;便于统一处理认证/未认证逻辑 |
permitAll | 允许所有用户(包括匿名用户)访问该资源 | 常用于登录页、注册页、静态资源;配置在 HttpSecurity 中 |
isAnonymous() | SpEL 表达式,判断当前用户是否为匿名用户 | 可用于条件判断:hasRole('USER') or isAnonymous();与 permitAll 配合使用实现开放策略 |
| 未认证用户(Unauthenticated) | 指尚未通过任何认证流程的用户,但在 Spring Security 中会被自动赋予匿名身份 | 实际不存在”完全无身份”状态;所有请求都有 Authentication 对象 |
| 匿名用户限制 | 匿名用户通常权限极低,只能访问公开资源 | 不能访问需要 ROLE_USER 或更高权限的接口;可通过 remember-me 提升为持久化认证 |
第四章:Web 安全配置详解
4.1 HttpSecurity 配置结构与常用方法
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| HttpSecurity | HttpSecurity http | Spring Security Web 安全配置的核心 DSL 对象,用于构建安全规则链 | 见下方代码块 | 必须调用 build() 返回 SecurityFilterChain Bean;配置顺序不影响执行顺序(由过滤器链决定) |
| csrf() | http.csrf(Customizer) | 配置 CSRF(跨站请求伪造)防护策略 | http.csrf(csrf -> csrf.disable()); | 默认启用(表单登录时);前后端分离常设为 disable(配合 JWT) |
| authorizeHttpRequests | http.authorizeHttpRequests(Customizer) | 配置基于 HTTP 请求的访问控制规则(Spring Security 6 推荐) | 见下方代码块 | 替代旧版 authorizeRequests();必须至少配置一条规则 |
| formLogin | http.formLogin(Customizer) | 配置基于表单的登录机制 | 见下方代码块 | 默认生成 /login 页面;可自定义登录页、成功/失败处理 |
| logout | http.logout(Customizer) | 配置登出行为 | http.logout(logout -> logout.logoutUrl("/sign-out").permitAll()); | 默认 POST /logout;可清除 Cookie、Session、CSRF Token 等 |
基础配置示例:
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated())
.formLogin(form -> form
.loginPage("/login")
.permitAll());
return http.build();
}
CSRF 配置(启用状态下的 Cookie 存储):
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
4.2 URL 路径权限控制:authorizeHttpRequests / authorizeRequests
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| requestMatchers | requestMatchers(String...) | 匹配指定路径模式的请求 | 见下方代码块 | 支持 Ant 风格路径;多个模式可用可变参数 |
| mvcMatcher | mvcMatcher(String) | 基于 Spring MVC 的路径匹配,支持上下文路径 | http.authorizeHttpRequests(auth -> auth.mvcMatcher("/user/{id}").access(new WebExpressionAuthorizationManager("hasRole('USER')"))) | 更精确,考虑 DispatcherType;推荐用于 MVC 应用 |
| servletPath | servletPath(String) | 指定 Servlet 的路径前缀(如 /app) | 见下方代码块 | 影响所有后续匹配规则;与 contextPath 不同 |
| anyRequest | anyRequest() | 匹配所有未被前面规则覆盖的请求 | 见下方代码块 | 必须放在最后;用于兜底策略 |
完整路径控制示例:
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated()
.requestMatchers("/css/**", "/js/**").permitAll()
.anyRequest().denyAll()
);
servletPath 配置示例:
http.servletPath("/app")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/dashboard").authenticated());
// 实际匹配 /app/dashboard
4.3 表单登录配置:formLogin 与登录流程控制
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| loginPage | form.loginPage(String) | 指定自定义登录页面路径 | form.loginPage("/login.html") | 页面需暴露为公开路径;默认 /login 会被覆盖 |
| loginProcessingUrl | form.loginProcessingUrl(String) | 指定表单提交的目标 URL(认证处理端点) | form.loginProcessingUrl("/authenticate") | 默认为 /login(POST);需与前端表单 action 一致 |
| defaultSuccessUrl | form.defaultSuccessUrl(String, boolean) | 登录成功后默认跳转路径 | form.defaultSuccessUrl("/home", true) | 第二个参数 force 表示是否始终跳转(忽略原请求) |
| successHandler | form.successHandler(AuthenticationSuccessHandler) | 自定义登录成功处理器 | form.successHandler(new CustomSuccessHandler()) | 可实现重定向、JSON 响应等;覆盖 defaultSuccessUrl |
| failureHandler | form.failureHandler(AuthenticationFailureHandler) | 自定义登录失败处理器 | form.failureHandler(new CustomFailureHandler()) | 可返回错误码、记录日志等 |
| usernameParameter | form.usernameParameter(String) | 自定义用户名表单字段名 | form.usernameParameter("email") | 默认为 “username”;需与前端 input name 一致 |
| passwordParameter | form.passwordParameter(String) | 自定义密码表单字段名 | form.passwordParameter("pwd") | 默认为 “password” |
4.4 登出机制配置:logout 与登出处理器
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| logoutUrl | logout.logoutUrl(String) | 自定义登出请求的 URL | http.logout(logout -> logout.logoutUrl("/sign-out")) | 默认为 /logout(POST);可配置多个 URL |
| logoutSuccessUrl | logout.logoutSuccessUrl(String) | 登出成功后跳转的 URL | http.logout(logout -> logout.logoutSuccessUrl("/login?logout")) | 默认跳转到登录页 |
| logoutSuccessHandler | logout.logoutSuccessHandler(LogoutSuccessHandler) | 自定义登出成功处理器 | http.logout(logout -> logout.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())) | 可返回 JSON、重定向等;覆盖 logoutSuccessUrl |
| invalidateHttpSession | logout.invalidateHttpSession(boolean) | 是否使 HttpSession 失效 | http.logout(logout -> logout.invalidateHttpSession(true)) | 默认 true;会清除会话数据 |
| deleteCookies | logout.deleteCookies(String...) | 登出时删除指定 Cookie | http.logout(logout -> logout.deleteCookies("JSESSIONID", "remember-me")) | 常用于清除 remember-me Cookie |
| clearAuthentication | logout.clearAuthentication(boolean) | 是否清除 SecurityContext 中的 Authentication | 默认 true | 设为 false 则保留认证信息(少见) |
4.5 CSRF 防护机制与配置
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| disable | csrf.disable() | 禁用 CSRF 防护 | http.csrf(csrf -> csrf.disable()) | 常用于 REST API + JWT 场景;仅在可信环境使用 |
| csrfTokenRepository | csrf.csrfTokenRepository(CsrfTokenRepository) | 配置 CSRF Token 存储策略 | http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) | 默认使用 HttpSession;Cookie 方式便于前端读取 |
| requireCsrfProtectionMatcher | csrf.requireCsrfProtectionMatcher(RequestMatcher) | 自定义哪些请求需要 CSRF 保护 | http.csrf(csrf -> csrf.requireCsrfProtectionMatcher(new MediaTypeRequestMatcher(...))) | 默认保护非 GET/HEAD/TRACE/OPTIONS 请求 |
| sessionAuthenticationStrategy | 通过其他机制配置 | 防止 Session Fixation,常与 CSRF 联动 | http.sessionManagement(session -> session.sessionAuthenticationStrategy(new ChangeSessionIdAuthenticationStrategy())) | changeSessionId() 可增强安全性 |
4.6 CORS 配置与跨域安全策略
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| cors | http.cors(Customizer) | 启用并配置 CORS 策略 | http.cors(Customizer.withDefaults()) | 需配合 @Bean CorsConfigurationSource 使用 |
| CorsConfigurationSource | @Bean CorsConfigurationSource | 提供 CORS 配置源 | 见下方代码块 | 必须注册为 Bean;allowCredentials = true 时 origin 不能为 ”*“ |
| setAllowedOrigins | config.setAllowedOrigins(List) | 设置允许的来源 | config.setAllowedOrigins(Arrays.asList("https://example.com")) | 推荐具体域名,避免 ”*“;配合 allowCredentials 使用需精确 |
| setAllowedOriginPatterns | config.setAllowedOriginPatterns(List) | 设置允许的来源模式(支持 ”*” 通配) | config.setAllowedOriginPatterns(Arrays.asList("https://*.example.com")) | Spring 5.3+ 支持;更灵活的跨域控制 |
CORS 配置示例:
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("GET", "POST"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
4.7 安全响应头配置:X-Frame-Options、HSTS 等
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| frameOptions | http.headers(headers -> headers.frameOptions(fo -> fo.disable())) | 配置 X-Frame-Options 头 | headers.frameOptions(fo -> fo.sameOrigin()) | 默认 DENY,防止点击劫持;sameOrigin 允许同源嵌套 |
| hsts | headers.httpStrictTransportSecurity(hsts -> ...) | 启用 HSTS(HTTP 严格传输安全) | 见下方代码块 | 强制浏览器使用 HTTPS;配置后不可逆,慎用 |
| contentTypeOptions | headers.contentTypeOptions(Customizer) | 启用 X-Content-Type-Options 防 MIME 欺骗 | headers.contentTypeOptions(Customizer.withDefaults()) | 默认启用,设为 nosniff |
| xssProtection | headers.xssProtection(Customizer) | 配置 X-XSS-Protection 头 | headers.xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)) | 浏览器 XSS 过滤器;新版浏览器已弃用 |
| contentSecurityPolicy | headers.contentSecurityPolicy(csp -> ...) | 配置 CSP(内容安全策略) | headers.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'; script-src 'self'")) | 强大的前端安全控制;配置复杂,需逐步调试 |
HSTS 配置示例:
headers.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubdomains(true));
4.8 会话管理:SessionManagementConfigurer 与并发控制
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| sessionManagement | http.sessionManagement(Customizer) | 配置会话管理策略 | 见下方代码块 | 控制会话创建、失效、并发等行为 |
| maximumSessions | session.maximumSessions(int) | 设置单用户最大并发会话数 | session.maximumSessions(1) | 超出限制时阻止新登录或踢出旧会话 |
| maxSessionsPreventsLogin | maxSessionsPreventsLogin(boolean) | 达到最大会话数时是否阻止新登录 | true 阻止新登录 / false 踢出最早会话 | 默认 false |
| expiredUrl | expiredUrl(String) | 会话过期后跳转的 URL | session.expiredUrl("/login?expired") | 仅当并发控制启用时有效 |
| invalidSessionUrl | invalidSessionUrl(String) | 无效会话(如 ID 不存在)时跳转 URL | session.invalidSessionUrl("/login") | 与 expiredUrl 不同 |
| sessionCreationPolicy | session.sessionCreationPolicy(SessionCreationPolicy) | 控制会话创建策略 | session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) | STATELESS:不创建会话(JWT 场景);ALWAYS:总是创建 |
会话管理配置示例:
http.sessionManagement(session -> session
.invalidSessionUrl("/login?expired")
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.expiredUrl("/login?expired"));
第五章:高级认证机制
5.1 基于数据库的认证:整合 JPA/MyBatis 实现 UserDetailsService
| 名称 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|
| UserRepository | 使用 JPA 访问用户数据 | 见下方代码块 | 需确保 findByUsername 查询高效(加索引) |
| MyBatis Mapper | 使用 MyBatis 访问用户数据 | 见下方代码块 | SQL 需防止注入;结果映射正确 |
| PasswordEncoder | 注入密码编码器用于验证 | encoder.matches(rawPassword, user.getPassword()) | 必须使用编码器验证,不可明文比较 |
JPA 方式:
@Repository
interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
@Service
public class DatabaseUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("..."));
return buildUserDetails(user);
}
}
MyBatis 方式:
@Mapper
public interface UserMapper {
User selectByUsername(String username);
}
// 在 UserDetailsService 中使用
public UserDetails loadUserByUsername(String username) {
User user = userMapper.selectByUsername(username);
if (user == null) throw new UsernameNotFoundException("...");
return User.builder()...build();
}
密码验证:
if (!encoder.matches(rawPassword, user.getPassword())) {
throw new BadCredentialsException("Invalid password");
}
5.2 JWT 认证原理与流程(概念)
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| JWT(JSON Web Token) | 一种开放标准(RFC 7519),用于在各方之间安全传输信息作为 JSON 对象,通常用于认证和信息交换 | 包含 Header、Payload、Signature 三部分;可被签名(HMAC/RS256)和/或加密 |
| 无状态认证 | 服务器不保存会话状态,用户信息和权限包含在 Token 中,每次请求携带 Token 即可 | 适合分布式、微服务架构;Token 一旦签发无法主动失效(需配合黑名单) |
| Token 签发流程 | 用户登录 → 服务验证凭据 → 生成 JWT → 返回客户端 | 通常在 /login 端点完成;Token 存储在 localStorage 或 Cookie |
| Token 验证流程 | 客户端请求携带 Token(通常在 Authorization: Bearer <token>)→ 服务解析并验证签名 → 提取用户信息 → 设置 SecurityContext | 需自定义过滤器拦截请求;验证签名和过期时间 |
| JWT 缺点 | 无法主动注销、Token 较长、Payload 信息可能泄露 | 可通过短期 Token + Refresh Token 缓解;敏感信息不应放入 Payload |
5.3 自定义 JWT 过滤器:JwtAuthenticationFilter 实现
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| OncePerRequestFilter | public class JwtAuthenticationFilter extends OncePerRequestFilter | 确保过滤器在每次请求中只执行一次 | 见下方代码块 | 推荐继承此类 |
| doFilterInternal | protected void doFilterInternal(...) | 执行过滤逻辑 | 见下方代码块 | 必须调用 filterChain.doFilter() 继续流程;捕获异常避免中断 |
| Bearer Token 解析 | request.getHeader("Authorization") | 从请求头提取 JWT Token | 见下方代码块 | 注意空格和前缀 |
| SecurityContextHolder | SecurityContextHolder.getContext().setAuthentication(auth) | 将认证信息放入安全上下文 | 见下方代码块 | 后续过滤器和控制器可获取用户信息 |
完整 JWT 过滤器实现:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain) {
String token = extractToken(request);
if (token != null && jwtUtil.validate(token)) {
String username = jwtUtil.getUsername(token);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(...);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
}
5.4 OAuth2 协议核心角色与四种授权模式(概念)
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| Resource Owner | 资源所有者,即用户,授权客户端访问其资源 | 通常是最终用户 |
| Client | 客户端应用,请求访问用户资源 | 如 Web 应用、移动 App |
| Authorization Server | 认证服务器,负责颁发访问令牌 | 如 Google、GitHub、自建 Auth Server |
| Resource Server | 资源服务器,存储用户数据并接受带 Token 的请求 | 如用户 API、订单系统 |
| 授权码模式(Authorization Code) | 最安全的模式,客户端获取授权码后换取 Token | 适用于有后端的应用;支持 PKCE 增强安全 |
| 简化模式(Implicit) | 前端应用直接获取 Token(通过 URL fragment) | 已不推荐,因 Token 暴露在 URL |
| 密码模式(Resource Owner Password Credentials) | 用户直接提供用户名密码给客户端,客户端换取 Token | 仅适用于高度信任的客户端;Spring Security 6 已弃用 |
| 客户端模式(Client Credentials) | 客户端以自身身份获取 Token,不涉及用户 | 用于服务间通信;无用户上下文 |
5.5 OAuth2 客户端集成:spring-security-oauth2-client
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| spring-security-oauth2-client | Maven 依赖 | 提供 OAuth2 客户端支持 | 见下方代码块 | 用于实现第三方登录 |
| application.yml 配置 | spring.security.oauth2.client.registration.google | 配置 OAuth2 客户端注册信息 | 见下方代码块 | 支持 google、github、facebook 等;可自定义 provider |
| OAuth2AuthorizedClientService | @Autowired OAuth2AuthorizedClientService | 获取已授权的客户端信息 | OAuth2AuthorizedClient client = clientService.loadAuthorizedClient("google", "username") | 可获取 access_token |
| @RegisteredOAuth2AuthorizedClient | @RegisteredOAuth2AuthorizedClient("google") | 注入已授权客户端 | 见下方代码块 | 简化 Token 获取 |
Maven 依赖:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
application.yml 配置:
spring:
security:
oauth2:
client:
registration:
google:
client-id: your-client-id
client-secret: your-secret
scope: profile,email
控制器中获取 Token:
public String home(@RegisteredOAuth2AuthorizedClient("google") OAuth2AuthorizedClient client) {
String token = client.getAccessToken().getTokenValue();
// ...
}
5.6 OAuth2 资源服务器配置:JWT Token 验证
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| spring-security-oauth2-resource-server | Maven 依赖 | 启用 OAuth2 资源服务器功能 | 见下方代码块 | 用于验证 JWT Token |
| issuer-uri | spring.security.oauth2.resourceserver.jwt.issuer-uri | 指定 JWT 签发者 URI,自动获取公钥 | 见下方代码块 | 需支持 .well-known/jwks.json |
| jwk-set-uri | spring.security.oauth2.resourceserver.jwt.jwk-set-uri | 指定 JWK Set URI 手动配置公钥地址 | jwk-set-uri: https://idp.example.com/oauth2/jwks | 用于无法使用 issuer-uri 的场景 |
| JwtDecoder Bean | @Bean | 自定义 JwtDecoder | return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build() | 可完全控制解码逻辑 |
Maven 依赖:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
application.yml 配置:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com
5.7 单点登录(SSO)基础实现原理(概念)
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| SSO(Single Sign-On) | 用户一次登录,即可访问多个相互信任的系统,无需重复认证 | 提升用户体验;常见于企业应用集成 |
| 中央认证服务器(CAS) | 负责统一处理用户认证请求,生成票据(Ticket) | 如 Keycloak、Auth0、自建系统;所有客户端信任该服务器 |
| 服务票据(Service Ticket) | CAS 发放的一次性票据,客户端用其换取用户信息 | 防重放攻击;有效期短 |
| 登录流程 | 用户访问 App1 → 重定向至 CAS → 用户登录 → CAS 返回 ST → App1 验证 ST 获取用户信息 → 登录成功 → 访问 App2 时自动登录 | 基于重定向和票据交换;首次登录需输入凭证 |
| 会话共享 | 多种实现方式:共享 Cookie(同域)、Token 传递、OAuth2/OpenID Connect | OpenID Connect 是现代 SSO 主流方案;基于 JWT 实现身份传递 |
第六章:自定义安全组件
6.1 登录成功处理器:AuthenticationSuccessHandler
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| AuthenticationSuccessHandler | 接口 | 定义登录成功后的处理逻辑,用于自定义跳转、记录日志、返回 JSON 等 | public class CustomSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { // 重定向或写入 JSON 响应 response.sendRedirect("/dashboard"); }} | - 必须实现 onAuthenticationSuccess 方法 - 不应调用 filterChain.doFilter() |
| SimpleUrlAuthenticationSuccessHandler | 类 | Spring 提供的默认实现,支持配置成功跳转 URL | @BeanAuthenticationSuccessHandler successHandler() { SimpleUrlAuthenticationSuccessHandler handler = new SimpleUrlAuthenticationSuccessHandler("/home"); handler.setAlwaysUseDefaultTargetUrl(true); return handler;} | - 可设置 defaultTargetUrl 和 alwaysUseDefault - 适用于简单重定向场景 |
| SavedRequestAwareAuthenticationSuccessHandler | 类 | 继承 SimpleUrlAuthenticationHandler,支持跳转到用户原始请求地址 | @BeanAuthenticationSuccessHandler successHandler() { return new SavedRequestAwareAuthenticationSuccessHandler();} | - 默认行为:登录后跳转到引发登录的页面 - 适合需要保持用户上下文的场景 |
6.2 登录失败处理器:AuthenticationFailureHandler
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| AuthenticationFailureHandler | 接口 | 定义认证失败后的处理逻辑,如返回错误信息、记录失败次数 | public class CustomFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write("{\"error\": \"" + exception.getMessage() + "\"}"); }} | - 可根据 exception 类型区分错误(如账户锁定、密码错误) - 通常用于 REST API 返回 JSON 错误 |
| SimpleUrlAuthenticationFailureHandler | 类 | Spring 提供的默认实现,登录失败后重定向到指定页面 | @BeanAuthenticationFailureHandler failureHandler() { return new SimpleUrlAuthenticationFailureHandler("/login?error");} | - 适用于表单登录失败跳转 - 可结合 request.getParameter(“username”) 显示错误信息 |
| 异常类型判断 | exception instanceof BadCredentialsException | 根据不同异常类型执行不同逻辑 | if (exception instanceof LockedException) { // 账户已锁定} else if (exception instanceof DisabledException) { // 账户被禁用} | - 常见异常:BadCredentialsException, LockedException, DisabledException, AccountExpiredException |
6.3 访问拒绝处理器:AccessDeniedHandler
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| AccessDeniedHandler | 接口 | 处理授权失败(403 Forbidden)的情况,如权限不足 | public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { response.setStatus(HttpStatus.FORBIDDEN.value()); response.getWriter().write("{\"error\": \"Access Denied\"}"); }} | - 仅在已认证用户访问无权限资源时触发 - 未认证访问受保护资源由 AuthenticationEntryPoint 处理 |
| SimpleAccessDeniedHandler | 类 | Spring 提供的简单实现,仅设置状态码 | new SimpleAccessDeniedHandler(); | - 默认返回 403 状态码 - 无自定义响应内容 |
| 配置方式 | http.exceptionHandling(e -> e.accessDeniedHandler(...)) | 在 HttpSecurity 中注册处理器 | http.exceptionHandling(e -> e.accessDeniedHandler(new CustomAccessDeniedHandler())); | - 必须通过 exceptionHandling() 配置 - 可与 authenticationEntryPoint 配合使用 |
6.4 自定义过滤器:实现 Filter 并注册到过滤器链
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Filter 接口 | public class CustomFilter implements Filter | 创建自定义过滤器,拦截请求并执行安全逻辑 | public class CustomFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 前置处理 chain.doFilter(request, response); // 后置处理 }} | - 必须调用 chain.doFilter() 继续流程 - 可修改请求/响应 |
| OncePerRequestFilter | public class CustomOnceFilter extends OncePerRequestFilter | 确保过滤器在单次请求中只执行一次,推荐使用 | public class CustomOnceFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ... { // 业务逻辑 filterChain.doFilter(request, response); }} | - 避免在异步请求中重复执行 - Spring Security 内部多数过滤器继承此类 |
| 注册为 Bean | @Component 或 @Bean | 将过滤器注册为 Spring Bean | @Componentpublic class CustomFilter { ... } | - 仅注册为 Bean 不会自动加入安全链 |
6.5 替换或插入过滤器:addFilterBefore、addFilterAfter、addFilterAt
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| addFilterBefore | http.addFilterBefore(filter, TargetFilter.class) | 在指定过滤器之前添加自定义过滤器 | http.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class); | - TargetFilter 必须是安全链中存在的过滤器 - 常用于在认证前插入 JWT 解析 |
| addFilterAfter | http.addFilterAfter(filter, TargetFilter.class) | 在指定过滤器之后添加自定义过滤器 | http.addFilterAfter(new AuditFilter(), FilterSecurityInterceptor.class); | - 适用于审计、日志等后置操作 - 注意目标过滤器是否存在 |
| addFilterAt | http.addFilterAt(filter, TargetFilter.class) | 替换指定位置的过滤器(不推荐,易出错) | 不常用,可能破坏默认流程 | - 实际很少使用,因会替换原有过滤器 - 更推荐 addFilterBefore/After |
| 常见过滤器顺序 | UsernamePasswordAuthenticationFilter → DefaultLoginPageGeneratingFilter → LogoutFilter → BasicAuthenticationFilter → FilterSecurityInterceptor | 了解默认过滤器顺序有助于定位插入点 | 查看 SecurityFilterChain 的 getFilters() 方法 | - 插入点选择影响执行顺序和结果 |
6.6 安全上下文管理:SecurityContext 与 SecurityContextHolder
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| SecurityContext | SecurityContextHolder.getContext() | 存储当前安全上下文,包含 Authentication 对象 | SecurityContext context = SecurityContextHolder.getContext();Authentication auth = context.getAuthentication(); | - 每个请求独享一个上下文(由 SecurityContextPersistenceFilter 管理) |
| SecurityContextHolder | 类 | 静态工具类,用于访问 SecurityContext | String username = SecurityContextHolder.getContext() .getAuthentication().getName(); | - 默认使用 ThreadLocal 策略(MODE_THREADLOCAL) - 异步时需注意上下文传递 |
| Authentication | context.getAuthentication() | 表示当前用户的认证信息,包含 principal、credentials、authorities 等 | Object principal = auth.getPrincipal();// 通常是 UserDetails 对象Collection<? extends GrantedAuthority> authorities = auth.getAuthorities(); | - principal:用户主体(UserDetails 或用户名) - credentials:凭证(登录后通常设为 null) - authorities:权限列表 |
| 设置认证信息 | context.setAuthentication(auth) | 手动设置认证信息(如 JWT 过滤器中) | UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authToken); | - 通常在自定义认证过滤器中使用 - 需确保 UserDetails 已加载 |
| 策略模式 | SecurityContextHolder.setStrategyName(...) | 设置上下文存储策略 | SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLE_THREADLOCAL); | - MODE_THREADLOCAL:默认,基于线程 - MODE_GLOBAL:JVM 全局(不推荐) |
第七章:安全最佳实践与调优
7.1 多环境安全配置管理(dev/test/prod)
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Profile 配置文件 | application-{profile}.yml | 为不同环境提供独立的安全配置 | application-dev.yml:spring.security.oauth2.client.registration.github.client-secret: dev-secretlogging.level.org.springframework.security: DEBUGapplication-prod.yml: spring.security.oauth2.client.registration.github.client-secret: ${GITHUB_CLIENT_SECRET}security.headers.hsts: all | - 敏感配置使用环境变量占位符 ${} - 开发环境可禁用 CSRF、开启调试日志 |
| @Profile 注解 | @Profile("prod") | 条件化注册安全 Bean | @Configuration@Profile("prod")public class ProductionSecurityConfig { @Bean public SecurityFilterChain prodFilterChain(HttpSecurity http) throws Exception { http.headers().hsts(hsts -> hsts.maxAgeInSeconds(31536000)); return http.build(); }} | - 避免生产环境 Bean 在开发时加载 - 可组合使用 @Profile(“!dev”) |
| ConditionalOnProperty | @ConditionalOnProperty | 根据配置属性条件化启用配置 | @Bean@ConditionalOnProperty(name = "app.security.mfa.enabled", havingValue = "true")public MfaAuthenticationFilter mfaFilter() { ... } | - 实现功能开关(Feature Toggle) - 便于灰度发布 |
| ConfigurationProperties | @ConfigurationProperties("app.security") | 类型安全地绑定安全配置 | @ConfigurationProperties("app.security")public class SecurityProperties { private boolean csrfEnabled = true; private List<String> allowedOrigins = Arrays.asList("https://example.com"); // getters and setters} | - 集中管理配置项 - 支持嵌套对象和列表 |
7.2 敏感信息加密与配置保护
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Jasypt 加密 | jasypt.encryptor.password | 对配置文件中的敏感值进行加解密 | application.yml:spring: datasource: password: ENC(ABCD1234...) # 加密后的密码pom.xml: <dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId></dependency> | - 启动时需提供主密码(jasypt.encryptor.password) - 加密值格式为 ENC(cipherText) |
| 环境变量 | ${DB_PASSWORD} | 从操作系统环境变量读取敏感信息 | spring.datasource.password: ${DB_PASSWORD} | - 部署时通过 export DB_PASSWORD=… 设置 - CI/CD 流水线中配置为 secret |
| Hashicorp Vault | spring.cloud.vault.* | 集中式密钥管理服务 | spring.cloud.vault: host: vault.example.com scheme: https authentication: TOKEN token: ${VAULT_TOKEN} | - 需引入 spring-cloud-starter-vault-config - 适用于大规模微服务架构 |
| AWS KMS / Azure Key Vault | 云服务商密钥管理 | 利用云平台安全服务保护密钥 | 使用云 SDK 或 Spring Cloud AWS/Azure 模块集成 | - 高安全性,支持审计和轮换 - 成本较高,适合关键系统 |
7.3 安全事件监听:AuthenticationSuccessEvent 等
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| ApplicationListener | implements ApplicationListener<AuthenticationSuccessEvent> | 监听认证成功事件 | @Componentpublic class AuthSuccessEventListener implements ApplicationListener<AuthenticationSuccessEvent> { @Override public void onApplicationEvent(AuthenticationSuccessEvent event) { String username = event.getAuthentication().getName(); log.info("User {} logged in successfully", username); }} | - 可用于记录登录日志、更新用户状态 |
| @EventListener | @EventListener | 基于注解的事件监听(推荐) | @EventListenerpublic void handleAuthenticationSuccess(AuthenticationSuccessEvent event) { // 处理逻辑}@EventListenerpublic void handleAuthenticationFailure(AuthenticationFailureEvent event) { String username = event.getAuthentication().getName(); loginAttemptService.recordFailure(username);} | - 更简洁,支持条件过滤 @EventListener(condition = “#event.authentication.authorities.?[…].size() > 0”) |
| 常见安全事件 | AuthenticationSuccessEvent / AuthenticationFailureBadCredentialsEvent / InteractiveAuthenticationSuccessEvent / SessionDestroyedEvent | 监听关键安全事件 | @EventListenerpublic void onSessionDestroyed(SessionDestroyedEvent event) { // 清理与会话相关的缓存或资源} | - AuthenticationFailureEvent 有多种子类型(如 BadCredentials, LockedException) - 事件在事务提交后发布(默认) |
| 异步处理 | @Async | 异步执行耗时监听逻辑 | @EventListener@Asyncpublic void handleLoginSuccess(AuthenticationSuccessEvent event) { // 发送通知、更新统计等} | - 需启用 @EnableAsync - 避免阻塞主请求流程 |
7.4 用户详情缓存优化策略
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| CachingUserDetailsService | new CachingUserDetailsService(delegate) | 包装 UserDetailsService 实现缓存 | @Beanpublic UserDetailsService userDetailsService(UserDetailsService delegate) { return new CachingUserDetailsService(delegate);} | - 使用 Spring 默认 UserCache(基于 ConcurrentMap) - 缓存 UserDetails 对象 |
| 自定义 UserCache | userCache.setUserCache(...) | 替换默认缓存实现为 Redis 等 | @Beanpublic UserCache userCache(RedisConnectionFactory factory) { RedisCache redisCache = new RedisCache(factory, "user-cache", Duration.ofMinutes(30)); return new SpringCacheUserCache(redisCache);} | - 需引入 spring-boot-starter-data-redis - 实现分布式缓存 |
| 缓存失效策略 | userCache.removeUserFromCache(username) | 手动清除缓存(如用户信息更新后) | @Servicepublic class UserService { @Autowired private UserCache userCache; public void updateUser(String username, UserUpdateForm form) { // 更新数据库 userCache.removeUserFromCache(username); // 清除旧缓存 }} | - 避免缓存与数据库不一致 - 可结合事件机制自动失效 |
| 缓存粒度 | 缓存整个 UserDetails 对象 | 减少数据库查询次数 | UserDetails userDetails = userCache.getUserFromCache(username);if (userDetails == null) { userDetails = delegate.loadUserByUsername(username); userCache.putUserInCache(userDetails);} | - 适合用户信息不频繁变更的场景 - 注意内存占用 |
7.5 常见安全漏洞防范:CSRF、XSS、Session Fixation
| 漏洞类型 | 防范措施 | 配置示例 | 注意事项 |
|---|---|---|---|
| CSRF(跨站请求伪造) | 启用 CSRF 保护,使用 Token 验证 | http.csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));// 前端从 Cookie 读取 XSRF-TOKEN 并设置为请求头 X-XSRF-TOKEN | - 表单登录必须启用 - REST API 使用 JWT 时可禁用 |
| XSS(跨站脚本) | 输出编码、CSP 策略、输入过滤 | http.headers(headers -> headers .xssProtection(xss -> xss.headerValue(ENABLED_MODE_BLOCK)) .contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'; script-src 'self'"))); | - 避免在页面拼接用户输入 - 使用 Thymeleaf 等模板引擎自动转义 |
| Session Fixation(会话固定) | 登录后更换 Session ID | http.sessionManagement(session -> session .sessionAuthenticationStrategy(new ChangeSessionIdAuthenticationStrategy())); | - 默认策略为 migrateSession()(Servlet 3.1+) - 可防止攻击者预设 Session ID |
| 会话劫持 | 使用安全 Cookie、HTTPS、HttpOnly | http.sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .invalidSessionUrl("/login")).securityContext(sc -> sc.requireAllSecureCookies(true)); | - secure=true:仅 HTTPS 传输 - httpOnly=true:JS 无法访问 |
| 敏感操作二次验证 | 对关键操作(如改密、支付)要求重新认证 | 自定义过滤器或 AOP 拦截,检查 Authentication 时间戳或要求重新输入密码 | - 提升安全性 - 影响用户体验,需权衡 |
第八章:Spring Security 6 与响应式编程
8.1 Spring Security 6 主要变更与新特性(概念)
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Lambda DSL | http.authorizeHttpRequests(authz -> {...}) | 新的函数式配置风格,替代旧的链式调用 | @BeanSecurityWebFilterChain springSecurity(ServerHttpSecurity http) { http .authorizeExchange(exchanges -> exchanges .pathMatchers("/public/**").permitAll() .anyExchange().authenticated() ) .formLogin(withDefaults()); return http.build();} | - 更清晰的代码结构 - 需使用 ServerHttpSecurity(响应式)或 HttpSecurity(阻塞) |
| Security 6 默认策略收紧 | 内置默认安全增强 | 提高默认安全性,减少配置遗漏风险 | 无需额外代码,框架自动启用 | - 如:CSRF 默认启用 - 需注意迁移旧项目时的兼容性 |
| OAuth2 Authorization Server 支持 | spring-security-oauth2-authorization-server | 内建 OAuth2 授权服务器功能 | 引入 spring-security-oauth2-authorization-server 依赖 | - 不再依赖 Spring Security OAuth - 支持 PKCE、JWT 等标准 |
| CSRF 改进 | CsrfTokenRequestAttributeHandler | 更灵活的 CSRF Token 管理 | http.csrf(csrf -> csrf .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())); | - 更好支持 REST API 和单页应用(SPA) |
8.2 Lambda DSL 配置方式:新风格安全配置
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Lambda 配置块 | authorizeHttpRequests(authz -> {...}) | 使用 Lambda 表达式配置安全规则 | http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/css/**", "/js/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .formLogin(login -> login .loginPage("/login") .permitAll() ); | - 推荐用于 Spring Security 6+ - 可读性更强,作用域更清晰 |
| 方法引用与命名变量 | formLogin(CustomConfig::loginConfig) | 提高配置复用性和可测试性 | private static void loginConfig(FormLoginSpec login) { login.loginPage("/login").permitAll();}http.formLogin(YourClass::loginConfig); | - 适合复杂配置抽取 - 便于单元测试 |
| 条件化配置 | if (env.isProd()) { ... } | 根据环境动态配置安全策略 | http.authorizeHttpRequests(authz -> { authz.requestMatchers("/dev/**"); if (!isProduction) { authz.permitAll(); } else { authz.hasRole("DEV"); }}); | - 避免硬编码环境判断 - 推荐使用 @Profile 替代 |
8.3 响应式安全:WebFlux 与 Spring Security 集成
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| ServerHttpSecurity | ServerHttpSecurity | 响应式安全配置主类(类比 HttpSecurity) | @Beanpublic SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { http .authorizeExchange(exchanges -> exchanges .anyExchange().authenticated() ) .httpBasic(withDefaults()); return http.build();} | - 必须返回 SecurityWebFilterChain - 使用 @EnableWebFluxSecurity |
| Reactor Context 与 SecurityContext | Mono.subscriberContext() | 在响应式流中访问安全上下文 | public Mono<String> getCurrentUsername() { return Mono.subscriberContext() .map(ctx -> { Authentication auth = ctx.get(SecurityContext.class).getAuthentication(); return auth.getName(); });} | - 不再使用 ThreadLocal - 上下文通过 ContextView 传递 |
| 响应式认证入口点 | ServerAuthenticationEntryPoint | 处理未认证请求的响应 | http.exceptionHandling(e -> e .authenticationEntryPoint((exchange, ex) -> Mono.fromRunnable(() -> { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); }) )); | - 返回 Mono<Void> - 适用于 REST API 返回 JSON |
| 响应式授权决策 | ReactiveAuthorizationManager | 响应式权限判断 | .authorizeExchange(exchanges -> exchanges .pathMatchers("/api/**") .access((auth, context) -> auth.map(a -> a.getAuthorities()) .map(au -> au.contains(new SimpleGrantedAuthority("USER"))) .defaultIfEmpty(false) ) ) | - 使用 Mono<Boolean> 进行判断 - 支持异步权限查询 |
8.4 响应式用户详情服务:ReactiveUserDetailsService
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| ReactiveUserDetailsService | 接口 | 响应式加载用户详情,返回 Mono<UserDetails> | @Componentpublic class MongoReactiveUserDetailsService implements ReactiveUserDetailsService { @Autowired private ReactiveUserRepository userRepository; @Override public Mono<UserDetails> findByUsername(String username) { return userRepository.findByUsername(username) .switchIfEmpty(Mono.error(new UsernameNotFoundException("User not found"))) .map(user -> User.withUsername(user.getUsername()) .password(user.getPassword()) .authorities("USER") .build()); }} | - 适合与 MongoDB、R2DBC 等响应式数据源集成 - 避免阻塞线程 |
| InMemoryReactiveUserDetailsManager | 类 | 内存式响应式用户管理器(测试用) | @Beanpublic ReactiveUserDetailsService reactiveUserDetailsService() { UserDetails user = User.withDefaultPasswordEncoder() .username("user") .password("password") .roles("USER") .build(); return new InMemoryReactiveUserDetailsManager(user);} | - 仅用于演示或测试 - 密码编码器使用需注意安全 |
| 响应式密码编码 | ReactivePasswordEncoder | 响应式密码验证(较少直接使用) | 框架自动集成 PasswordEncoder 到响应式流程 | - 通常仍使用 BCryptPasswordEncoder - 框架负责适配 |
| 与 WebClient 集成 | 在 WebClient 请求中携带认证信息 | 调用下游服务时传递安全上下文 | webClient.get() .uri("/api/data") .attributes(SecurityContextServerWebExchangeWebFilter. securityContext(Mono.just(securityContext))) .retrieve() .bodyToMono(String.class); | - 实现服务间安全上下文传播 - 适用于微服务场景 |
第九章:实战项目集成
9.1 REST API 安全设计:无状态 JWT 方案
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| JWT 生成 | Jwts.builder().signWith().compact() | 创建签名的 JWT Token | String token = Jwts.builder() .setSubject(user.getUsername()) .claim("roles", user.getAuthorities()) .setExpiration(new Date(System.currentTimeMillis() + 86400000)) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); | - 使用强密钥(HS512 或 RS256) - 避免在 payload 中存储敏感信息 |
| JWT 解析与验证 | Jwts.parser().setSigningKey().parseClaimsJws() | 验证 Token 并提取用户信息 | try { Jws<Claims> claims = Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token); String username = claims.getBody().getSubject();} catch (JwtException e) { throw new BadCredentialsException("Invalid JWT");} | - 捕获 JwtException 处理无效 Token - 验证过期时间、签名等 |
| JWT 过滤器 | OncePerRequestFilter | 在请求链中拦截并验证 JWT | public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { String token = extractToken(request); if (token != null && jwtUtil.validateToken(token)) { String username = jwtUtil.getUsername(token); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, getAuthorities(username)); SecurityContextHolder.getContext().setAuthentication(auth); } chain.doFilter(request, response); }} | - 必须在 UsernamePasswordAuthenticationFilter 之前插入 - 支持 Bearer 前缀 |
| 安全配置禁用 Session | sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) | 确保应用无状态 | http .csrf().disable() .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authz -> authz .requestMatchers("/auth/login", "/auth/refresh").permitAll() .anyRequest().authenticated() ) .addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class); | - 禁用 CSRF(若使用 JWT) - 不创建 HttpSession |
9.2 前后端分离登录:Token 刷新与存储策略
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| Refresh Token | 长生命周期 Token | 用于获取新的 Access Token,避免频繁登录 | public class TokenResponse { private String accessToken; private String refreshToken; // 构造函数、getter/setter}// 登录成功返回return ResponseEntity.ok(new TokenResponse(accessToken, refreshToken)); | - Refresh Token 应存储在安全位置(如 HttpOnly Cookie) - 设置较短过期时间(如 7 天) |
| Token 刷新端点 | /auth/refresh | 接收 Refresh Token 并返回新 Access Token | @PostMapping("/refresh")public ResponseEntity<?> refreshToken(@RequestBody RefreshRequest request) { if (refreshTokenService.validate(request.getRefreshToken())) { String username = refreshTokenService.getUsername(request.getRefreshToken()); String newAccessToken = jwtUtil.generateToken(username); return ResponseEntity.ok(new TokenResponse(newAccessToken, request.getRefreshToken())); } return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();} | - 验证 Refresh Token 有效性 - 可实现”一次使用即失效”策略 |
| 存储策略:HttpOnly Cookie | response.addCookie() | 安全存储 Refresh Token,防范 XSS | Cookie cookie = new Cookie("refreshToken", refreshToken);cookie.setHttpOnly(true);cookie.setSecure(true); // HTTPScookie.setPath("/auth/refresh");cookie.setMaxAge(604800); // 7 daysresponse.addCookie(cookie); | - 前端 JS 无法访问,防止 XSS 窃取 - 需配合 SameSite 属性 |
| 存储策略:LocalStorage | localStorage.setItem("token", token) | 前端存储 Access Token(仅限 SPA) | // 登录后localStorage.setItem("accessToken", response.data.accessToken);// 请求拦截器axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; | - 易受 XSS 攻击 - 适合低安全要求场景 |
| CORS 配置 | @CrossOrigin 或 CorsConfiguration | 允许前端域名访问后端 API | @Beanpublic CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOriginPatterns(Arrays.asList("https://yourapp.com")); config.setAllowedMethods(Arrays.asList("*")); config.setAllowedHeaders(Arrays.asList("*")); config.setExposedHeaders(Arrays.asList("Authorization")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source;} | - allowCredentials=true 时 origin 不能为 * - 暴露 Authorization 头供前端读取 |
9.3 多租户系统中的权限隔离
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 租户标识解析 | 请求头、子域名、路径 | 识别当前请求所属租户 | String tenantId = request.getHeader("X-Tenant-ID");if (tenantId == null) { tenantId = resolveFromSubdomain(request);}SecurityContextHolder.getContext().setTenantId(tenantId); // 自定义扩展 | - 常见方式:Header、Host、URL Path - 需在过滤器早期解析 |
| 数据库隔离策略 | Schema Per Tenant / Row Level | 数据存储层面的隔离 | - 独立 Schema:每个租户独立数据库或 schema - 共享表 + tenant_id:所有租户数据在同一表,通过 tenant_id 字段区分 | - Schema 隔离:安全性高,成本高 - Row Level:成本低,需严格 WHERE 条件 |
| JPA 多租户支持 | @TenantId、Hibernate 多租户 | 集成 JPA 实现自动租户过滤 | // 实体类@Entitypublic class Document { @Id private Long id; private String content; private String tenantId; // 租户标识}// Hibernate 配置开启多租户spring.jpa.properties.hibernate.multiTenancy=SCHEMA | - 使用 Hibernate MultiTenantConnectionProvider - 避免手动拼接 SQL |
| Spring Security 动态权限 | hasPermission() 表达式 | 基于租户和资源的细粒度授权 | http.authorizeHttpRequests(authz -> authz .requestMatchers("/docs/{id}").access((authentication, object) -> hasPermission(authentication, object, "read", getTenantId()))); | - 自定义 PermissionEvaluator - 结合 ObjectIdentity 和 Sid |
| 租户上下文过滤器 | TenantContextFilter | 设置当前线程的租户上下文 | public class TenantContextFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { String tenantId = resolveTenantId((HttpServletRequest) request); TenantContext.setCurrentTenant(tenantId); try { chain.doFilter(request, response); } finally { TenantContext.clear(); } }} | - 使用 ThreadLocal 存储租户 ID - 必须在安全过滤器之前执行 |
9.4 动态权限管理:数据库驱动权限配置
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| 权限数据模型 | Role, Permission, RolePermission 表 | 存储角色与权限的映射关系 | CREATE TABLE role_permission ( role_id BIGINT, permission_id BIGINT, PRIMARY KEY (role_id, permission_id)); | - 支持 RBAC 模型 - 可扩展为资源-操作矩阵 |
| 动态加载 UserDetails | UserDetailsService 查询数据库 | 根据用户加载其角色和权限 | public UserDetails loadUserByUsername(String username) { User user = userRepository.findByUsername(username); List<Permission> perms = permissionService.findPermissionsByUser(user.getId()); List<GrantedAuthority> authorities = perms.stream() .map(p -> new SimpleGrantedAuthority(p.getName())) .collect(Collectors.toList()); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), authorities);} | - 缓存 UserDetails 减少 DB 查询 - 注意 N+1 查询问题 |
| 动态安全配置 | SecurityFilterChain 动态构建 | 从数据库读取 URL 权限规则 | @Beanpublic SecurityFilterChain filterChain(HttpSecurity http, PermissionService permissionService) throws Exception { List<UrlPermission> rules = permissionService.loadUrlPermissions(); ExpressionUrlAuthorizationManager manager = ExpressionUrlAuthorizationManager .withDefaultRolePrefix().requestMatchers(rules); http.authorizeHttpRequests(authz -> authz.withObjectPostProcessor(manager)); return http.build();} | - 启动时加载或定期刷新 - 可结合 @RefreshScope(Spring Cloud) |
| 权限变更事件 | @EventListener(PermissionChangedEvent.class) | 权限更新后刷新缓存或通知 | @EventListenerpublic void onPermissionChange(PermissionChangedEvent event) { userDetailsService.clearCache(); // 清除用户缓存 securityRuleCache.refresh(); // 刷新安全规则缓存} | - 保证权限变更实时生效 - 避免系统重启 |
9.5 与 Spring Boot Admin 集成的安全配置
| 名称 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|---|---|---|---|
| SBA Client 安全配置 | spring.boot.admin.client.* | 客户端注册时携带安全凭据 | spring.boot.admin.client.url: https://admin.example.comspring.boot.admin.client.username: ${SBA_CLIENT_USER}spring.boot.admin.client.password: ${SBA_CLIENT_PASS}spring.boot.admin.client.instance.name: My Application | - 使用环境变量保护凭据 - 确保 SBA Server 已启用安全 |
| SBA Server 安全配置 | spring.security.user.* | 保护 SBA Server 管理界面 | spring.security.user.name: adminspring.security.user.password: ${SBA_ADMIN_PASS}spring.boot.admin.ui.public-url: https://admin.example.com | - 强密码策略 - 可集成 OAuth2 或 LDAP |
| 自定义登录页 | @Controller + login.html | 为 SBA Server 提供自定义登录界面 | @Controllerpublic class LoginController { @GetMapping("/login") public String login() { return "login"; }} | - 放置在 src/main/resources/templates/login.html - 需配置 spring.boot.admin.ui.login-form=true |
| 健康端点保护 | management.endpoints.web.exposure.include=* | 控制敏感端点暴露 | # Client 配置management.endpoints.web.exposure.include=health,info,env,metricsmanagement.endpoint.health.show-details=when-authorizedspring.security.oauth2.client.registration.admin-client.scope=health.read,info.read | - 避免暴露 shutdown、env 等高危端点 - 细粒度授权 |
| OAuth2 集成 | spring.security.oauth2.client.* | 使用 OAuth2 登录 SBA Server | spring.security.oauth2.client.registration.github.client-id: ${GITHUB_CLIENT_ID}spring.security.oauth2.client.registration.github.client-secret: ${GITHUB_CLIENT_SECRET}spring.security.oauth2.client.registration.github.scope: read:user | - 支持 GitHub、Google 等提供商 - 提升用户体验 |