[Spring Security] @AuthenticationPrincipal 유닛 테스트 - Custom Mock User 삽입하기
✨ 이번 포스팅에서는, @AuthenticationPrincipal 유닛 테스트에 대해서 기록해보고자 합니다.
현재 프로젝트는 JWT + Spring Security를 사용하고 있고, Spring Security 에서 제공해주는 User 객체가 아닌 실제 디비에 저장되어있는 Custom 한 User 정보를 이용해 인증된 유저를 SecurityContextHolder에 저장하는 방식으로 구현해놓았습니다.
참고 👉 Spring Security 가이드 (with. Spring boot 3.0) - 스프링 시큐리티란, 동작 과정, 사용 방법, JWT 발급
그렇기 때문에 요청이 들어오는 Rest API 에 담긴 JWT가 정상적으로 인증이 가능한 토큰이라면
→ 지속적으로 DB에 접근하지 않고도 로그인한 객체 정보를 가져오기 위해 사용하는 @AuthenticationPrincipal 어노테이션을 사용할 수 있습니다.
👏🏻 여기까지는 좋았는데 RestDocs 를 사용해서 Controller 러를 테스트하는데 문제가 있었습니다.
✔️ Spring Security Controller Unit Test
보통 스프링 시큐리티를 사용하는 Controller 는 @WithMockUser 를 사용하면 moking 유저를 사용해서 인증절차를 통과시킬 수 있습니다.
저도 잘 이용해 왔구요.
📌 하지만, @WithMockUser를 사용하면 Spring security.core.userdetails의 User객체가 들어가기 때문에 아래와같이 직접 생성한 특정 User 객체를 이용할 때는 Error가 발생합니다.
@GetMapping("chat/rooms")
public ResponseEntity<List<ChatRoomResponse>> findMyChatRoom(@AuthenticationPrincipal final User user) {
return ResponseEntity.ok(chatFacade.findMyRoom(user.getUserUuid()));
}
- core user에는 username, password, role 만이 들어가 있기 때문이기도 하고
- @AuthenticationPrincipal 로 인해 바인딩 되는 과정에서, 내가 주입받고자하는 instance 타입이 아니라면 바인딩이 되지 않기 때문입니다.
📌@AuthenticationPrincipal 이 Context에서 로그인한 User 객체를 바인딩하는 과정을 살펴보면
- 해당 정보를 불러온 User 객체(principal.getClss())와 바인딩하고자하는 파라미터 객체 (parameter.getParameterType()) 을 비교하여 맞지않으면 null 을 반환해줍니다.
↓
그렇기 때문에 Test 실행 시 NullPointException이 발생했습니다 !
@WebMvcTest(ChatController.class)
class ChatControllerTest extends ControllerTestConfig {
@Test
@WithMockUser
@DisplayName("이렇게하면 @Authentication 으로 바인딩될 떄 null 이 들어감")
void getChatRooms() throws Exception {
//then
ResultActions resultActions = mockMvc.perform(
RestDocumentationRequestBuilders.get("/rest-api/url")
.header("Authorization", "Bearer {ACCESS_TOKEN}")
).andDo(
document("RestDocs 문서",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
resource(
ResourceSnippetParameters.builder()
.requestFields()
.responseFields(
...
)
.responseSchema(Schema.schema("ChatRoomResponse"))
.build()
)
));
resultActions.andExpect(MockMvcResultMatchers.status().isOk());
}
}
✔️ @WithMockUser 에 Custom User 객체를 주입하는 방법
그렇다고 테스트를 코드를 위해 프러덕션 코드를 바꿀수 도 없으니,,,, 우리는 방법을 찾아야합니다.
제가 찾은 가장 우아하고(?) 테스트코드 또한 여기저기 지저분하게 작성하지 않은 방법은 Test 시 사용할 저만의 @WithMockUser 어노테이션을 만드는 것 입니다.
📌Custom 한 @WithMockUser 만들기
@WithCustomMockUser
- @WithSecurityContext 어노테이션을 사용하면 사용할 SecurityContext를 지정해줄 수 있습니다. 즉 어떤 객체로 바인딩 할 것인지를 직접 만들어서 적용할 수 있습니다
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithCustomMockUserSecurityContextFactory.class)
public @interface WithCustomMockUser {
String userUuid() default "userUuid";
String role() default "NORMAL";
}
WithCustomMockUserSecurityContextFactory.class
- 사용할 SecurityContext를 정의해줄 클래스입니다.
- WithSecuriryContextFactory 제네릭 바운디드 타입이 Annotation이므로 위에서 만들어준 WithMockUser를 타입으로 넣어줍니다.
public class WithCustomMockUserSecurityContextFactory implements WithSecurityContextFactory<WithCustomMockUser> {
@Override
public SecurityContext createSecurityContext(WithCustomMockUser annotation) {
String userUuid = annotation.userUuid();
String role = annotation.role();
//여기서 바인딩되어 반환할 객체를 정의해주면 됩니다
User user = UserFixture.make(userUuid, role);
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(user, "password", List.of(new SimpleGrantedAuthority(role)));
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(token);
return context;
}
}
ControllerTest.class
- 이렇게 해서 주입해줄 mok user를 어노테이션을 이용하여 커스텀하게 주입할 수 있었습니다.
@WebMvcTest(ChatController.class)
class ChatControllerTest extends ControllerTestConfig {
@Test
@WithCustomMockUser
@DisplayName("이렇게하면 @Authentication 으로 바인딩될 떄 null 이 들어감")
void getChatRooms() throws Exception {
//then
ResultActions resultActions = mockMvc.perform(
RestDocumentationRequestBuilders.get("/rest-api/url")
.header("Authorization", "Bearer {ACCESS_TOKEN}")
).andDo(
document("RestDocs 문서",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
resource(
ResourceSnippetParameters.builder()
.requestFields()
.responseFields(
...
)
.responseSchema(Schema.schema("ChatRoomResponse"))
.build()
)
));
resultActions.andExpect(MockMvcResultMatchers.status().isOk());
}
}
만족스러운 스프링 시큐리티 mock user inject 방법이었습니다
끝!
✅ Spring Security
컨트롤러 유닛 테스트
@AuthenticationPrincipal Mocking 하기
====================== 🫡 Spring Security 시리즈======================