[1년 후 마실가실] @WebMvcTest Security 403
1년 전 진행했던 마실가실 프로젝트를 🛠️리팩토링하며 정리한 내용입니다.
최근 제 Controller 테스트 코드가 열린교회 닫힘이었습니다.
분명 전 create API에 대해서 permitAll()을 설정했는데, 테스트 코드로 가입이라도 하려면 칼같이 403 Forbidden을 반환했습니다.
아니, 제가 괜찮다는데.. Postman에서도 괜찮다고 했는데 테스트 코드가 거절을 했습니다.
가입을 못하면,, 좀 이상하기에,, 왜 문제가 발생하는지, 어떻게 수정했는지 정리해보고자 합니다.
Test Code에서 403 Forbidden이 발생하는 원인1
CSRF(Cross-Site Request Forgery)
* @AutoConfidurationMockMvc
* MockMvc 테스트 환경에서 CSRF 보호가 기본적으로 적용
* CSRF(Cross-Site Request Forgery) 보호 때문에 POST 요청에 대해 CSRF 토큰이 필요
cf. csrf 공격
* 인증된 사용자가 웹 애플리케이션에 특정 요청을 보내도록 유도하는 공격 행위
* 원하는 요청을 위조한 후, 이메일이나 웹사이트에 요청이 삽입된 하이퍼링크를 심어 놓아 사용자가 해당 링크를 클릭하면 요청이 자동으로 전송
CSRF(Cross-Site Request Forgery) 관련 해결방안
1. SecurityFilterChain에서 비활성화 처리
* csrf(AbstractHttpConfigurer::disable)
2. Test 코드에서 csrf 활성화 처리
* with(csrf())
⭐ JWT는 Token 방식으로, crfs 보호 방식을 사용하지 않아도 괜찮음
* CSRF 토큰을 사용하여 클라이언트가 서버에 요청할 때마다 유효한 토큰을 함께 전송해야만 요청이 성공하도록 하는 방식으로 보안을 강화 -> CSRF 토큰 대신 JWT 사용
Test Code에서 403 Forbidden이 발생하는 원인2
Spring Security Filter
* @AutoConfidurationMockMvc
* SecurityFilterChain 설정 시, API 관련 권한 부족
Spring Security Filter 관련 해결방안
1. @AutoConfidurationMockMvc(addFilters = false)
* Spring Security Filter 해제
🚨인증없는 테스트는 앙꼬없는 찐빵으로 특별한 이유가 없으면 사용 X
2. SecurityFilterChain에서 해당 uri 설정 확인
* permitAll() 처리가 되어있는지 확인
* anyRequest().authenticated() 처리가 되어있을 경우, 잘못된 API 주소로 요청 시, 401 오류 발생(= 인증이 문제가 됨)
* anyRequest().authenticated() 처리가 안되어있을 경우, 잘못된 API 주소로 요청 시, 403 오류 발생(= 차단 됨)
저는 회원가입 API 주소를 최근에 REST API에 맞춰 create -> new로 변경하였는데, SecurityFilterChain 수정이 안되어있던 게 문제가 되었습니다. 문자열 처리가 되어있는 부분은 늘 유의를 해야합니다😮!
SecurityConfig.java
@Configuration // 스프링의 환경설정 파일임을 의미하는 애너테이션
@EnableWebSecurity // 모든 요청 URL이 스프링 시큐리티의 제어를 받도록 만드는 애너테이션
@RequiredArgsConstructor
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.httpBasic(AbstractHttpConfigurer::disable) // 기본 HTTP 인증을 비활성화
.csrf(AbstractHttpConfigurer::disable) // CSRF 보호를 비활성화, CSRF 토큰을 사용하여 클라이언트가 서버에 요청할 때마다 유효한 토큰을 함께 전송해야만 요청이 성공하도록 하는 방식으로 보안을 강화 -> 토큰으로 대체
.cors(httpSecurityCorsConfigurer -> corsConfigurationSource())
.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션을 사용하지 않고, 상태를 저장하지 않도록 설정(STATLESS)
).authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v2/users/login").permitAll()
.requestMatchers("/api/v2/users/new").permitAll()
.requestMatchers("/api/v2/users/me", "/api/v2/users/logout").hasRole("USER")
.requestMatchers("/api/v2/users/reissue").permitAll()
.anyRequest().authenticated() // 이 외의 접근은 인증이 필요
)
.addFilterBefore(jwtAuthenticationFilterForSpecificUrls(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
UserControllerTest.java
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Test
@DisplayName("Controller: 회원 가입 실패, 중복된 이메일")
void createFailDuplicateEmail() throws Exception {
// given
SignUpRequestDTO signUpDto = SignUpRequestDTO.builder()
.status("M")
.email("temp@email.com")
.phone("01023698745")
.nickname("name")
.password("temp123!")
.confirmPassword("temp123!")
.build();
// when // then
doThrow(new BusinessException(ErrorCode.DUPLICATED_EMAIL))
.when(userService).create(any(SignUpRequestDTO.class));
mockMvc.perform(post("/api/v2/users/new")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(signUpDto)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errorMessage").value("이미 존재하는 이메일 입니다."));
// 회원 생성 메소드가 호출되었는지 확인
verify(userService).create(refEq(signUpDto));
}
}
🙋♀️
본 포스트는 공부 목적으로 작성하였습니다.
보시는 도중 잘못된 부분이나 개선할 부분이 있다면 댓글로 알려주시면 수정하도록 하겠습니다.
📑
참고 자료
https://lemontia.tistory.com/1088
https://nordvpn.com/ko/blog/csrf/?srsltid=AfmBOoqLrbNTfgqsgkW6o-bHv3MfBZW8uVXqHFa3HgW2ueu6LQpSqQD5
https://velog.io/@sysy123/Spring-Boot-Error-ControllerTest-Spring-Security-403-%EC%97%90%EB%9F%AC