기존에 중복검사(예외처리)까지 한 코드에서 DB에 회원가입 정보를 저장할때 비밀번호를 암호화하기 위해 Spring Security를 사용하고 Test를 하는 과정에서 오류가 발생했다.
@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());
}
}
요청을 할때 권한을 같이 넘겨줘야 한다.
@WithMockUser, @WithUserDetails 와 같은 애노테이션을 이용해 권한을 넘겨주는 방법을 통해 해결할 수 있다.
@WithMockUser - 인증된 사용자
@WithAnonymousUser - 미인증 사용자
@WithUserDetails - 메서드가 principal 내부의 값을 직접 사용하는 경우 (별도의 사전 설정 필요)
따라서 @WithMockUser을 붙여주게 되면 권한은 있기때문에 401오류는 사라진다
csrf란??
- 공격자가 악의적인 코드를 심어놓은 사이트를 만들어놓고, 로그인 된 사용자가 클릭하게 만들어 사용자 의지와 무관한 요청을 발생시키는 공격(게시물같은 곳에 링크만 올려놓던가 광고등을 통해서 사이트에 접속하게 만듦)
- 로그인 한 상태에서 사용자가 해킹 사이트에 접속시 공격자는 해당 유저의 정보를 빼가고 그 정보를 가지고 메인 서버로부터 그 유저의 개인정보를 호출하여 정보를 빼가는 방식이다.
즉, POST, UPDATE, DELETE와 같이 작업하는 기능들이나 중요한 정보를 호출하는 부분인 GET에 대해서는 csrf를 적용하고 있다.
- 따라서 이러한 문제점을 방지하기 위해 csrf 토큰을 사용하는데 토큰 값을 비교해서 일치하는 경우에만 메서드를 처리하도록 한다.
- 서버가 뷰를 만들때 사용자 별 csrf 랜덤값을 만들어 세션에 로그인 정보와 같이 담아 HTTP 요청마다 csrf 토큰을 같이 넘겨 준다. 이때, 서버는 매 페이지마다 csrf 토큰값과 세션에 저장되어 있는 토큰값을 비교해 일치한 경우에만 처리를 진행한다.
따라서 Test코드에서도 csrf의 주소 값을 Http 요청할때 넣어 같이 보내줘야 하므로 .with(csrf( ))을 통해 담아준다.
@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());
}
}
참고 자료 : SpringSecurity Test 오류 : https://sedangdang.tistory.com/303
이미지 출처 : http://www.bluekaizen.org