如何在@WebMvcTest 测试中忽略@EnableWebSecurity 注释类

Posted

技术标签:

【中文标题】如何在@WebMvcTest 测试中忽略@EnableWebSecurity 注释类【英文标题】:How to ignore @EnableWebSecurity annotated class in @WebMvcTest tests 【发布时间】:2020-04-29 11:29:30 【问题描述】:

在下面的测试类中,我不希望 @EnableWebSecurity 注解的类被 Spring Context 捕获:

@WebMvcTest(controllers = UserController.class)
class UserControllerTest 

    @MockBean
    private UserService userService;

    @Autowired
    private ObjectMapper jsonMapper;

    @Autowired
    private MockMvc mockMvc;

    @Test
    void create_should_return_registered_user_when_request_is_valid() throws Exception 
        // given
        final String EMAIL = "test@test.com";
        final String PASSWORD = "test_password";
        final UserDto userDto = buildDto(EMAIL, PASSWORD);
        final User expectedUser = buildUser(EMAIL, PASSWORD);

        // when
        when(userService.registerUser(userDto)).thenReturn(expectedUser);

        // then
        MvcResult response = mockMvc.perform(post(UserAPI.BASE_URL)
                .contentType(MediaType.APPLICATION_JSON)
                .content(jsonMapper.writeValueAsString(userDto)))
                .andExpect(status().isCreated())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andReturn();

        String responseBodyJson = response.getResponse().getContentAsString();
        User responseUser = jsonMapper.readValue(responseBodyJson, User.class);

        assertThat(responseUser, is(equalTo(expectedUser)));
        verify(userService, times(1)).registerUser(userDto);
        verifyNoMoreInteractions(userService);
    

    @Test
    void create_should_return_conflict_when_request_valid_but_email_in_use() throws Exception 
        // given
        final String EMAIL = "test@test.com";
        final String PASSWORD = "test_password";
        final UserDto userDto = buildDto(EMAIL, PASSWORD);

        // when
        when(userService.registerUser(userDto)).thenThrow(new EmailAlreadyInUseException(EMAIL));

        // then
        mockMvc.perform(post(UserAPI.BASE_URL)
                .contentType(MediaType.APPLICATION_JSON)
                .content(jsonMapper.writeValueAsString(userDto)))
                .andExpect(status().isConflict());

        verify(userService, times(1)).registerUser(userDto);
        verifyNoMoreInteractions(userService);
    

    @Test
    void create_should_return_bad_request_when_request_has_invalid_email() throws Exception 
        // given
        final String BAD_EMAIL = "test_test.com";
        final String PASSWORD = "test_password";
        final UserDto userDto = buildDto(BAD_EMAIL, PASSWORD);

        // when

        // then
        mockMvc.perform(post(UserAPI.BASE_URL)
                .contentType(MediaType.APPLICATION_JSON)
                .content(jsonMapper.writeValueAsString(userDto)))
                .andExpect(status().isBadRequest());

        verifyNoInteractions(userService);
    

    @Test
    void create_should_return_bad_request_when_request_has_invalid_password() throws Exception 
        // given
        final String EMAIL = "test@test.com";
        final String BAD_PASSWORD = "";
        final UserDto userDto = buildDto(EMAIL, BAD_PASSWORD);

        // when

        // then
        mockMvc.perform(post(UserAPI.BASE_URL)
                .contentType(MediaType.APPLICATION_JSON)
                .content(jsonMapper.writeValueAsString(userDto)))
                .andExpect(status().isBadRequest());

        verifyNoInteractions(userService);
    

    @Test
    void create_should_return_bad_request_when_request_is_missing_email() throws Exception 
        // given
        final String PASSWORD = "test_password";
        final UserDto userDto = buildDto(null, PASSWORD);

        // when

        // then
        mockMvc.perform(post(UserAPI.BASE_URL)
                .contentType(MediaType.APPLICATION_JSON)
                .content(jsonMapper.writeValueAsString(userDto)))
                .andExpect(status().isBadRequest());

        verifyNoInteractions(userService);
    

    @Test
    void create_should_return_bad_request_when_request_is_missing_password() throws Exception 
        // given
        final String EMAIL = "test@test.com";
        final UserDto userDto = buildDto(EMAIL, null);

        // when

        // then
        mockMvc.perform(post(UserAPI.BASE_URL)
                .contentType(MediaType.APPLICATION_JSON)
                .content(jsonMapper.writeValueAsString(userDto)))
                .andExpect(status().isBadRequest());

        verifyNoInteractions(userService);
    

    private UserDto buildDto(String email, String password) 
        UserDto userDto = new UserDto();
        userDto.setEmail(email);
        userDto.setPassword(password);
        return userDto;
    

    private User buildUser(String email, String password)
        User user = new User();
        user.setId(1);
        user.setEmail(email);
        user.setPassword(password);
        return user;
    


现在默认加载,因为它的依赖没有加载,所以报错:

com.example.ordersapi.auth.configuration.SecurityConfiguration 中构造函数的参数 0 需要一个无法找到的 'org.springframework.security.core.userdetails.UserDetailsS​​ervice' 类型的 bean。

我见过一些解决方案,例如 @WebMvcTest(controllers = SomeController.class, secure = false),但这些似乎已被弃用。

