[Spring][JUnit5][Mockito] 로그인 테스트 Spring Security로 인한 삽질

jhkim·2022년 12월 23일
2

Spring

목록 보기
7/7
post-thumbnail

삽질의 과정을 쭉 써두기는 했으나,
이 모든게 다 mockMvc초기화 방식에 따른 차이였다는걸 깨달았다!
사실 안해도 되는 거였다!


//테스트 코드
@WebMvcTest(UserController.class)
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    private final JsonUtil jsonUtil = new JsonUtil();

    @MockBean
    UserService userService;

    @MockBean
    LoginService loginService;


    @Test
    @DisplayName("로그인에 성공할 경우 ResultResponse(")
    void loginTest() throws Exception {
        //given - 회원가입
        when(loginService.isValidUser(any())).thenReturn(true);
        when(userService.findByUsername(any())).thenReturn(DEFAULT_USER);
        doNothing().when(loginService).login(anyLong());

        mockMvc.perform(post("/api/v1/users/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(jsonUtil.toJsonString(USER_LOGIN_REQUEST))
                )

                .andExpect(status().isOk())
                .andExpect(MockMvcResultMatchers.content().string(jsonUtil.toJsonString(ResultResponse.of(USER_LOGIN_SUCCESS))))
                .andDo(print()); //전체 메시지 확인
                System.out.println(content());
    }
}

//로그인 컨트롤러

  @ApiOperation(value = "로그인")
  @PostMapping("/login")
  public ResponseEntity<ResultResponse> login(
      @RequestBody @Valid UserDto.LoginRequest userRequest) {
    boolean isValidUser = loginService.isValidUser(userRequest); //when().thenReturn(true)에 의해 무조건 true로 넘어감

    if (isValidUser) {
      User user = userService.findByUsername(userRequest.getUsername()); //when()으로 인해 무조건 DEFAULT_USER리턴
      loginService.login(user.getId()); //doNothing()으로 인해 넘어감
    }
    return ResponseEntity.ok(ResultResponse.of(USER_LOGIN_SUCCESS));
  }

내가 작성한 로그인 로직은
Exception이 터지지 않는 이상 무조건 200 ok로 응답하게 되어있는데,
이상하게 403에러가 떴다.

그래서 찾아본 결과
SpringSecurity를 사용할 경우, 기본적으로 csrf를 체크하기 때문에
(참고)
post요청을 보냈을 때 403 forbidden에러가 뜬다는 것

@EnableWebSecuriy를 달지 않았는데도 Security가 동작하는건가?!

@Configuration
public class SecurityConfig {

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


}

사실 security 를 넣어두긴 했지만 진짜 보안 설정을 사용하고 있는 것은 아니라,
현재 SecurityConfig는 이 상태였다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().permitAll();
    http.csrf().disable();

    
    return http.build();
  }
  

일단 csrf체크를 막기 위해서 한번 SecurityFilterChain을 넣고 @EnableWebSecurity를 달았다.
csrf.disable()을 넣었으나 여전히 403 Forbidden



왜 403 Error가 뜬 걸까?

https://kang-james.tistory.com/entry/%EB%B3%B4%EC%95%88%EC%9D%B8%EC%A6%9D-CSRF-%EB%A1%9C-%EC%9D%B8%ED%95%B4%EC%84%9C-403%EC%97%90%EB%9F%AC%EA%B0%80-%EB%B0%9C%EC%83%9D%ED%96%88%EC%9D%84-%EB%95%8C
위 링크를 참고했다.



CSRF?

그러니까 원래 security에서 비정상적인 요청을 관리하기 위해서 csrf 토큰을 사용하여,
사용자의 세션에 임의의 값을 저장하고 모든 요청마다 그 값을 포함해서 전송한다.

그리고 요청이 들어올 때 마다 백엔드에서 세션에 저장된 값과 요청으로 전송된 값이 일치하는지 검증하여 방어한다.
csrf토큰이 없었기에 403 forbidden에러가 떴던 것이다
나는 지금 테스트하면서 이런 처리가 필요하지 않으므로 ,

testImplementation 'org.springframework.security:spring-security-test'

일단 spring security test의존성을 추가하고

기존 테스트 코드에 .with(csrf())만 추가해주면
내 요청에 csrf토큰이 알아서 만들어져서 들어가는것같다

보면 요청에 csrf가 들어간것 확인 가능!!

