Spring Security 中的 @EnableWebSecurity 和 @EnableGlobalMethodSecurity

1、概览

有时我们需要在 Spring Boot 应用的不同路径上应用多个 Security Filter。

本文将带你了解在 Spring Scurity 中自定义 Security 的两种方法 - 通过使用 @EnableWebSecurity@EnableGlobalMethodSecurity

本文通过一个简单的应用示例来说明这两者的区别。该应用包含一些管理员(ADMIN)才能访问的资源和一些只有认证了的用户(USER)才能访问的资源以及一些任何人都可以访问、下载的公共资源。

2、Spring Boot 整合 Spring Security

2.1、Maven 依赖

无论采用哪种方法,都需要添加 Spring Boot Stater 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2.2、Spring Boot 自动配置

当 classpath 上存在 Spring Security 时,Spring Boot Security Auto-Configuration 的 WebSecurityEnablerConfiguration 就会激活 @EnableWebSecurity。这会在应用中加载默认的安全配置。

默认的安全配置会激活 HTTP Security Filter 和 Security Filter Chain,并对端点应用 Basic Authentication 认证。

3、保护端点

第一种方式,创建一个 MySecurityConfigurer 类,使用 @EnableWebSecurity 对其进行注解。

@EnableWebSecurity
public class MySecurityConfigurer {
}

3.1、快速了解 SecurityFilterChain Bean

首先,注册 SecurityFilterChain Bean:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
    http.formLogin();
    http.httpBasic();
    return http.build();
}

如上,可以看到接收到的任何请求都需要进行身份认证,并且使用基本的表单登录来提示输入凭证。

当使用 HttpSecurity DSL 时,可以用如下写法:

http.authorizeRequests().anyRequest().authenticated()
  .and().formLogin()
  .and().httpBasic()

3.2、要求用户拥有适当的角色

现在,配置安全机制,只允许具有 ADMIN(管理员)角色的用户访问 /admin 端点。以及只允许 USER 角色的用户访问 /protected 端点。