我正在运行 Spring Boot v2.2.2.RELEASE。


这是安全配置类:

@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter 

    @Value("$spring.h2.console.enabled:false")
    private boolean h2ConsoleEnabled;

    private final UserDetailsService userDetailsService;
    private final AuthorizationFilter authorizationFilter;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        if (h2ConsoleEnabled) 
            http.authorizeRequests()
                    .antMatchers("/h2-console", "/h2-console/**").permitAll()
                    .and()
                    .headers().frameOptions().sameOrigin();
        

        http.cors().and().csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint(unauthorizedHandler())
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, AuthenticationAPI.BASE_URL).permitAll()
                .anyRequest().authenticated();

        http.addFilterBefore(authorizationFilter, UsernamePasswordAuthenticationFilter.class);
    

    private AuthenticationEntryPoint unauthorizedHandler() 
        return (request, response, e) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    

    /**
     * We have to create this bean otherwise we can't wire AuthenticationManager in our code.
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception 
        return super.authenticationManagerBean();
    

    @Bean
    public PasswordEncoder passwordEncoder() 
        return new BCryptPasswordEncoder();
    


【问题讨论】:

你能分享你的 com.example.ordersapi.auth.configuration.SecurityConfiguration 吗?谢谢 已添加到问题中。谢谢! 您是否创建了自己的UserDetailsService 实现?如果有,请提供。你很接近,没有错过太多。 嗨,我确实尝试过提供它,它也是依赖项,但之后我遇到了另一个问题:spring security is up,显然没有使用我的配置/服务,即使它们被加载到上下文,当我尝试使用 @WithMockService 时,我仍然得到 403 Forbidden 作为响应状态 它应该是……奇怪。请分享它,以便我可以尝试复制...谢谢 【参考方案1】:

可以使用@Configuration 和@EnableWebSecurity 属性覆盖安全配置。因为您没有使用@TestConfiguration,所以您可能需要使用@Import 导入类,如下所示。我喜欢这个解决方案,而不是扫描你的主机包中的 bean,因为我觉得你可以更好地控制框架正在加载的内容。

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = MyController.class)
@Import(MyController.class)
public class MyControlleTests 

    @Autowired
    private MockMvc mvc;

    @MockBean
    private SomeDependency someDependencyNeeded;

    @Configuration
    @EnableWebSecurity
    static class SecurityConfig extends WebSecurityConfigurerAdapter 

        @Override
        protected void configure(HttpSecurity http) throws Exception
        
            http
                    .csrf().disable()
                    .authorizeRequests().anyRequest().anonymous();
        
    

    @Test
    public void some_route_returns_ok() throws Exception 

        MockHttpServletRequestBuilder requestBuilder =
                MockMvcRequestBuilders.get("mycontrolleraction");

        mvc
                .perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isOk());

    

请注意,有人可能会争辩说您应该只将安全性作为测试的一部分;但是,我的意见是,您应该尽可能隔离地测试架构的每个组件。

【讨论】:

【参考方案2】:

在将 Spring Boot 从 2.1.x 迁移到 2.2.x 后,我也遇到了同样的问题,即 401 代码。此后,secure 字段从@WebMvcTest 注释中移除。

我通过添加忽略过滤器(包括身份验证过滤器)的注释来修复:

@WebMvcTest(value = SomeResource.class)
@AutoConfigureMockMvc(addFilters = false)
class SomeTest  

【讨论】:

【参考方案3】:

我找到的最简单的解决方案是在您的 SecurityConfiguration 类中添加 @Profile(!test)。这应该完全防止在测试期间加载类。 默认情况下,测试使用测试配置文件运行,如果您要覆盖您可能必须放入您正在使用的配置文件。 (日志显示上下文启动时哪个配置文件处于活动状态)。 有关个人资料的更多信息:https://www.baeldung.com/spring-profiles。

您也可以使用@WithMockUser(roles = "MANAGER")。有关更多信息,请参阅此问题:Spring Test & Security: How to mock authentication?

【讨论】:

嗨!感谢您的回答我还尝试为我的配置和所有其他安全 bean 提供另一个配置文件,它解决了当前的问题,但即使仍然会设置网络安全(使用默认值),当我尝试使用 @WithMockUser 我仍然得到 403 Forbbiden MvcResult response = mockMvc.perform(post(UserAPI.BASE_URL).with(csrf())... 让它通过。这很糟糕,因为我不希望安全问题污染这些测试套房 仅供参考,解决方案在这里:***.com/questions/59776298/…

以上是关于如何在@WebMvcTest 测试中忽略@EnableWebSecurity 注释类的主要内容,如果未能解决你的问题,请参考以下文章

如何将 @WebMvcTest 用于单元测试 POST 方法?

在 Spring Boot 1.4 MVC 测试中使用 @WebMvcTest 设置 MockMvc

使用 @WebMvcTest 测试中的 ApplicationContext 异常

在 Spring Boot 测试类上使用 @WebMvcTest 注释时出错

@WebMvcTest 在 Spring Boot 测试中为不同服务提供“Error Creating bean with name”错误

SpringBoot @WebMvcTest,自动装配 RestTemplateBuilder