1. 概述
Spring Security 允許自定義 HTTP 安全功能,例如端點授權或身份驗證管理器配置,通過擴展 WebSecurityConfigurerAdapter 類。但是,在最近的版本中,Spring 已棄用此方法,並鼓勵基於組件的安全性配置。
在本教程中,我們將學習如何在 Spring Boot 應用程序中替換此棄用方法,並運行一些 MVC 測試。
2. Spring Security Without the WebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter and suggests creating configurations without it.
Let’s create an example Spring Boot application using in-memory authentication to show this new type of configuration.
First, we’ll define our configuration class:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {
// config
}
We’ll add method security annotations to enable processing based on different roles.
2.1. Configure Authentication
With the WebSecurityConfigurerAdapter, we’ll use an AuthenticationManagerBuilder to set our authentication context.
@Bean
public UserDetailsService userDetailsService(BCryptPasswordEncoder bCryptPasswordEncoder) {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("user")
.password(bCryptPasswordEncoder.encode("userPass"))
.roles("USER")
.build());
manager.createUser(User.withUsername("admin")
.password(bCryptPasswordEncoder.encode("adminPass"))
.roles("USER", "ADMIN")
.build());
return manager;
}
Or, given our UserDetailService, we can even set an AuthenticationManager: Similarly, this will work if we use JDBC or LDAP authentication. @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
authorizationManagerRequestMatcherRegistry.requestMatchers(HttpMethod.DELETE).hasRole("ADMIN")
.requestMatchers("/admin/**").hasAnyRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/login/**").permitAll()
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
2.3. Configure Web Security
For Web security, we can now use the callback interface WebSecurityCustomizer. We’ll add a debug level and ignore some paths, like images or scripts: 現在,我們將為我們的應用程序定義一個簡單的 REST 控制器類: 正如我們之前提到的,在定義 HTTP 安全性時,我們將添加一個通用的 /login 終點,供任何人訪問,以及針對管理員和用户的特定終點,以及未通過角色保護的 /all 終點,但仍然需要身份驗證。 讓我們使用 MVC 模擬測試,將新的配置添加到 Spring Boot 測試中,以測試我們的端點。 匿名用户可以訪問 /login 端點。如果他們嘗試訪問其他內容,他們將被拒絕訪問 (401): 此外,對於所有端點(除了 /login),我們始終需要身份驗證,就像對於 /all 端點一樣。 用户角色可以訪問通用端點和為該角色提供的所有其他路徑: 值得注意的是,如果用户角色嘗試訪問管理端點,用户將收到“禁止”錯誤 (403)。 相反,沒有憑據的人,如前一個示例中的匿名用户,將收到“未授權”錯誤 (401)。 正如你所看到的,具有管理角色的人可以訪問任何端點: 隨着 Spring Security 最近的更新,用於 HTTP 安全配置的方法,如 csrf() 和 requiresChannel(),存在需要調整的版本以進行遷移到最新版本的 Spring Boot 的情況。 在本部分,我們將討論使用 lambda 表達式配置這些方法的更新方式。 以前使用簡單的調用 http.csrf().disable() 來設置 CSRF 保護的設置,現在已調整為 lambda 語法: 採用這種方法,我們以功能的方式指定 CSRF 配置,與最近的 Spring Security 更新保持一致。 同樣,用於強制 HTTPS 的 requiresChannel() 方法已移動到基於 lambda 的格式: 我們可以將更新後的 csrf 和 requiresChannel 配置包含在單個 SecurityFilterChain bean 中。 此設置強制 HTTPS 跨所有請求,並簡化 CSRF 的禁用: 在本文中,我們學習瞭如何創建 Spring Security 配置,而無需使用 WebSecurityConfigureAdapter,並在創建身份驗證、HTTP 安全和 Web 安全相關組件的同時,替換它。@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, UserDetailService userDetailService)
throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder)
.and()
.build();
}
2.2. Configure HTTP Security
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.debug(securityDebug).ignoring().requestMatchers("/css/**", "/js/**", "/img/**", "/lib/**", "/favicon.ico");
}
3. Endpoints 控制器
@RestController
public class ResourceController {
@GetMapping("/login")
public String loginEndpoint() {
return "Login!";
}
@GetMapping("/admin")
public String adminEndpoint() {
return "Admin!";
}
@GetMapping("/user")
public String userEndpoint() {
return "User!";
}
@GetMapping("/all")
public String allRolesEndpoint() {
return "All Roles!";
}
@DeleteMapping("/delete")
public String deleteEndpoint(@RequestBody String s) {
return "I am deleting " + s;
}
}4. 測試端點
4.1. 測試匿名用户
@Test
@WithAnonymousUser
public void whenAnonymousAccessLogin_thenOk() throws Exception {
mvc.perform(get("/login"))
.andExpect(status().isOk());
}
@Test
@WithAnonymousUser
public void whenAnonymousAccessRestrictedEndpoint_thenIsUnauthorized() throws Exception {
mvc.perform(get("/all"))
.andExpect(status().isUnauthorized());
}
4.2. 測試用户角色
@Test
@WithUserDetails()
public void whenUserAccessUserSecuredEndpoint_thenOk() throws Exception {
mvc.perform(get("/user"))
.andExpect(status().isOk());
}
@Test
@WithUserDetails()
public void whenUserAccessRestrictedEndpoint_thenOk() throws Exception {
mvc.perform(get("/all"))
.andExpect(status().isOk());
}
@Test
@WithUserDetails()
public void whenUserAccessAdminSecuredEndpoint_thenIsForbidden() throws Exception {
mvc.perform(get("/admin"))
.andExpect(status().isForbidden());
}
@Test
@WithUserDetails()
public void whenUserAccessDeleteSecuredEndpoint_thenIsForbidden() throws Exception {
mvc.perform(delete("/delete").content("{}"))
.andExpect(status().isForbidden());
}
4.3. 測試管理角色
@Test
@WithUserDetails(value = "admin")
public void whenAdminAccessUserEndpoint_thenOk() throws Exception {
mvc.perform(get("/user"))
.andExpect(status().isOk());
}
@Test
@WithUserDetails(value = "admin")
public void whenAdminAccessAdminSecuredEndpoint_thenIsOk() throws Exception {
mvc.perform(get("/admin"))
.andExpect(status().isOk());
}
@Test
@WithUserDetails(value = "admin")
public void whenAdminAccessDeleteSecuredEndpoint_thenIsOk() throws Exception {
mvc.perform(delete("/delete").content("{}"))
.andExpect(status().isOk());
}
5. 處理已棄用的 csrf() 和 requiresChannel()
http.csrf(csrf -> csrf.disable());http.requiresChannel(channel -> channel.anyRequest().requiresSecure());@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf->csrf.disable())
.requiresChannel(channel -> channel.anyRequest().requiresSecure())
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
uthorizationManagerRequestMatcherRegistry.requestMatchers(HttpMethod.DELETE).hasRole("ADMIN")
.requestMatchers("/admin/**").hasAnyRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/login/**").permitAll()
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.sessionManagement(httpSecuritySessionManagementConfigurer ->
httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}6. 結論