강잇
강이의 개발블로그
강잇
전체 방문자
오늘
어제
  • 분류 전체보기 (102)
    • Langauge (32)
      • Java-basic (29)
      • Java (3)
    • SpringBoot (7)
    • Algorithm (5)
      • BAEKJOON (5)
    • WEB (7)
      • HTML & CSS (7)
    • DB (1)
      • MySQL (1)
    • OS (17)
      • Mac (2)
      • Linux (4)
      • Terminal Command (11)
    • Computer Science (7)
      • Hard ware (1)
      • Database (1)
      • Data structure (2)
      • Algorithm (2)
      • Network (1)
    • Git (5)
      • 개념 (1)
      • 활용 (1)
      • Trouble Shooting (2)
    • ETC. (13)
      • Install (6)
      • IntelliJ (1)
      • Eclipse (2)
      • Error (3)
      • Tip (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 백준
  • CSS 속성
  • CSS 박스 크기 설정
  • 메소드
  • til
  • 메서드
  • 자바
  • 알고리즘 공부

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
강잇
SpringBoot

[TroubleShooting] SpringBoot Controller Test - MockMvc 302 Found, 403 Forbidden

[TroubleShooting] SpringBoot Controller Test - MockMvc 302 Found, 403 Forbidden
SpringBoot

[TroubleShooting] SpringBoot Controller Test - MockMvc 302 Found, 403 Forbidden

2022. 11. 24. 11:12

개발 환경

  • Build : Gradle
  • SpringBoot :  2.7.5
  • Java : 11
  • OS : Mac

발생 - 403 Forbidden

RestDocs를 사용하여 API 문서화 작업을 하려고 Controller에서 테스트 코드를 작성 후 테스트를 진행하였고, 403 Forbidden 에러가 발생했다.

@WebMvcTest(controllers = ArtworkController.class)
@MockBean({JpaMetamodelMappingContext.class, ClientRegistrationRepository.class})
@AutoConfigureRestDocs
public class ArtworkControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ArtworkService artworkService;

    @Autowired
    private Gson gson;

    @Test
    void postArtworkTest() throws Exception {
        Long memberId = 1L;
        Long galleryId = 1L;

        MockMultipartFile image = new MockMultipartFile(
                "image",
                "image.png",
                "image/png",
                "<<image.png>>".getBytes());

        String title = "올해 네 컷 - D25";
        String content = "화이팅!";

        ArtworkRequestDto requestDto = ArtworkRequestDto.builder().image(image).title(title).content(content).build();
        String request = gson.toJson(requestDto);

        willDoNothing().given(artworkService).createArtwork(memberId, galleryId, requestDto);

        ResultActions actions =
                mockMvc.perform(
                        RestDocumentationRequestBuilders.multipart("/galleries/{gallery-id}/artworks", galleryId)
                                .file(image)
                                .param("title", title)
                                .param("content", content)
                                .header("Authorization", "Bearer (accessToken)")
                                .accept(MediaType.APPLICATION_JSON)
                                .contentType(MediaType.MULTIPART_FORM_DATA)
                                .characterEncoding("UTF-8")
                                .flashAttr("write", requestDto)
                );
        actions
                .andExpect(status().isCreated());
    }
}
MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /galleries/1/artworks
       Parameters = {title=[올해네컷 프로젝트], content=[조금만 더 화이팅~!]}
          Headers = [Content-Type:"multipart/form-data;charset=UTF-8"]
             Body = null
    Session Attrs = {org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN=org.springframework.security.web.csrf.DefaultCsrfToken@27210a3b}

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 403
    Error message = Forbidden
          Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

Status expected:<201> but was:<403>
Expected :201
Actual   :403

원인 - 403 Forbidden

403 Forbidden 에러의 발생에 대해 검색을 하니 대부분 CSRF 토큰에 대한 문제로 발생한다고 하였다.

CSRF(Cross-Site-Request Forgery)란?
악의적인 코드를 심어놓은 사이트에서 로그인한 사용자가 클릭하게 하여 사용자의 의지와 무관한 요청을 발생시키는 공격을 의미한다.
Spring Security에서는 이를 방지하기 위해 "CSRF Token"을 이용해 토큰을 비교한 뒤 일치하는 경우에만 메서드를 정상적으로 실행하게 만든다.

참고로 RESTFul한 웹 서비스에서는 대부분 비활성화로 설정한다고 한다.
REST API는 대부분 무상태성을 유지하며, JWT와 같은 토큰 방식으로 인증하기 때문에 요청이 세션에 의존하지 않기 때문이다.

여기서 궁금한 점이 Security Config에서 csrf().disable() 설정으로 비활성화를 해놨는데.. 왜 csrf 관련 문제가 생겼는지 의문이다..

결국 원인은 받은 요청에 CSRF 토큰이 담겨있지 않아 세션에 저장된 CSRF 값과 비교할 수 없어 403 Forbidden 에러가 발생했던 것이다.

해결 - 403 Forbidden

해결하는 방법은 다음과 같이 .with(csrf())을 추가하면 된다. (403은 해결됐는데, 또 다른 에러가 발생하여 밑에 정리함...)

