JWT JUnit 테스트에서 Authentication 객체 만들어 넣기

wish17·2023년 5월 23일
1
post-thumbnail

JWT 인증 과정과 테스트 문제점

JWT(JSON Web Token) 인증은 사용자의 인증 정보를 토큰에 담아 서버에 전달하는 인증 방식이다. 사용자가 로그인을 하면 서버는 사용자의 인증 정보를 포함한 JWT를 생성하여 사용자에게 전달한다. 사용자는 이후 요청에서 이 JWT를 헤더에 포함시켜서 인증 정보를 서버에 전달한다.

그러나 이 인증 과정을 단위 테스트에서 그대로 구현하는 것은 매우 복잡하며, 별도의 인증 서버나 JWT 발급과정이 필요하다. 또한, 실제 인증 과정을 테스트에 포함시키면, 인증 과정에서 발생하는 오류 때문에 테스트가 실패할 수 있다. 따라서, 단위 테스트에서는 보통 인증 과정을 생략하고 모의(Mock) 객체를 이용하여 단순화했다.

단위 테스트 인증 과정을 생략하기

WebSecurityConfigurerAdapter는 보안 구성을 변경하게 해주는 클래스로, 이를 상속받아 원하는 보안 설정을 오버라이드할 수 있다.

아래 코드와 같이 TestSecurityConfig 클래스에서 모든 요청을 허용하는 보안 설정을 만들어 JWT 인증을 무시하도록 설정했다.
(인증 절차와 별개로 실제 비즈니스 로직에 집중하기 위함)

@TestConfiguration
public class TestSecurityConfig extends WebSecurityConfigurerAdapter { // JWT 인증과정을 무시하기 위한 테스트용 시큐리티 config
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .anyRequest().permitAll();
    }
}

@WebMvcTest 어노테이션과 @Import 어노테이션을 이용해 이 설정을 테스트 클래스에 적용할 수 있다. @Import 어노테이션을 통해 TestSecurityConfig를 가져와서 원래의 보안 설정 대신 사용하게 하여, 이 설정이 테스트에서 사용되게 적용했다.

@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
@Import(TestSecurityConfig.class) // JWT 인증과정을 무시하기 위해 사용
public class MemberControllerTest {
	
    // test코드 작성
    
}

Authentication 객체 생성하기

스프링 시큐리티에서는 Authentication 객체를 사용하여 사용자의 인증 정보를 관리한다. Authentication 객체는 사용자의 ID, 비밀번호, 권한 등의 인증 정보를 포함하고 있다. 따라서, 테스트에서 인증 과정을 생략하려면 Authentication 객체를 직접 생성하여 사용해야 한다.

스프링 시큐리티는 Authentication 객체를 생성하는 여러 가지 방법을 제공하는데, 이 중에서 TestingAuthenticationToken을 사용하는 방법이 가장 간단하다. TestingAuthenticationToken은 테스트 용도로 만들어진 Authentication 구현체로서, 사용자 ID, 비밀번호, 권한 등을 직접 지정할 수 있다.

Authentication authentication = new TestingAuthenticationToken("test1@gmail.com", null, "ROLE_USER");

MockMvc 요청에 Authentication 객체 추가하기

생성한 Authentication 객체를 MockMvc 요청에 추가하려면 SecurityMockMvcRequestPostProcessorsauthentication() 메서드를 사용하면 된다.

SecurityMockMvcRequestPostProcessors.authentication(Authentication) 메서드를 사용하면, 주어진 Authentication 객체로 SecurityContext를 설정하고 이를 MockMvc 요청에 추가할 수 있다. 이렇게 하면 해당 요청은 인증된 것처럼 처리되므로, 인증에 의존하는 로직을 테스트할 수 있게 된다.

즉, 이 메서드는 Authentication 객체를 받아서 SecurityContext를 설정하고 이 SecurityContext를 현재 요청의 보안 컨텍스트로 설정하는거다. 이렇게 함으로써, 해당 요청은 주어진 Authentication 객체에 의해 인증된 것처럼 처리되므로, 인증이 필요한 로직을 테스트할 수 있다.

