복붙노트

[SPRING] 보안되지 않은 URL에 대해 401을 반환하는 Spring 테스트

SPRING

보안되지 않은 URL에 대해 401을 반환하는 Spring 테스트

MVC 테스트 용으로 Spring을 사용하고 있습니다.

여기 내 시험 수업이있다.

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

다음은 MVC 구성입니다.

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

다음은 보안 설정입니다.

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

테스트를 마칠 때 메시지와 함께 실패합니다.

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)

나는 그것이 봄 보안으로 보호된다는 사실로 인해 실패한 것으로 알고 있지만, 응용 프로그램을 실행할 때 인증받지 않아도 해당 URL에 액세스 할 수 있습니다.

내가 뭔가 잘못하고 있는거야?

해결법

  1. ==============================

    1.대답을 찾았습니다. 스프링 워드 프로세서는 다음과 같이 말한다 :

    대답을 찾았습니다. 스프링 워드 프로세서는 다음과 같이 말한다 :

    그리고 github 에서이 문제에 따르면 :

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

    기본적으로 @WebMvcTest는 spring-security-test가 클래스 경로 (내 경우에는)에 있으면 스프링 보안을 자동으로 구성합니다.

    그래서 WebSecurityConfigurer 클래스가 선택되지 않았기 때문에 기본 보안이 자동으로 구성되었습니다. 즉, 보안 설정에서 보안되지 않은 url의 401을받는 동기입니다. 스프링 보안 기본 자동 구성은 모든 URL을 기본 인증으로 보호합니다.

    이 문제를 해결하기 위해 @ContextConfiguration 클래스에 주석을 달고, @MockBean이 문서에 설명되어있는 것처럼 주석을 달았습니다.

    그리고 여기 테스트 클래스가 있습니다.

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

    응용 프로그램, MvcConfig 및 SecurityConfig는 모두 내 구성 클래스입니다.

  2. ==============================

    2.이것이 원래 질문을했을 때 사용할 수 있는지 확실하지 않지만 웹 요청의 보안 부분을 테스트하고 싶지 않은 경우 (엔드 포인트가 안전하지 않은 것으로 알려진 경우 합리적인 것처럼 보임), 이것이 간단하게 수행 할 수 있다고 생각합니다. @WebMvcTest annotation의 secure 속성을 사용한다. (기본값은 true이므로 false로 설정하면 Spring Security의 MockMvc 지원 자동 구성을 비활성화해야한다.)

    이것이 원래 질문을했을 때 사용할 수 있는지 확실하지 않지만 웹 요청의 보안 부분을 테스트하고 싶지 않은 경우 (엔드 포인트가 안전하지 않은 것으로 알려진 경우 합리적인 것처럼 보임), 이것이 간단하게 수행 할 수 있다고 생각합니다. @WebMvcTest annotation의 secure 속성을 사용한다. (기본값은 true이므로 false로 설정하면 Spring Security의 MockMvc 지원 자동 구성을 비활성화해야한다.)

    @WebMvcTest(secure = false)
    

    javadocs에서 더 많은 정보보기

  3. ==============================

    3.SpringRunner 대신 SpringJUnit4ClassRunner를 사용하면 보안 계층에서 요청을 잡을 수 있습니다. 기본 인증을 사용하는 경우 mockMvc.perform 내부에서 httpBasic 메소드를 사용해야합니다.

    SpringRunner 대신 SpringJUnit4ClassRunner를 사용하면 보안 계층에서 요청을 잡을 수 있습니다. 기본 인증을 사용하는 경우 mockMvc.perform 내부에서 httpBasic 메소드를 사용해야합니다.

     mockMvc.perform(get("/").with(httpBasic(username,rightPassword))
    
  4. ==============================

    4.몇 가지 문제가 있었고 여기와 @Sam Brannen 주석의 답변을 통해 문제를 해결했습니다.

    몇 가지 문제가 있었고 여기와 @Sam Brannen 주석의 답변을 통해 문제를 해결했습니다.

    단순화하고 대답을 조금 더 업데이트하려면 내 spring-boot2 프로젝트에서 해결 방법을 공유하고 싶습니다.

    나는 끝점 아래에서 테스트하고 싶다.

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

    모든 서비스 메소드는 인터페이스 여야합니다. 그렇지 않으면 응용 프로그램 컨텍스트를 초기화 할 수 없습니다. 내 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);
        }
    }
    

    그리고 아래에서 내 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)));
      }
    }
    

    그래서 그냥 @Import (SecurityConfig.class) 내 mvc 테스트 클래스에 추가해야합니다.

  5. from https://stackoverflow.com/questions/39554285/spring-test-returning-401-for-unsecured-urls by cc-by-sa and MIT license