1. 概述
我們可能需要在 Spring Boot 應用程序的不同路徑中應用多個安全過濾器。
在本教程中,我們將探討兩種自定義安全的方法——通過使用 EnableWebSecurity 和 EnableGlobalMethodSecurity。
為了説明這些差異,我們將使用一個包含一些管理員資源、已認證用户資源,以及我們樂於供任何人下載的公共資源的簡單應用程序。
2. Spring Boot Security
2.1. Maven 依賴項
無論我們採用哪種方法,首先都需要添加 spring boot starter for security:
<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,為我們提供支持。
這會將 Spring 的默認安全配置應用於我們的應用程序。
默認安全激活了 HTTP 安全過濾器和安全過濾器鏈,並對我們的端點應用了基本身份驗證。
3. 保護我們的端點
對於我們的首次方法,讓我們首先創建一個 MySecurityConfigurer 類,並確保用 @EnableWebSecurity 註解它。
@EnableWebSecurity
public class MySecurityConfigurer {
}
3.1. SecurityFilterChain Bean 的快速瀏覽
首先,讓我們看看如何註冊一個 SecurityFilterChain 豆:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin(Customizer.withDefaults());
http.httpBasic(Customizer.withDefaults());
return http.build();
}
這裏我們看到,我們接收到的任何請求都會被認證,並且我們有一個基本的表單登錄來提示憑據。
當我們想要使用 HttpSecurity DSL 時,我們會這樣編寫:
http.authorizeRequests(request -> request.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
3.2. 要求用户具有適當的角色
現在,讓我們配置我們的安全以僅允許具有 ADMIN 角色才能訪問我們的 /admin 端點。我們還允許僅允許具有 USER 角色才能訪問我們的 /protected 端點。
我們通過創建一個 SecurityFilterChain 豆來實現:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests(request -> request.requestMatchers(new AntPathRequestMatcher("/admin/**"))
.hasRole("ADMIN"))
.authorizeHttpRequests(request -> request.requestMatchers(new AntPathRequestMatcher("/protected/**"))
.hasRole("USER"))
.build();
}
3.3. 寬鬆地為公共資源配置安全
我們不需要對我們的公共 /hello 資源進行身份驗證,因此我們將配置 WebSecurity 不對它們執行任何操作。
就像之前一樣,讓我們註冊一個 WebSecurityCustomizer 豆:
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
.requestMatchers(new AntPathRequestMatcher("/public/*"));
}
4. 使用註解保護端點
為了使用註解驅動的方法來應用安全,我們可以使用 @EnableGlobalMethodSecurity。
4.1. 使用安全註解要求用户具有適當的角色
現在,我們使用方法註解來配置我們的安全,僅允許 ADMIN 用户訪問我們的 /admin 端點,以及 USER 用户訪問我們的 /protected 端點。
讓我們啓用 JSR-250 註解,通過在我們的 EnableGlobalMethodSecurity 註解中設置 jsr250Enabled=true 來完成:
@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. 強制所有公共方法具有安全
當我們使用註解作為實現安全的方式時,我們可能會忘記為方法添加註解。這會無意中創建一個安全漏洞。
為了防止這種情況發生,我們應該拒絕訪問沒有授權註解的所有方法。
4.3. 允許訪問公共資源
Spring 的默認安全強制執行對我們所有端點的身份驗證,無論我們是否添加了基於角色的安全。
雖然我們之前的示例將安全應用於我們的 /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. Testing Our Security
現在我們已經配置了安全,我們應該檢查它是否按預期工作。
根據我們選擇的安全方法,我們有以下一個或兩個自動測試選項。我們可以通過向我們的應用程序發送 Web 請求來測試,也可以直接調用我們的控制器方法。
5.1. Testing via Web Requests
對於第一個選項,我們將創建一個帶有 的測試類:
@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. Testing via Auto-Wiring and Annotations
現在讓我們看一下我們的第二個選項。讓我們設置一個 :
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class GlobalMethodSpringBootIntegrationTest {
@Autowired
private AnnotationSecuredController api;
}
讓我們通過使用 註解來測試我們的公開方法:
@Test
@WithAnonymousUser
public void givenAnonymousUser_whenPublic_thenOk() {
assertThat(api.publicHello()).isEqualTo(HELLO_PUBLIC);
}
現在我們已經訪問了我們的公共資源,我們使用 註解來訪問我們的受保護方法。
首先,我們使用具有“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.1. 間接方法調用
在之前,當我們調用註解方法時,我們看到我們的安全註解成功應用。現在,讓我們在同一類中創建一個公共方法,但沒有安全註解。讓我們讓它調用我們的註解的 jsr250Hello 方法:
@GetMapping("/indirect")
public String indirectHello() {
return jsr250Hello();
}
現在,讓我們僅使用匿名訪問來調用“/indirect”端點:
@Test
@WithAnonymousUser
public void givenAnonymousUser_whenIndirectCall_thenNoSecurity() {
assertThat(api.indirectHello()).isEqualTo(HELLO_JSR_250);
}
我們的測試通過,因為我們的“安全”方法在不觸發任何安全檢查的情況下被調用。換句話説,內部類中的調用不會應用任何安全檢查。
6.2. 向不同類進行間接方法調用
現在,讓我們看看當未保護的方法調用不同類中的註解方法時會發生什麼。
首先,讓我們創建一個 DifferentClass 包含一個註解方法,differentJsr250Hello:
@Component
public class DifferentClass {
@RolesAllowed("USER")
public String differentJsr250Hello() {
return "Hello Jsr250";
}
}
現在,讓我們將 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。如果沒有,即使我們有所有安全註解,它們也可能沒有任何效果。
例如,如果我們使用 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 Language (SpEL) 用於更高級的授權場景。我們可以通過設置來啓用SpEL在我們的註解中。
@EnableGlobalMethodSecurity(prePostEnabled = true)
此外,當我們想要基於用户是否擁有某個領域對象來強制執行安全時,我們可以使用Spring Security Access Control Lists。
我們也應該注意到,當我們編寫反應式應用程序時,我們使用和。
8. 結論
在本教程中,我們首先探討了如何使用集中式安全規則方法來安全我們的應用程序,使用了 @EnableWebSecurity。
然後,我們在此基礎上,配置了安全規則,將這些規則更靠近影響它們的代碼。我們通過使用 @EnableGlobalMethodSecurity 並對我們想要保護的方法進行註釋來實現的。
最後,我們介紹了另一種放鬆對公共資源的安全性的方法,這些資源不需要安全保護。