1. 概述

Spring Security允许通过扩展WebSecurityConfigurerAdapter类自定义HTTP安全功能,如端点授权或身份验证管理器配置。然而,在最近的版本中,Spring弃用了这种做法,转而鼓励基于组件的安全配置。

在本教程中,我们将学习如何在Spring Boot应用中替换这种弃用,并运行一些MVC测试。

进一步阅读:

Spring 中废弃的类

In this tutorial, we’re going to take a look at the deprecated classes in Spring and Spring Boot and explain what these have been replaced with.

Spring Security 5中的默认密码编码器

In Spring Security 4, it was possible to store passwords in plain text using in-memory authentication.

警告:"WebMvcConfigurerAdapter 类型已过时

In this quick tutorial, we’ll have a look at one of the warnings we may see when working with a Spring 5.x.x version, namely the one referring to the deprecated WebMvcConfigurerAdapter class.

2. 不使用WebSecurityConfigurerAdapter的Spring Security

我们通常会看到Spring的HTTP安全配置类,它们继承自WebSecurityConfigurerAdapter类。

但从Spring 5.7.0-M2版本开始,Spring弃用了WebSecurityConfigurerAdapter的使用,并建议创建不依赖它的配置。

让我们通过内存身份验证创建一个示例Spring Boot应用,展示这种新型配置。

首先,我们定义配置类:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {

    // config

}

我们将添加方法安全注解,以便根据不同的角色进行处理。

2.1. 配置身份验证

使用WebSecurityConfigurerAdapter时,我们会使用AuthenticationManagerBuilder设置我们的身份验证上下文。

现在,如果我们想要避免弃用,我们可以定义一个UserDetailsManagerUserDetailsService组件:

@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;
}

或者,考虑到我们的UserDetailsService,我们甚至可以设置一个AuthenticationManager

@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, UserDetailService userDetailService) 
  throws Exception {
    return http.getSharedObject(AuthenticationManagerBuilder.class)
      .userDetailsService(userDetailsService)
      .passwordEncoder(bCryptPasswordEncoder)
      .and()
      .build();
}

对于使用JDBC或LDAP的身份验证,这同样适用。

2.2. 配置HTTP安全

更重要的是,如果我们想要避免HTTP安全方面的弃用,我们可以创建一个SecurityFilterChain bean。

例如,假设我们想要根据角色保护端点,并只为登录保留匿名入口点。同时,我们将限制任何删除请求到管理员角色。我们将使用基本身份验证:

@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();
}

HTTP安全将构建一个DefaultSecurityFilterChain对象来加载请求匹配器和过滤器。

2.3. 配置Web安全

现在,我们可以使用回调接口WebSecurityCustomizer来进行Web安全配置。

我们将添加调试级别并忽略像图片或脚本这样的路径:

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
    return web -> web.debug(securityDebug).ignoring().requestMatchers("/css/**", "/js/**", "/img/**", "/lib/**", "/favicon.ico");
}

3. 端点控制器

现在,我们将为我们的应用定义一个简单的REST控制器类:

@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;
    }
}

正如我们在定义HTTP安全时提到的那样,我们将添加一个对任何人都可访问的通用/login端点,特定于管理员和用户的端点,以及一个不受角色保护但仍然需要身份验证的/all端点。

4. 测试端点

现在,我们将新的配置添加到MVC模拟器中的Spring Boot测试中,以测试我们的端点。

4.1. 测试匿名用户

匿名用户可以访问/login端点。如果他们尝试访问其他内容,他们将被拒绝(401):

@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());
}

此外,除了/login端点外,所有端点始终需要身份验证,如/all端点。

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"))
      .andExpect(status().isForbidden());
}

值得注意的是,如果用户角色尝试访问受管理员权限保护的端点,用户将收到“禁止”(403)错误。

相反,没有凭据的人,如上一个示例中的匿名用户,将收到“未经授权”错误(401)。

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. 总结

在这篇文章中,我们学习了如何在不使用WebSecurityConfigurerAdapter的情况下创建Spring Security配置,并在创建身份验证、HTTP安全和Web安全组件的同时替换它。

如往常一样,可以在GitHub上的工作代码示例中找到完整实现。