이렇게 하면 배포시에 csrf체크를 해제하지 않아서 안전하면서,
테스트에서도 간단하게 csrf체크를 피해갈 수 있다



403이 해결되자 401 UnAuthorized Error

이렇게 수정하고 테스트를 돌려보자....
401에러가 뜬다........
이 역시 Spring Security로 인한 건데, 아마도 로그인 api에 대한 권한 설정이 되어있지 않아서 그런듯하다.
이 api에 대한 권한을 모든 사람에게 주면 되겠지만,
나는 Spring Security를 테스트에서 사용하고 싶지 않았으므로
api 요청에 권한을 주는 방식은 사용하고 싶지 않았다.


링크된 블로그를 참고하여

@WithMockUser를 붙여서 일단 해결했으나,
로그인은 사실 권한이 없어도 수행할 수 있어야 하므로
아마 SecurityConfig설정에서 로그인은 모두에게 허용되도록 바꾸던가,
아니면 테스트 환경에서는 Security가 동작하지 않도록 하는 어떤 방법이 필요할 것 같다.

일단@WithMockUser로 대충 넘기고 나중에 수정하자! 했다.


@WithMockUser는 위처럼 요청 세션에 authentication Token을 만들어서 넣어준다.




결론적으로 나는 security동작으로 인해 권한 허용이 되지 않는 것들을 csrf토큰을 요청에 넣어주고, @MockUser를 사용함으로서 해결했다.

그런데 팀원은 나와 같은 환경인데 따로 401/403에 대한 처리를 해주지 않았다고 한다.
왜인지 살펴보자, 둘이 MockMvc를 초기화하는 방법이 다름을 확인했다.


  • 팀원: 테스트 클래스에 @WebMvcTest만 달고 MockMvc는
    @BeforeEach로 Mockbuilder를 이용해 설정. 인코딩도 builder로 함

  • 나: 인코딩은 application.yml에 지정하고, mockMvc는 @Autowired로 주입받음



그리고 mockMvc를 초기화하는 방법은 @Autowired로 자동 주입받는 법이 있고
mockMvcBuilder를 사용해서 초기화하는방법이 있는데, 이건 커스텀 가능함

팀원이 한 방식은 아래처럼 MockMvcBuilder를 이용하여 mocMvc를 초기화하는 방법.

@WebMvcTest(UserController.class)
public class UserControllerTest {

    private final JsonUtil jsonUtil = new JsonUtil();

    @MockBean
    UserService userService;

    @MockBean
    LoginService loginService;


    private MockMvc mockMvc;

    @BeforeEach
    void setUp(WebApplicationContext applicationContext) {
        mockMvc =
                MockMvcBuilders.webAppContextSetup(applicationContext)
                        .addFilter(new CharacterEncodingFilter(StandardCharsets.UTF_8.name(), true))
                        .build();
    }
    ...

인코딩을 application.yml에 설정하고
setdup메소드 내의 인코딩을 설정하는 코드인
.addFilter(...)를 지우면 인코딩이 정상적으로 되지 않는다.
application.yml에 설정한 인코딩은
@Autowired로 mockMvc를 주입받을 때에만 동작했다.

만약 @Autowired도 달아놓고 setup메소드도 지우지 않아 MockBuilder를 남겨두면,
처음에 mockMvc에는 자동으로 주입받은 값이 들어가지만 이후 다시 초기화하므로
addFilter(...)를 지울시 역시나 인코딩 비정상적



내가 한 방식은 아래와 같이 @Autowired로 mockmvc를 주입받는 방식.
@WebMvcTest(UserController.class)
public class UserControllerTest {

    private final JsonUtil jsonUtil = new JsonUtil();

    @MockBean
    UserService userService;

    @MockBean
    LoginService loginService;


	@Autowired //주입받기
    private MockMvc mockMvc;

	//Mock build하는 코드 전부 삭제
    ...

인코딩은 application.yml에서 설정한대로 잘 된다.
다만 위 팀원의 코드와 다른 점이
내 코드에서는 spring security가 동작한다는 점이다

그래서 어쩔수없이 csrf토큰을 넣어주고 @withmockuser를 사용해서 session attrs에 authentication 토큰이 들어가도록 했던 것이다


MockMvc의 autowired가 어떤식으로 동작하는지 알면 이해가 될 것 같은데...
내가 만든 bean이 아니라 스프링부트가 자동으로 만들어서 넣어주는 것 같은데...
나중에 이어서 정리하기!!

0개의 댓글