PlayGround/마실가실 리팩토링

[1년 후 마실가실] @WebMvcTest Security 403

HJ0216 2024. 9. 21. 22:57

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

 

springboot Test 중에 403, 401 에러가 날때(spring security)

Spring Security 가 구성되어 있는 프로젝트에서 Controller 테스트를 하다보면 인증때문에 에러가 난다. WebMvcTest로 수행했다 하더라도 Security 는 Filter 영역에 추가되는 것이기 때문에 그렇다. WebMvcTest

lemontia.tistory.com

https://nordvpn.com/ko/blog/csrf/?srsltid=AfmBOoqLrbNTfgqsgkW6o-bHv3MfBZW8uVXqHFa3HgW2ueu6LQpSqQD5

 

크로스 사이트 요청 위조(CSRF)의 의미 | NordVPN

크로스 사이트 요청 위조란 무엇일까요? 이 글에서 크로스 사이트 요청 위조의 의미와 방지 방법을 확인해 보세요

nordvpn.com

https://velog.io/@sysy123/Spring-Boot-Error-ControllerTest-Spring-Security-403-%EC%97%90%EB%9F%AC

 

[Spring Boot] Error: ControllerTest + Spring Security 403 에러

1. ControllerTest에 @AutoConfigureMockMvc(addFilters = false) 추가 2. SecurityConfig에 http.csrf().disable() 추가 3. Test 코드에 @WithMockUser, with(csrf()) 추가

velog.io