반응형

1. 오류 상황

 restdocs를 사용하여 JUnit 테스트 코드에서 REST API에 대한 명세 파일을 생성 도중 특정 케이스에서만 Form parameters with the following names were not documented: [_csrf] 에러가 발생하였다.

_csrf 에 대한 SnippetException

 


2. 오류내용

 _csrf 라는 이름을 가진 매개변수가 formParameters에 정의되지 않았고, 최종적으로 문서화 되지않았다는 오류였다. 즉, 요청 파라미터에는 _csrf 값이 있는데, formParameters에는 정의하지 않아 발생했다.

테스트의 HttpServletRequest 정보

 


3. 코드

@Test
@TestMemberAuth
@DisplayName("운동 상태정보 수정")
void updateExecStatusToW() throws Exception{
    mockMvc.perform(
            patch("/api/member/exec-status")
                    .header("Authorization", "Bearer JWT_ACCESS_TOKEN")
                    .param("status", ExecStatus.W.name())
                    .with(csrf()))
            .andExpect(status().isNoContent())
            .andDo(document(
                    "updateExecStatus"
                    , requestHeaders(
                            headerWithName("Authorization").description("JWT_ACCESS_TOKEN"))
                    , formParameters(
                            parameterWithName("status").description("헬스 상태 (W : 준비중, H : 헬스중, I : 부상으로 쉬는중"))
                    )
            );
}

 

* with(csrf()) 구문이 포함되어 있는 이유

 with(csrf()) 는 MockHttpServletRequest에 CSRF 토큰 값을 추가하는 메서드이다. SpringSecurity 옵션을 통해 csrf 토큰 사용을 disable 처리했는데, mockMvc를 사용할 경우 기본적으로 csrf 토큰이 사용되도록 동작하기 때문이다. 이를 해결하기 위해 해당 구문을 사용하고 있었다.


5. 원인 분석

5.1. [ formParameters() + _csrf ] 케이스 예외 발생

 예외가 발생하는 케이스는 Paramters에 _csrf 값이 들어가고, form 으로 전송되는 Parameters 에 대한 문서화 메서드인 formParameters() 를 사용하는 케이스였다. Paramters에 _csrf가 들어가고 있는데 formParameters에는 이에 대한 명세를 하지 않았기 때문에 발생했다.

 

 반대로 Json 형태의 요청 값을 문서화 시킬 경우 requestFields() 메서드를 사용하는데 이는 Parameters 가 아닌 Body에 있는 값을 기준으로 매핑하기 때문에 _csrf 관련 에러가 발생하지 않았던 것이다.

application/form 타입의 요청에 대한 MockHttpServletRequest 정보
application.json 타입의 요청에 대한 MockHttpServletRequest 정보

 


6. 해결

6.1. csrf 토큰을 헤더로!

 알려진 해결방안은 테스트 전용 SecurityConfig 설정 파일을 만들어서 csrf에 대해 disable 처리하고, MockMvc 테스트 마다 생성한 설정파일을 Configuration 파일로 로드하는 코드를 추가하면 된다고 하는데, _csrf 값을 요청 파라미터가아닌 헤더로 받으면 되지 않을까라는 생각이 들었다.

 

 클라이언트에서 CSRF 토큰 값을 서버로 전송하는 방법은 요청 헤더에 토큰 값을 넣거나 요청 파라미터에 토큰 값을 넣는 것이며, 요청 헤더의 경우 X-CSRF-TOKEN, 요청 파라미터의 경우 _csrf 라는 키 값에 넣으면 된다.

 

 with(csrf())를 사용할 경우 요청 파라미터에 csrf 토큰 값이 포함되어 들어가고 있는데, 이를 요청 헤더로 이동시키면 Paramters의 _csrf 값을 제거될 것이고, 에러도 해결될 것 같았다.

 

 곧장 csrf() 의 내부코드를 보니 아래와 같이 asHeader 값이 true일 경우 토큰 값을 헤더에, 그 외에는 Paramter에 설정하는 것을 확인할 수 있었다. 기본값은 false 였기에 Paramter로 토큰이 전송되고 있었다.

csrf()에 메서드에 대한 csrf 설정 부분

 

asHeader만 true로 설정해주는 메서드인 asHeader()도 곧바로 찾을 수 있었다. 바로 적용해보았다.

asHeader를 true로 설정하는 asHeader() 메서드

 

 

6.2. 적용 및 테스트

 기존 csrf() 메서드에 체인 메서드 형태로 asHeader() 메서드만 추가해줬다. 테스트 결과 예외는 해결되었으며, 요청 값을 확인해보면 csrf 토큰 값이 Headers로 요청되는 것을 확인할 수 있다.

@Test
@TestMemberAuth
@DisplayName("운동 상태정보 수정")
void updateExecStatusToW() throws Exception{
    mockMvc.perform(
            patch("/api/member/exec-status")
                    .header("Authorization", "Bearer JWT_ACCESS_TOKEN")
                    .param("status", ExecStatus.W.name())
                    .with(csrf().asHeader())) // asHeader() 추가
            .andExpect(status().isNoContent())
            .andDo(document(
                    "updateExecStatus"
                    , requestHeaders(
                            headerWithName("Authorization").description("JWT_ACCESS_TOKEN"))
                    , formParameters(
                            parameterWithName("status").description("헬스 상태 (W : 준비중, H : 헬스중, I : 부상으로 쉬는중"))
                    )
            );
}

 

Parameters 에서 Headers로 옮겨진 csrf 토큰

 

반응형

+ Recent posts