web-dev-qa-db-fra.com

Spring Test renvoyant 401 pour les URL non sécurisées

J'utilise Spring pour les tests MVC

Voici ma classe de test

@RunWith(SpringRunner.class)
@WebMvcTest
public class ITIndexController {

    @Autowired
    WebApplicationContext context;

    MockMvc mockMvc;

    @MockBean
    UserRegistrationApplicationService userRegistrationApplicationService;

    @Before
    public void setUp() {
        this.mockMvc = MockMvcBuilders
                        .webAppContextSetup(context)
                        .apply(springSecurity())
                        .build();
    }

    @Test
    public void should_render_index() throws Exception {
        mockMvc.perform(get("/"))
            .andExpect(status().isOk())
            .andExpect(view().name("index"))
            .andExpect(content().string(containsString("Login")));
    }
}

Voici la configuration MVC

@Configuration
@EnableWebMvc
public class MvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/login/form").setViewName("login");
    }
}

Voici la configuration de sécurité

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("customUserDetailsService")
    UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/resources/**", "/signup", "/signup/form", "/").permitAll()
            .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login/form").permitAll().loginProcessingUrl("/login").permitAll()
                .and()
            .logout().logoutSuccessUrl("/login/form?logout").permitAll()
                .and()
            .csrf().disable();
    }

    @Autowired
    public void configureGlobalFromDatabase(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }
}

Lorsque j'exécute mon test, il échoue avec le message:

Java.lang.AssertionError: Status expected:<200> but was:<401>
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.Java:54)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.Java:81)
at org.springframework.test.web.servlet.result.StatusResultMatchers$10.match(StatusResultMatchers.Java:664)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.Java:171)
at com.marco.nutri.integration.web.controller.ITIndexController.should_render_index(ITIndexController.Java:46)
at Sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at Sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.Java:62)
at Sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.Java:43)
at Java.lang.reflect.Method.invoke(Method.Java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.Java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.Java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.Java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.Java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.Java:26)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.Java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.Java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.Java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.Java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.Java:252)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.Java:94)
at org.junit.runners.ParentRunner$3.run(ParentRunner.Java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.Java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.Java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.Java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.Java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.Java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.Java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.Java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.Java:191)
at org.Eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.Java:86)
at org.Eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.Java:38)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.Java:459)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.Java:678)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.Java:382)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.Java:192)

Je comprends qu'il échoue en raison du fait que l'url est protégée par une sécurité de printemps, mais lorsque j'exécute mon application, je peux accéder à cette URL même sans être authentifié.

Est-ce que je fais quelque chose de mal?

14
Marco Prado

J'ai trouvé la réponse
Spring docs dit que:

@WebMvcTest configurera automatiquement l'infrastructure Spring MVC et limitera les beans analysés à @Controller, @ControllerAdvice, @JsonComponent, Filter, WebMvcConfigurer et HandlerMethodArgumentResolver. Les beans @Component standard ne seront pas analysés lors de l'utilisation de cette annotation.

Et selon ce problème dans github:

https://github.com/spring-projects/spring-boot/issues/5476

Le @WebMvcTest par défaut configure automatiquement la sécurité du ressort si spring-security-test est présent dans le chemin de classe (ce qui dans mon cas l'est).

Donc, puisque les classes WebSecurityConfigurer ne sont pas sélectionnées, la sécurité par défaut était en cours de configuration automatique, c'est le motif que je recevais le 401 dans les URL qui n'était pas sécurisé dans ma configuration de sécurité. La configuration automatique par défaut de Spring Security protège toutes les URL avec une authentification de base.

Ce que j'ai fait pour résoudre le problème était d'annoter la classe avec @ContextConfiguration, et @MockBean comme il est décrit dans la documentation:

Souvent, @WebMvcTest sera limité à un seul contrôleur et utilisé en combinaison avec @MockBean pour fournir des implémentations fictives aux collaborateurs requis.

Et voici la classe de test

@RunWith(SpringRunner.class)
@WebMvcTest
@ContextConfiguration(classes={Application.class, MvcConfig.class, SecurityConfig.class})
public class ITIndex {

    @Autowired
    WebApplicationContext context;

    MockMvc mockMvc;

    @MockBean
    UserRegistrationApplicationService userRegistrationApplicationService;

    @MockBean
    UserDetailsService userDetailsService;

    @Before
    public void setUp() {
        this.mockMvc = MockMvcBuilders
                        .webAppContextSetup(context)
                        .apply(springSecurity())
                        .build();
    }

    @Test
    public void should_render_index() throws Exception {
        mockMvc.perform(get("/"))
            .andExpect(status().isOk())
            .andExpect(view().name("index"))
            .andExpect(content().string(containsString("Login")));
    }
}

Application, MvcConfig et SecurityConfig sont toutes mes classes de configuration

17
Marco Prado

Je ne sais pas si cela était disponible lorsque la question d'origine a été posée, mais si vous ne voulez vraiment pas tester la partie sécurité d'une demande Web (ce qui semble raisonnable si le point de terminaison est connu pour être non sécurisé), je pense que cela pourrait être fait simplement par en utilisant l'attribut secure de @WebMvcTest annotation (la valeur par défaut est true, donc la définir sur false devrait désactiver la configuration automatique du support MockMvc de Spring Security):

@WebMvcTest(secure = false)

Plus d'informations disponibles dans le javadocs

9
Michael Damone

Si vous utilisez SpringJUnit4ClassRunner au lieu de SpringRunner, vous pouvez intercepter vos demandes dans la couche de sécurité. Si vous utilisez l'authentification de base, vous devez utiliser la méthode httpBasic dans mockMvc.perform

 mockMvc.perform(get("/").with(httpBasic(username,rightPassword))
0
mertaksu

J'ai eu un problème et résoudre le problème à l'aide des réponses ici et du commentaire de @Sam Brannen.

Vous n'avez probablement pas besoin d'utiliser @ContextConfiguration. L'ajout simple de @Import (SecurityConfig.class) devrait généralement suffire.

Pour simplifier et mettre à jour les réponses un peu plus, je veux partager comment je le corrige dans mon projet spring-boot2.

Je veux tester sous le point final.

@RestController
@Slf4j
public class SystemOptionController {

  private final SystemOptionService systemOptionService;
  private final SystemOptionMapper systemOptionMapper;

  public SystemOptionController(
      SystemOptionService systemOptionService, SystemOptionMapper systemOptionMapper) {
    this.systemOptionService = systemOptionService;
    this.systemOptionMapper = systemOptionMapper;
  }

  @PostMapping(value = "/systemoption")
  public SystemOptionDto create(@RequestBody SystemOptionRequest systemOptionRequest) {
    SystemOption systemOption =
        systemOptionService.save(
            systemOptionRequest.getOptionKey(), systemOptionRequest.getOptionValue());
    SystemOptionDto dto = systemOptionMapper.mapToSystemOptionDto(systemOption);
    return dto;
  }
}

Toutes les méthodes de service doivent être des interfaces sinon le contexte d'application ne peut pas être initialisé. Vous pouvez vérifier mon SecurityConfig.

@Configuration
@EnableWebSecurity
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private ResourceServerTokenServices resourceServerTokenServices;

    @Override
    public void configure(final HttpSecurity http) throws Exception {
        if (Application.isDev()) {
            http.csrf().disable().authorizeRequests().anyRequest().permitAll();
        } else {
            http
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .authorizeRequests().regexMatchers("/health").permitAll()
                .antMatchers("/prometheus").permitAll()
                .anyRequest().authenticated()
                    .and()
                    .authorizeRequests()
                    .anyRequest()
                    .permitAll();
            http.csrf().disable();
        }
    }

    @Override
    public void configure(final ResourceServerSecurityConfigurer resources) {
        resources.tokenServices(resourceServerTokenServices);
    }
}

Et ci-dessous, vous pouvez voir ma classe SystemOptionControllerTest.

@RunWith(SpringRunner.class)
@WebMvcTest(value = SystemOptionController.class)
@Import(SecurityConfig.class)
public class SystemOptionControllerTest {

  @Autowired private ObjectMapper mapper;

  @MockBean private SystemOptionService systemOptionService;
  @MockBean private SystemOptionMapper systemOptionMapper;
  @MockBean private ResourceServerTokenServices resourceServerTokenServices;

  private static final String OPTION_KEY = "OPTION_KEY";
  private static final String OPTION_VALUE = "OPTION_VALUE";

  @Autowired private MockMvc mockMvc;

  @Test
  public void createSystemOptionIfParametersAreValid() throws Exception {
    // given

    SystemOption systemOption =
        SystemOption.builder().optionKey(OPTION_KEY).optionValue(OPTION_VALUE).build();

    SystemOptionDto systemOptionDto =
        SystemOptionDto.builder().optionKey(OPTION_KEY).optionValue(OPTION_VALUE).build();

    SystemOptionRequest systemOptionRequest = new SystemOptionRequest();
    systemOptionRequest.setOptionKey(OPTION_KEY);
    systemOptionRequest.setOptionValue(OPTION_VALUE);
    String json = mapper.writeValueAsString(systemOptionRequest);

    // when
    when(systemOptionService.save(
            systemOptionRequest.getOptionKey(), systemOptionRequest.getOptionValue()))
        .thenReturn(systemOption);
    when(systemOptionMapper.mapToSystemOptionDto(systemOption)).thenReturn(systemOptionDto);

    // then
    this.mockMvc
        .perform(
            post("/systemoption")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json)
                .accept(MediaType.APPLICATION_JSON))
        .andDo(print())
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(content().string(containsString(OPTION_KEY)))
        .andExpect(content().string(containsString(OPTION_VALUE)));
  }
}

J'ai donc juste besoin d'ajouter @Import(SecurityConfig.class) à ma classe de test mvc.

0
erhun