mockMvc.perform(
    patch("/members")
        // ...
        .with(authentication(authentication))
)

정리

스프링 시큐리티의 JWT 인증 과정을 단위 테스트에서 생략하거나 단순화하려면 TestingAuthenticationToken을 이용하여 Authentication 객체를 직접 생성하고, SecurityMockMvcRequestPostProcessors.authentication() 메서드를 이용하여 이 객체를 MockMvc 요청에 추가하면 된다. 이렇게 하면 인증 과정을 거치지 않고도 인증이 필요한 요청을 테스트할 수 있다.

적용 예시 풀 코드

@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
@Import(TestSecurityConfig.class) // JWT 인증과정을 무시하기 위해 사용
public class MemberControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private MemberService memberService;

    @MockBean
    private MemberMapper mapper;

    @Autowired
    private Gson gson;
    @MockBean
    private MemberUtils memberUtils;
   	
    @Test
    public void patchMemberTest() throws Exception {
        MemberDto.Patch patch = new MemberDto.Patch("changedNickName222", "changedPW222");
        String content = gson.toJson(patch);

        MemberJoinResponseDto responseDto = new MemberJoinResponseDto(makeUuid(),
                "test1@gmail.com",
                "https://avatars.githubusercontent.com/u/120456261?v=4",
                "changedNickName222",
                "",
                "",
                Member.MemberStatus.MEMBER_ACTIVE,
                List.of("user"));



        Authentication atc = new TestingAuthenticationToken("test1@gmail.com", null, "USER");

        given(mapper.memberPatchToMember(Mockito.any(MemberDto.Patch.class))).willReturn(new Member());

        given(memberService.updateMember(Mockito.any(Member.class),Mockito.anyString())).willReturn(new Member());

        given(mapper.memberToMemberResponse(Mockito.any(Member.class))).willReturn(responseDto);

        ResultActions actions =
                mockMvc.perform(
                        patch("/members")
                                .header(HttpHeaders.AUTHORIZATION, "Bearer " + "eyJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJVU0VSIl0sIm1lbWJlckVtYWlsIjoidGVzdDFAZ21haWwuY29tIiwic3ViIjoidGVzdDFAZ21haWwuY29tIiwiaWF0IjoxNjgxODIxOTEwLCJleHAiOjE2ODE4MjM3MTB9.h_V93dhS-RhzqVdYuRkxHHIxYjG61LSn87a_8HtpBgM") // JWT 토큰 값 설정
                                .accept(MediaType.APPLICATION_JSON)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(content)
                                .with(authentication(atc))
                );

        actions
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.email").value(responseDto.getEmail()))
                .andExpect(jsonPath("$.profileImage").value(responseDto.getProfileImage()))
                .andExpect(jsonPath("$.nickName").value(patch.getNickName()))
                .andDo(document("patch-member",
                        preprocessRequest(prettyPrint()),
                        preprocessResponse(prettyPrint()),
                        requestHeaders(
                                headerWithName("Authorization").description("JWT Access토큰")
                        ),
                        requestFields(
                                List.of(
                                        fieldWithPath("nickName").type(JsonFieldType.STRING).description("닉네임").optional(),
                                        fieldWithPath("password").type(JsonFieldType.STRING).description("비밀번호").optional()
                                )
                        ),
                        responseFields(
                                List.of(
                                        fieldWithPath("uuid").type(JsonFieldType.STRING).description("uuid"),
                                        fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"),
                                        fieldWithPath("profileImage").type(JsonFieldType.STRING).description("프로필 이미지"),
                                        fieldWithPath("nickName").type(JsonFieldType.STRING).description("닉네임"),
                                        fieldWithPath("aboutMe").type(JsonFieldType.STRING).description("자기소개"),
                                        fieldWithPath("withMe").type(JsonFieldType.STRING).description("함께하고 싶은 유형"),
                                        fieldWithPath("memberStatus").type(JsonFieldType.STRING).description("활동상태"),
                                        fieldWithPath("roles").type(JsonFieldType.ARRAY).description("권한")
                                )
                        )
                ));
    }
}

0개의 댓글