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
객체는 사용자의 ID, 비밀번호, 권한 등의 인증 정보를 포함하고 있다. 따라서, 테스트에서 인증 과정을 생략하려면 Authentication
객체를 직접 생성하여 사용해야 한다.
스프링 시큐리티는 Authentication
객체를 생성하는 여러 가지 방법을 제공하는데, 이 중에서 TestingAuthenticationToken
을 사용하는 방법이 가장 간단하다. TestingAuthenticationToken
은 테스트 용도로 만들어진 Authentication
구현체로서, 사용자 ID, 비밀번호, 권한 등을 직접 지정할 수 있다.
Authentication authentication = new TestingAuthenticationToken("test1@gmail.com", null, "ROLE_USER");
생성한 Authentication
객체를 MockMvc 요청에 추가하려면 SecurityMockMvcRequestPostProcessors
의 authentication()
메서드를 사용하면 된다.
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("권한")
)
)
));
}
}