개발 환경
- 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"} 등등
테스트
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 |