@WebMvcTest 에서 Spring Security 적용했을때 401/403 에러 발생

배지원·2022년 12월 6일
1

에러

목록 보기
3/7

기존에 중복검사(예외처리)까지 한 코드에서 DB에 회원가입 정보를 저장할때 비밀번호를 암호화하기 위해 Spring Security를 사용하고 Test를 하는 과정에서 오류가 발생했다.

1. 오류내용

TestCode

@WebMvcTest
class UserControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockBean
    UserService userService;

    @MockBean
    BCryptPasswordEncoder encoder;

    @Autowired
    ObjectMapper objectMapper;



    @Test
    @DisplayName("회원가입 성공")
    void join_success() throws Exception {
        // given
        UserJoinRequest userJoinRequest = UserJoinRequest.builder()
                .userName("han")
                .password("1q2w3e4r")
                .email("oceanfog1@gmail.com")
                .build();

        User user = userJoinRequest.toEntity(encoder.encode(userJoinRequest.getPassword()));        // 비밀번호 암호화
        UserDto userDto = UserDto.fromEntity(user);

        when(userService.join(any())).thenReturn(userDto);

        // when, then
        mockMvc.perform(post("/api/v1/users/join")
                        .contentType(MediaType.APPLICATION_JSON)        // Json 타입으로 사용
                        .content(objectMapper.writeValueAsBytes(userJoinRequest)))      // 삽입한 데이터 dto를 json 형식으로 변환
                .andDo(print())
                // userName 존재 여부 확인
                .andExpect(jsonPath("$..userName").exists())
                // userName의 값 비교
                .andExpect(jsonPath("$..userName").value("han"))
                .andExpect(status().isOk());

    }

    @Test
    @DisplayName("회원가입 실패")
    void join_fail() throws Exception {
        UserJoinRequest userJoinRequest = UserJoinRequest.builder()
                .userName("han")
                .password("1q2w3e4r")
                .email("oceanfog1@gmail.com")
                .build();

        // 이전에는 when/thenReturn을 통해 구현했는데 그렇게 하면 given 구역에서 when을 사용하면 헷갈릴 수 있으므로 given으로 구역을 표시하며 정확히 한다.
        given(userService.join(any()))
                .willThrow(new HospitalReviewAppException(ErrorCode.DUPLICATED_USER_NAME, ""));

        mockMvc.perform(post("/api/v1/users/join")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsBytes(userJoinRequest)))
                .andDo(print())
                .andExpect(status().isConflict());

        verify(userService).join(any());
    }
}

(1) 401 오류

  • Test 코드 실행시 401 오류가 발생한다. 그 이유는 Security가 모든 요청에 대해 권한을 요구하기 때문에 현재는 권한을 따고 주지 않아 오류가 발생했다.

(2) 403 오류

  • 이번에는 403 오류가 발생한다. 그 이유는 Security에서 로그인은 했지만 권한이 맞지 않아 발생하는 오류라고 한다.


2. 해결방안

(1) 401 오류

  • 요청을 할때 권한을 같이 넘겨줘야 한다.

  • @WithMockUser, @WithUserDetails 와 같은 애노테이션을 이용해 권한을 넘겨주는 방법을 통해 해결할 수 있다.

    @WithMockUser - 인증된 사용자
    @WithAnonymousUser - 미인증 사용자
    @WithUserDetails - 메서드가 principal 내부의 값을 직접 사용하는 경우 (별도의 사전 설정 필요)

    따라서 @WithMockUser을 붙여주게 되면 권한은 있기때문에 401오류는 사라진다


(2) 403 오류

  • Spring Security Test에서 403오류는 csrf때문에 발생한다고 한다.

csrf란??

  • 공격자가 악의적인 코드를 심어놓은 사이트를 만들어놓고, 로그인 된 사용자가 클릭하게 만들어 사용자 의지와 무관한 요청을 발생시키는 공격(게시물같은 곳에 링크만 올려놓던가 광고등을 통해서 사이트에 접속하게 만듦)

  • 로그인 한 상태에서 사용자가 해킹 사이트에 접속시 공격자는 해당 유저의 정보를 빼가고 그 정보를 가지고 메인 서버로부터 그 유저의 개인정보를 호출하여 정보를 빼가는 방식이다.
    즉, POST, UPDATE, DELETE와 같이 작업하는 기능들이나 중요한 정보를 호출하는 부분인 GET에 대해서는 csrf를 적용하고 있다.

  • 따라서 이러한 문제점을 방지하기 위해 csrf 토큰을 사용하는데 토큰 값을 비교해서 일치하는 경우에만 메서드를 처리하도록 한다.

  • 서버가 뷰를 만들때 사용자 별 csrf 랜덤값을 만들어 세션에 로그인 정보와 같이 담아 HTTP 요청마다 csrf 토큰을 같이 넘겨 준다. 이때, 서버는 매 페이지마다 csrf 토큰값과 세션에 저장되어 있는 토큰값을 비교해 일치한 경우에만 처리를 진행한다.

따라서 Test코드에서도 csrf의 주소 값을 Http 요청할때 넣어 같이 보내줘야 하므로 .with(csrf( ))을 통해 담아준다.


(3) CODE

@WebMvcTest
class UserControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockBean
    UserService userService;

    @MockBean
    BCryptPasswordEncoder encoder;

    @Autowired
    ObjectMapper objectMapper;



    @Test
    @DisplayName("회원가입 성공")
    @WithMockUser
    void join_success() throws Exception {
        // given
        UserJoinRequest userJoinRequest = UserJoinRequest.builder()
                .userName("han")
                .password("1q2w3e4r")
                .email("oceanfog1@gmail.com")
                .build();

        User user = userJoinRequest.toEntity(encoder.encode(userJoinRequest.getPassword()));        // 비밀번호 암호화
        UserDto userDto = UserDto.fromEntity(user);

        when(userService.join(any())).thenReturn(userDto);

        // when, then
        mockMvc.perform(post("/api/v1/users/join")
                        .with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)        // Json 타입으로 사용
                        .content(objectMapper.writeValueAsBytes(userJoinRequest)))      // 삽입한 데이터 dto를 json 형식으로 변환
                .andDo(print())
                // userName 존재 여부 확인
                .andExpect(jsonPath("$..userName").exists())
                // userName의 값 비교
                .andExpect(jsonPath("$..userName").value("han"))
                .andExpect(status().isOk());
    }

    @Test
    @DisplayName("회원가입 실패")
    @WithMockUser
    void join_fail() throws Exception {
        UserJoinRequest userJoinRequest = UserJoinRequest.builder()
                .userName("han")
                .password("1q2w3e4r")
                .email("oceanfog1@gmail.com")
                .build();

        // 이전에는 when/thenReturn을 통해 구현했는데 그렇게 하면 given 구역에서 when을 사용하면 헷갈릴 수 있으므로 given으로 구역을 표시하며 정확히 한다.
        given(userService.join(any()))
                .willThrow(new HospitalReviewAppException(ErrorCode.DUPLICATED_USER_NAME, ""));

        mockMvc.perform(post("/api/v1/users/join")
                        .with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsBytes(userJoinRequest)))
                .andDo(print())
                .andExpect(status().isConflict());

        verify(userService).join(any());
    }
}


3. 결과

  • 세션에 담아놓은 csrf의 값과 csrf 토큰값을 비교해서 실행되는 정상동작하는 모습이다.


참고 자료 : SpringSecurity Test 오류 : https://sedangdang.tistory.com/303
이미지 출처 : http://www.bluekaizen.org

profile
Web Developer

0개의 댓글