1、概览 {#1概览}
有时我们需要在 Spring Boot 应用的不同路径上应用多个 Security Filter。
本文将带你了解在 Spring Scurity 中自定义 Security 的两种方法 - 通过使用 @EnableWebSecurity
和 @EnableGlobalMethodSecurity
。
本文通过一个简单的应用示例来说明这两者的区别。该应用包含一些管理员(ADMIN)才能访问的资源和一些只有认证了的用户(USER
)才能访问的资源以及一些任何人都可以访问、下载的公共资源。
2、Spring Boot 整合 Spring Security {#2spring-boot-整合-spring-security}
2.1、Maven 依赖 {#21maven-依赖}
无论采用哪种方法,都需要添加 Spring Boot Stater 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.2、Spring Boot 自动配置 {#22spring-boot-自动配置}
当 classpath 上存在 Spring Security 时,Spring Boot Security Auto-Configuration 的 WebSecurityEnablerConfiguration
就会激活 @EnableWebSecurity
。这会在应用中加载默认的安全配置。
默认的安全配置会激活 HTTP Security Filter 和 Security Filter Chain,并对端点应用 Basic Authentication 认证。
3、保护端点 {#3保护端点}
第一种方式,创建一个 MySecurityConfigurer
类,使用 @EnableWebSecurity
对其进行注解。
@EnableWebSecurity
public class MySecurityConfigurer {
}
3.1、快速了解 SecurityFilterChain Bean {#31快速了解-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、要求用户拥有适当的角色 {#32要求用户拥有适当的角色}
现在,配置安全机制,只允许具有 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、公共资源 {#33公共资源}
通过 WebSecurity
进行配置,不对公共资源 /hello
进行身份认证。
注册一个 WebSecurityCustomizer
Bean:
@Bean
public WebSecurityCustomizer ignoreResources() {
return (webSecurity) -> webSecurity
.ignoring()
.antMatchers("/hello/*");
}
4、使用注解保护端点 {#4使用注解保护端点}
可以通过 @EnableGlobalMethodSecurity
来应用基于注解驱动的安全设置。
4.1、通过 Security 注解要求用户具有适当的角色 {#41通过-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、确保所有公共方法的安全 {#42确保所有公共方法的安全}
当使用基于注解的安全保护时,可能会忘记对方法进行注解。这会在无意中造成安全漏洞。
为了防止这种情况,应该拒绝访问所有没有授权(Authorization)注解的方法。
4.3、允许访问公共资源 {#43允许访问公共资源}
无论我们是否添加了基于角色的安全保护,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、测试 {#5测试}
安全配置完毕后,测试是否按照预期运行。
根据我们选择的安全保护方式,自动化测试有一种或两种选择。可以向应用发送 Web 请求,或者直接调用 Controller 方法。
5.1、通过 Web 请求进行测试 {#51通过-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、通过自动装配和注解进行测试 {#52通过自动装配和注解进行测试}
创建 @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、注意事项 {#6注意事项}
当选择基于方法注解的安全保护时,有一些要点需要注意。
只有当通过 public
方法进入一个类时,才会应用注解的安全保护。
6.1、间接调用 {#61间接调用}
之前,调用注解方法时,可以看到其安全设置生效。
现在让我们在同一个类中创建一个 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、不同类中的间接调用 {#62不同类中的间接调用}
现在,来看看在没有安全设置的方法中,调用有安全注解的方法。安全设置是否会生效。
首先,创建一个 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、最后一点注意事项 {#63最后一点注意事项}
必须确保正确配置了 @EnableGlobalMethodSecurity
注解。如果没有正确配置,尽管使用了 Security 注解,它们可能根本没有任何效果。
例如,如果使用 JSR-250 注解,但没有指定 jsr250Enabled=true
,而是指定了 prePostEnabled=true
,那么 JSR-250 注解将不起任何作用!
@EnableGlobalMethodSecurity(prePostEnabled = true)
当然,可以在 @EnableGlobalMethodSecurity
注解中同时添加两种注解类型,表示要使用到两种注解:
@EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true)
7、需要更高级的授权场景 {#7需要更高级的授权场景}
与 JSR-250 相比,还可以使用 Spring Method Security。这包括使用功能更加强大的 Spring Security Expression(SpEL)来实现更高级的授权场景。可以通过设置 prePostEnabled=true
在 EnableGlobalMethodSecurity
注解中启用 SpEL:
@EnableGlobalMethodSecurity(prePostEnabled = true)
外,当希望根据 Domain 对象是否由用户拥有来强制执行安全校验时,可以使用 Spring Security 访问控制列表(ACL)。
在响应式应用中,使用 @EnableWebFluxSecurity
和 @EnableReactiveMethodSecurity
来代替。
8、总结 {#8总结}
本文介绍了如何在 Spring Security 中通过 @EnableWebSecurity
和 @EnableGlobalMethodSecurity
这两种不同的方式来实现基于路由和方法的认证授权。
Ref:https://www.baeldung.com/spring-enablewebsecurity-vs-enableglobalmethodsecurity