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 安全配置方案:

  1. 集中式配置:通过 @EnableWebSecuritySecurityFilterChain 中统一管理规则
  2. 注解驱动:通过 @EnableGlobalMethodSecurity 实现方法级精细化控制

关键选择建议:

  • 需要路径级控制 → 选 @EnableWebSecurity
  • 需要方法级控制 → 选 @EnableGlobalMethodSecurity
  • 混合使用时注意配置顺序与优先级

完整示例代码请参考 GitHub 仓库


原始标题:Spring @EnableWebSecurity vs. @EnableGlobalMethodSecurity