Spring/Test-Driven Develop

[Spring Security] @AuthenticationPrincipal 유닛 테스트 - Custom Mock User 삽입하기

민돌v 2023. 6. 15. 21:20
✨ 이번 포스팅에서는, @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()));
}
  1. core user에는 username, password, role 만이 들어가 있기 때문이기도 하고
  2. @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 시리즈======================