❗️컨트롤러 테스트코드를 작성하는 와중에 401과 403 에러를 마주쳤다.
해결하는건 크게 어렵지 않았다!!!
컨트롤러 단위 테스트 진행 과정
먼저, 사용한 기술은 이러하다.
▶︎ Spring Data JPA , Spring Security
단위 테스트를 진행했기 때문에 @WebMvcTest 를 사용했다.
@SpringBootTest VS @WebMvcTest
❗️ 여기서 잠깐! !
@SpringBootTest와 @WebMvcTest 이 두 어노테이션의 차이는 뭘까?
분명 둘다 테스트 코드 작성시 사용하는건데.. 언제 어떤것을 사용 해야할까?
▶︎ @SpringBootTest
- 애플리케이션 전체를 테스트할 때 주로 사용된다. (통합 테스트)
- 애플리케이션의 모든 컴포넌트들을 로드한다. (securityConfig 등 모든 컴포넌트가 로드됨)
- 실제 애플리케이션 환경과 유사한 환경에서 테스트한다.
▶︎ @WebMvcTest
- 주로 웹 계층만 테스트할 때 사용된다. (컨트롤러 단위 테스트)
- 주로 컨트롤러(Controller)와 관련된 빈들만 로드하므로, 더 가벼우며 빠른 테스트가 가능하다. (securityConfig 로드 x)
따라서, 나는 컨트롤러 단위 테스트를 했기 때문에 @WebMvcTest를 사용하였다.
@WebMvcTest(OptionController.class)
@MockBean(JpaMetamodelMappingContext.class)
class OptionControllerTest {
ObjectMapper objectMapper;
public OptionControllerTest() {
objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
}
@Autowired MockMvc mockMvc;
@MockBean OptionFacade optionFacade;
@MockBean TokenProvider tokenProvider;
@Test
@DisplayName("옵션 조회 성공 - 서브 옵션 x")
void getOptions() throws Exception {
// given
Long productId = 1L;
List<OptionResponse> optionResponse = List.of(OptionResponse.builder()
.id(1L)
.name("오슬롭")
.build());
given(optionFacade.getOptions(productId)).willReturn(optionResponse);
// when
ResultActions resultActions = mockMvc.perform(get("/options/{productId}", productId));
// then
resultActions.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("success"))
.andDo(print());
}
}
@WebMvcTest(OptionController.class) // 해당 컨트롤러로 MockMvc를 생성
@MockBean(JpaMetamodelMappingContext.class) // JPA와 관련된 빈들은 목으로 등록(@EnableJpaAuditing 사용시 필요)
결과는...

401에러 발생.. (java.lang.AssertionError: Status expected:<200> but was:<401>)
▶︎ 401 ?
: Unauthorized 상태를 말하며 클라이언트가 요청한 리소스에 대한 인증(인증되지 않은 상태)이 실패했음을 나타낸다. 즉, 클라이언트가 요청한 리소스에 접근할 권한이 없는 상태를 말하므로 인증이 필요하다.
에러 원인
쉽게 설명하면, 현재 코드에서 @WebMvcConfig 를 사용했는데, 이 어노테이션을 사용함으로써 내가 만들어둔 Bean들은 안불러와지고 컨트롤러 테스트에 필요한 빈만 가져온다. 따라서 내가 설정해둔 SecurityConfig가 빈으로 등록이 안돼서 인증에 실패한 것이였다.
공식 문서를 보면
By default, tests annotated with @WebMvcTest will also auto-configure Spring Security
위와 같은 설명이 있는데, 스프링 시큐리티가 자동으로 구성하는 Configuration 파일들을 불러와서 사용한다는 말이다. 따라서 내가 작성한 설정들 대신에 스프링 시큐리티가 자동으로 구성하는 설정 파일들을 사용하기 때문에 인증에 실패한 것이였다.
401 해결 방법
해결 방법은 3가지가 있다.
- ExcludeFilter를 이용해서 인증 대상에서 제외하는 방법 (사실상 궁극적인 해결이 아닌 회피방법)
- @WithMockUser, @WithUserDetails 와 같은 애노테이션을 이용해 권한을 주는 방법
- 단위 테스트를 하기 위한 별도의 test 설정 파일을 만드는 방법 (ex. 별도의 파일에 @BeforeEach를 사용해서 테스트 실행 전에 로그인 로직을 타도록 작성한 뒤 해당 설정 파일을 extends 해서 사용하는 방법 등 - 추후 통합 테스트에서 사용 예시 보여줄 예정)
1번 방법은 사실상 회피 방법이고 3번예시로 들 수 있는 방법은 추후 통합 테스트에서 보여줄 예정이니, 2번 예시로 진행해봤다.
@WithMockUser를 사용했는데 '인증 권한을 가지고 있는 모의 사용자' 이다.
@WebMvcTest(OptionController.class) // 해당 컨트롤러로 MockMvc를 생성
@MockBean(JpaMetamodelMappingContext.class) // JPA와 관련된 빈들은 목으로 등록(@EnableJpaAuditing 사용시 필요)
@WithMockUser("user1") // 인증된 모의(가짜) 사용자를 만들어서 사용
class OptionControllerTest {
ObjectMapper objectMapper;
public OptionControllerTest() {
objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
}
@Autowired MockMvc mockMvc;
@MockBean OptionFacade optionFacade;
@MockBean TokenProvider tokenProvider;
@Test
@DisplayName("옵션 조회 성공 - 서브 옵션 x")
void getOptions() throws Exception {
// given
Long productId = 1L;
List<OptionResponse> optionResponse = List.of(OptionResponse.builder()
.id(1L)
.name("오슬롭")
.build());
given(optionFacade.getOptions(productId)).willReturn(optionResponse);
// when
ResultActions resultActions = mockMvc.perform(get("/options/{productId}", productId));
// then
resultActions.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("success"))
.andDo(print());
}
}

