1. 概述

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

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

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上的工作代码示例中找到完整实现。