ResultActions actions =
                mockMvc.perform(
                        RestDocumentationRequestBuilders.multipart("/galleries/{gallery-id}/artworks", galleryId)
                                .file(image)
                                .param("title", title)
                                .param("content", content)
                                .header("Authorization", "Bearer (accessToken)")
                                // <============== 
                                .with(csrf()) 
                                // <========= 추가됨 
                                .accept(MediaType.APPLICATION_JSON)
                                .contentType(MediaType.MULTIPART_FORM_DATA)
                                .characterEncoding("UTF-8")
                                .flashAttr("write", requestDto)
                );
        actions
                .andExpect(status().isCreated());

발생 - 302 Found

위에서 CSRF 관련 코드를 추가한 뒤 테스트를 실행하니 다음과 같이 302 Found를 응답받았다.

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /galleries/1/artworks
       Parameters = {title=[올해 네 컷 - D25], content=[화이팅!], _csrf=[64334fab-9a1f-449b-b664-471ffa351c93]}
          Headers = [Content-Type:"multipart/form-data;charset=UTF-8", Authorization:"Bearer (accessToken)", Accept:"application/json"]
             Body = null
    Session Attrs = {org.springframework.web.servlet.support.SessionFlashMapManager.FLASH_MAPS=[FlashMap [attributes={write=com.codestates.mainproject.oneyearfourcut.domain.artwork.dto.ArtworkRequestDto@58068b40}, targetRequestPath=null, targetRequestParams={}]]}

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 302
    Error message = null
          Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY", Location:"http://localhost:8080/login"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = http://localhost:8080/login
          Cookies = []

Status expected:<201> but was:<302>
Expected :201
Actual   :302

원인 - 302 Found

MockHttpServletResponse:
           Status = 302
    Error message = null
          Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY", Location:"http://localhost:8080/login"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = http://localhost:8080/login
          Cookies = []

하나씩 살펴보던 중 Redirected URL이 login으로 찍혀 있는 것을 보고 인증 관련 문제라고 생각이 들었다.

역시나 API 요청에서 인증 정보가 없어 인증을 먼저 진행하라고 리다이렉트되어 302가 발생한 것이었다.

해결 - 302 Found

인증 정보를 설정하는 방법은 다양하지만 개인적으로 생각했을 때 가장 간단한 방법으로 해결하기로 했다.

다음과 같이 테스트코드 위에 @WithMockUser 어노테이션을 추가하면 된다.

@Test
@WithMockUser(username = "test@gmail.com", password = "0000")
void postArtworkTest() throws Exception {
    Long memberId = 1L;
    Long galleryId = 1L;
    
    ...
}
@WithMockUser 어노테이션은?
테스트에 필요한 인증된 정보를 제공하는 어노테이션이다.
간단한 정보는 WithMockUser를 이용해 설정할 수 있다.

WIthMockUser의 default 값은 다음과 같으며, 필요한 경우 위와 같이 수정을 통해 인증 정보를 간단하게 설정하면 될 듯 하다.
- username = ""
- password = "password"
- authorities = {}
- roles = {"USER"} 등등

테스트

403과 302를 만난 후에야 볼 수 있었던 초록색 체크..


Reference

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

https://developer.mozilla.org/ko/docs/Web/HTTP/Status/302

 

302 Found - HTTP | MDN

하이퍼텍스트 전송 프로토콜 (HTTP)의 302 Found 리다이렉트 상태 응답 코드는 클라이언트가 요청한 리소스가 Location (en-US) 헤더에 주어진 URL에 일시적으로 이동되었음을 가리킨다. 브라우저는 사용

developer.mozilla.org

 

저작자표시 (새창열림)

'SpringBoot' 카테고리의 다른 글

[SpringBoot] AWS S3 파일(이미지) 업로드 및 삭제하기 구현  (0) 2022.11.28
[SpringBoot] MockMvc - multipart() POST외 다른 HTTPMethod 사용하기  (0) 2022.11.26
[SpringDataJPA] 쿼리메서드 참조 객체의 필드 사용  (0) 2022.11.17
[Trouble Shooting] MaxUploadSizeExceededException  (0) 2022.11.03
[에러해결] HttpMediaTypeNotAcceptableException: Could not find acceptable representation  (0) 2022.09.04
  • 개발 환경
  • 발생 - 403 Forbidden
  • 원인 - 403 Forbidden
  • 해결 - 403 Forbidden
  • 발생 - 302 Found
  • 원인 - 302 Found
  • 해결 - 302 Found
  • 테스트
  • Reference
'SpringBoot' 카테고리의 다른 글
  • [SpringBoot] AWS S3 파일(이미지) 업로드 및 삭제하기 구현
  • [SpringBoot] MockMvc - multipart() POST외 다른 HTTPMethod 사용하기
  • [SpringDataJPA] 쿼리메서드 참조 객체의 필드 사용
  • [Trouble Shooting] MaxUploadSizeExceededException
강잇
강잇
학습한 내용을 정리 및 기록하는 블로그

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.