성공!!
하나 더 남았다.... 403에러 마주치다..
GET 방식에서는 나지 않았던 403 에러가 POST 방식에서 발생했다..!

▶︎ 403 ?
: Forbidden 상태를 말하며 클라이언트가 요청한 리소스에 대한 접근 권한이 있지만, 해당 권한으로는 요청된 동작(주로 리소스에 대한 접근)이 허용되지 않았음을 나타낸다. 즉, 로그인을 해서 인증은 됐지만 권한이 맞지않는 경우를 말한다.
에러 원인
csrf와 관련이 있다.
▶︎ CSRF(Cross-Site Request Forgery)
- 웹 어플리케이션에서 발생할 수 있는 보안 취약점 중 하나
- 공격자가 사용자의 권한을 이용하여 특정 동작을 수행하도록 속이는 공격
공격 시나리오
- 사용자 로그인 : 사용자가 웹 애플리케이션에 로그인한다.
- 공격자가 악의적인 페이지 제작: 공격자는 특별히 설계된 악의적인 웹 페이지를 만든다.
- 악의적인 요청 포함: 악의적인 웹 페이지에는 특별한 요청이 포함되어 있다. 이 요청은 사용자의 권한으로 특정 동작을 수행하도록 설정되어 있다.
- 사용자가 악의적인 페이지에 접근: 사용자가 악의적인 웹 페이지에 접근하면, 그 페이지에 포함된 악의적인 요청이 사용자의 권한으로 서버에 전송된다.
- 서버의 무심코 동작 수행: 사용자의 권한으로 요청이 수행되어 원치 않는 동작이 서버에서 실행된다. 즉, 사용자 의지와 무관한 요청을 발생시키는 공격
CSRF 방지 방법
- CSRF 토큰 사용: 모든 중요한 요청에는 사용자에게 할당된 고유한 CSRF 토큰이 포함되어 있어야 한다. 이 토큰은 공격자가 모르기 때문에 CSRF 공격을 예방하는 데 도움이 된다.
- SameSite 쿠키 속성 사용: SameSite 쿠키 속성을 사용하여 외부 사이트에서의 쿠키 전송을 방지함으로써 CSRF 공격을 방지할 수 있다.
- Referrer 검증: 요청의 Referer 헤더를 검증하여 허용된 도메인에서만 요청을 허용하도록 설정할 수 있다.
- 악의적인 요청 방지: 웹 어플리케이션에서는 민감한 동작을 수행하는 요청에 대해 추가적인 인증 및 권한 검사를 수행하여 공격을 방지할 수 있다.
403 해결 방법
Spring Security에서는 CSRF Token 을 이용해 토큰 값을 비교해서 일치하는 경우에만 메서드를 처리하도록 만든다.(Synchronizer Token Pattern 방식)
- GET 요청에 대해서는 CSRF 검증을 수행하지 않는다.
with.(csrf())를 추가하면 된다.
ex) ResultActions resultActions = mockMvc.perform(post("/options")
.content(objectMapper.writeValueAsString(saveOptionRequest))
.contentType(MediaType.APPLICATION_JSON)
.with(csrf())); // 403 에러 해결을 위해 추가
추가 전

추가 후
요청 시파라미터에 csrf 관련해서 토큰 값을 넣어서 보내주고 있다.


403 에러 해결 완료!!!!
.
.
.
🔆 참고 블로그
[Springboot] @WebMvcTest - Controller Test에서 401과 데이트했다.
우리 결혼했어요.
velog.io
https://sedangdang.tistory.com/303
@WebMvcTest 에서 Spring Security 적용, 401/403 에러 해결하기 - csrf
요약 401 Unauthorized -> @WithMockUser, @WithMockUserDetails 사용 403 Forbidden -> with(csrf()) 추가 @WebMvcTest Annotation that can be used for a Spring MVC test that focuses only on Spring MVC components. Using this annotation will disable full aut
sedangdang.tistory.com
'BE > Spring-Boot' 카테고리의 다른 글
[SpringBoot] 스케줄러(@Scheduled)가 간헐적으로 동작 안하는 문제 (0) | 2024.05.07 |
---|---|
[SpringBoot] Spring Security Config에서 permitAll()에 대한 진실과 오해 (4) | 2024.01.06 |
[JPA Auditing] 생성자/수정자 자동화 (+생성 일시/수정 일시) (0) | 2023.11.03 |
[JPA] 엔티티 이름이 예약어일 경우 (SQLSyntaxErrorException) (0) | 2023.09.08 |
[SpringBoot] Spring Security + React CORS 문제 해결 (0) | 2023.07.08 |