为此,创建 SecurityFilterChain Bean:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/**")
        .hasRole("ADMIN")
        .antMatchers("/protected/**")
        .hasRole("USER");
    return http.build();
}

3.3、公共资源

通过 WebSecurity 进行配置,不对公共资源 /hello 进行身份认证。

注册一个 WebSecurityCustomizer Bean:

@Bean
public WebSecurityCustomizer ignoreResources() {
    return (webSecurity) -> webSecurity
      .ignoring()
      .antMatchers("/hello/*");
}

4、使用注解保护端点

可以通过 @EnableGlobalMethodSecurity 来应用基于注解驱动的安全设置。

4.1、通过 Security 注解要求用户具有适当的角色

在方法上使用注解来配置 Security,只允许 ADMIN 用户访问 /admin 端点,并允许 USER 用户访问 /protected 端点。

EnableGlobalMethodSecurity 注解中设置 jsr250Enabled=true 来启用 JSR-250 注解:

@EnableGlobalMethodSecurity(jsr250Enabled = true)
@Controller
public class AnnotationSecuredController {
    @RolesAllowed("ADMIN")
    @RequestMapping("/admin")
    public String adminHello() {
        return "Hello Admin";
    }

    @RolesAllowed("USER")
    @RequestMapping("/protected")
    public String jsr250Hello() {
        return "Hello Jsr250";
    }
}

4.2、确保所有公共方法的安全

当使用基于注解的安全保护时,可能会忘记对方法进行注解。这会在无意中造成安全漏洞。

为了防止这种情况,应该拒绝访问所有没有授权(Authorization)注解的方法。

4.3、允许访问公共资源

无论我们是否添加了基于角色的安全保护,Spring 的默认 Security 都会对的所有端点进行身份认证。

上述示例对 /admin/protected 端点应用了安全保护,同时仍希望允许访问 /hello 中的文件资源。

虽然可以再次通过继承 WebSecurityAdapter 实现,但 Spring 提供了一个更简单的选择。

使用注解保护方法后,现在可以添加 WebSecurityCustomizer 实现来开放 /hello/* 资源:

public class MyPublicPermitter implements WebSecurityCustomizer {
    public void customize(WebSecurity webSecurity) {
        webSecurity.ignoring()
            .antMatchers("/hello/*");
    }
}

或者,也可以在配置类中创建一个实现它的 Bean:

@Configuration
public class MyWebConfig {
    @Bean
    public WebSecurityCustomizer ignoreResources() {
        return (webSecurity) -> webSecurity
          .ignoring()
          .antMatchers("/hello/*");
    }
}

Spring Security 初始化时,会调用找到的任何 WebSecurityCustomizer,包括我们定义的的。

5、测试

安全配置完毕后,测试是否按照预期运行。

根据我们选择的安全保护方式,自动化测试有一种或两种选择。可以向应用发送 Web 请求,或者直接调用 Controller 方法。

5.1、通过 Web 请求进行测试

创建一个@SpringBootTest 测试类,注入 TestRestTemplate

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class WebSecuritySpringBootIntegrationTest {
    @Autowired
    private TestRestTemplate template;
}

添加一个测试,测试公共资源是否可访问:

@Test
public void givenPublicResource_whenGetViaWeb_thenOk() {
    ResponseEntity<String> result = template.getForEntity("/hello/baeldung.txt", String.class);
    assertEquals("Hello From Baeldung", result.getBody());
}

也可以尝试访问受保护的资源:

@Test
public void whenGetProtectedViaWeb_thenForbidden() {
    ResponseEntity<String> result = template.getForEntity("/protected", String.class);
    assertEquals(HttpStatus.FORBIDDEN, result.getStatusCode());
}

这里,会得到 FORBIDDEN 响应,因为匿名请求不具备所需的角色。

无论选择哪种方法,都可以用这种方法来测试应用的安全。

5.2、通过自动装配和注解进行测试

创建 @SpringBootTest 测试类,并注入 AnnotationSecuredController

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class GlobalMethodSpringBootIntegrationTest {
    @Autowired
    private AnnotationSecuredController api;
}

首先,使用 @WithAnonymousUser 测试公开的方法:

@Test
@WithAnonymousUser
public void givenAnonymousUser_whenPublic_thenOk() {
    assertThat(api.publicHello()).isEqualTo(HELLO_PUBLIC);
}

接着,使用 @WithMockUser 注解来访问受保护的方法。

首先,使用具有 USER 角色的用户来测试受 JSR-250 注解保护方法:

@WithMockUser(username="baeldung", roles = "USER")
@Test
public void givenUserWithRole_whenJsr250_thenOk() {
    assertThat(api.jsr250Hello()).isEqualTo("Hello Jsr250");
}

然后,尝试在用户没有正确角色的情况下访问相同的方法:

@WithMockUser(username="baeldung", roles = "NOT-USER")
@Test(expected = AccessDeniedException.class)
public void givenWrongRole_whenJsr250_thenAccessDenied() {
    api.jsr250Hello();
}

Spring Security 拦截了请求,并抛出了 AccessDeniedException

只有在选择基于注解的安全保护时才能使用这种测试方法。

6、注意事项

当选择基于方法注解的安全保护时,有一些要点需要注意。

只有当通过 public 方法进入一个类时,才会应用注解的安全保护。

6.1、间接调用

之前,调用注解方法时,可以看到其安全设置生效。

现在让我们在同一个类中创建一个 public 方法,但不带安全注解,让它调用注解的 jsr250Hello 方法:

@GetMapping("/indirect")
public String indirectHello() {
    return jsr250Hello();
}

现在,使用匿名访问调用的 /indirect 端点:

@Test
@WithAnonymousUser
public void givenAnonymousUser_whenIndirectCall_thenNoSecurity() {
    assertThat(api.indirectHello()).isEqualTo(HELLO_JSR_250);
}

测试通过,因为 “受保护方法” (jsr250Hello)上的安全设置未生效。换句话说,在同一类中内部直接调用 “受保护的方法”,会导致其安全设置失效。

这涉及到 Spring 的动态代理机制。

6.2、不同类中的间接调用

现在,来看看在没有安全设置的方法中,调用有安全注解的方法。安全设置是否会生效。

首先,创建一个 DifferentClass 类,带有注解方法 differentJsr250Hello

@Component
public class DifferentClass {
    @RolesAllowed("USER")
    public String differentJsr250Hello() {
        return "Hello Jsr250";
    }
}

现在,在 Controller 中注入 DifferentClass,并添加一个不受保护的 differentClassHello 方法来调用它。

@Autowired
DifferentClass differentClass;

@GetMapping("/differentclass")
public String differentClassHello() {
    return differentClass.differentJsr250Hello();
}

最后,测试一下调用,看看安全设置是否生效:

@Test(expected = AccessDeniedException.class)
@WithAnonymousUser
public void givenAnonymousUser_whenIndirectToDifferentClass_thenAccessDenied() {
    api.differentClassHello();
}

所以,你可以看到,在同类中直接调用受注解保护的方法,安全设置不会生效。只有从其他类中调用时,才会起作用。

6.3、最后一点注意事项

必须确保正确配置了 @EnableGlobalMethodSecurity 注解。如果没有正确配置,尽管使用了 Security 注解,它们可能根本没有任何效果。

例如,如果使用 JSR-250 注解,但没有指定 jsr250Enabled=true,而是指定了 prePostEnabled=true,那么 JSR-250 注解将不起任何作用!

@EnableGlobalMethodSecurity(prePostEnabled = true)

当然,可以在 @EnableGlobalMethodSecurity 注解中同时添加两种注解类型,表示要使用到两种注解:

@EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true)

7、需要更高级的授权场景

与 JSR-250 相比,还可以使用 Spring Method Security。这包括使用功能更加强大的 Spring Security Expression(SpEL)来实现更高级的授权场景。可以通过设置 prePostEnabled=trueEnableGlobalMethodSecurity 注解中启用 SpEL:

@EnableGlobalMethodSecurity(prePostEnabled = true)

外,当希望根据 Domain 对象是否由用户拥有来强制执行安全校验时,可以使用 Spring Security 访问控制列表(ACL)

在响应式应用中,使用 @EnableWebFluxSecurity@EnableReactiveMethodSecurity 来代替。

8、总结

本文介绍了如何在 Spring Security 中通过 @EnableWebSecurity@EnableGlobalMethodSecurity 这两种不同的方式来实现基于路由和方法的认证授权。


Ref:https://www.baeldung.com/spring-enablewebsecurity-vs-enableglobalmethodsecurity