1. 概述
在 Spring Boot 应用中,我们常需要为不同路径配置多种安全过滤器。本文将深入探讨两种定制化安全方案:通过 @EnableWebSecurity
和 @EnableGlobalMethodSecurity
实现安全控制。
我们将通过一个包含管理资源、认证用户资源和公开资源的示例应用,直观展示两种方案的差异与适用场景。
2. Spring Boot 安全基础
2.1 Maven 依赖
无论采用哪种方案,首先需要添加 Spring Security 启动器:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.2 自动配置机制
当 Spring Security 在类路径中时,WebSecurityEnablerConfiguration
会自动启用 @EnableWebSecurity
。这会触发:
- 默认安全配置生效
- HTTP 安全过滤器与安全过滤器链自动激活
- 所有接口启用基础认证(Basic Auth)
简单粗暴地说:引入依赖后,Spring Boot 会自动给所有接口加锁!
3. 接口级安全控制
创建 MySecurityConfigurer
类并标注 @EnableWebSecurity
:
@Configuration
@EnableWebSecurity
public class MySecurityConfigurer {
// 配置内容见下文
}
3.1 SecurityFilterChain 核心配置
通过 SecurityFilterChain
Bean 定义基础安全规则:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(withDefaults());
return http.build();
}
等价的 DSL 写法:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests()
.anyRequest().authenticated()
.and()
.formLogin();
return http.build();
}
3.2 基于角色的访问控制
配置 /admin
接口仅允许 ADMIN
角色,/protected
接口仅允许 USER
角色:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/protected/**").hasRole("USER")
.anyRequest().authenticated()
)
.formLogin(withDefaults());
return http.build();
}
3.3 公开资源放行
对 /hello
资源跳过安全检查:
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/hello/**");
}
踩坑提示:
ignoring()
配置会完全跳过安全过滤器链,优先级高于authorizeHttpRequests()
!
4. 注解驱动的安全控制
使用 @EnableGlobalMethodSecurity
实现方法级安全:
4.1 JSR-250 注解配置
启用 JSR-250 注解支持:
@Configuration
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig {
}
在控制器方法上添加注解:
@RolesAllowed("ADMIN")
@GetMapping("/admin")
public String adminHello() {
return "Admin Access";
}
@RolesAllowed("USER")
@GetMapping("/protected")
public String userHello() {
return "User Access";
}
4.2 强制方法级安全
为避免遗漏注解导致安全漏洞,建议:
- 配置全局拒绝未注解方法访问
- 使用
@DenyAll
作为默认安全策略
4.3 公开资源处理
通过 WebSecurityCustomizer
放行 /hello/**
资源:
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/hello/**");
}
或直接在配置类中实现:
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return new WebSecurityCustomizer() {
@Override
public void customize(WebSecurity web) {
web.ignoring().requestMatchers("/hello/**");
}
};
}
5. 安全配置测试
5.1 Web 请求测试
使用 @TestRestTemplate
测试:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebSecurityTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void whenAccessPublicResource_thenSuccess() {
ResponseEntity<String> response = restTemplate.getForEntity("/hello", String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
@Test
public void whenAccessProtectedResourceWithoutAuth_thenForbidden() {
ResponseEntity<String> response = restTemplate.getForEntity("/protected", String.class);
assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode());
}
}
5.2 注解驱动测试
直接注入控制器测试:
@SpringBootTest
public class AnnotationSecurityTest {
@Autowired
private AnnotationSecuredController controller;
@Test
@WithAnonymousUser
public void whenCallPublicMethod_thenSuccess() {
String response = controller.publicHello();
assertEquals("Public Access", response);
}
@Test
@WithMockUser(roles = "USER")
public void whenCallUserMethodWithUserRole_thenSuccess() {
String response = controller.userHello();
assertEquals("User Access", response);
}
@Test
@WithMockUser(roles = "ADMIN")
public void whenCallUserMethodWithAdminRole_thenForbidden() {
assertThrows(AccessDeniedException.class, () -> controller.userHello());
}
}
注意:此测试方式仅适用于注解驱动的安全方案!
6. 注解使用注意事项
6.1 同类方法间接调用
当未注解方法调用同类注解方法时,安全检查会被绕过:
@GetMapping("/indirect")
public String indirectJsr250Hello() {
return jsr250Hello(); // 绕过安全检查!
}
@RolesAllowed("USER")
public String jsr250Hello() {
return "Secured Hello";
}
测试验证:
@Test
@WithAnonymousUser
public void whenCallIndirectMethod_thenBypassSecurity() {
String response = controller.indirectJsr250Hello();
assertEquals("Secured Hello", response); // 匿名用户也能访问!
}
6.2 跨类方法调用
调用其他类的注解方法时,安全检查正常生效:
@Service
public class DifferentClass {
@RolesAllowed("USER")
public String differentJsr250Hello() {
return "Different Secured Hello";
}
}
@RestController
public class AnnotationSecuredController {
@Autowired
private DifferentClass differentClass;
@GetMapping("/different")
public String differentClassHello() {
return differentClass.differentJsr250Hello();
}
}
测试验证:
@Test
@WithAnonymousUser
public void whenCallDifferentClassMethod_thenSecurityEnforced() {
assertThrows(AccessDeniedException.class, () -> controller.differentClassHello());
}
6.3 配置陷阱
错误配置会导致注解失效:
// 错误示例:启用 prePostEnabled 但使用 JSR-250 注解
@EnableGlobalMethodSecurity(prePostEnabled = true) // jsr250Enabled 未启用!
public class InvalidConfig {
}
正确配置多注解支持:
@EnableGlobalMethodSecurity(
jsr250Enabled = true,
prePostEnabled = true
)
public class MultiAnnotationConfig {
}
7. 进阶安全方案
当基础注解无法满足需求时:
- 使用 Spring 方法安全 + SpEL 表达式:
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize("hasRole('ADMIN') or #user.id == authentication.principal.id") public void updateUser(User user) { ... }
- 基于域对象的 ACL(访问控制列表)
- 响应式应用改用
@EnableWebFluxSecurity
和@EnableReactiveMethodSecurity
8. 总结
本文对比了两种 Spring Boot 安全配置方案:
- 集中式配置:通过
@EnableWebSecurity
在SecurityFilterChain
中统一管理规则 - 注解驱动:通过
@EnableGlobalMethodSecurity
实现方法级精细化控制
关键选择建议:
- 需要路径级控制 → 选
@EnableWebSecurity
- 需要方法级控制 → 选
@EnableGlobalMethodSecurity
- 混合使用时注意配置顺序与优先级
完整示例代码请参考 GitHub 仓库。