BackEnd/Spring

[Spring] Spring Security에서 @WithMockUser를 커스텀하기

샤아이인 2023. 1. 2.

 

이번 글을 직전 글에서 발생한 문제를 해결하기 위해 @WithMockUser를 커스텀하여 사용하는 글을 작성해볼려 한다.

 

직전글을 꼭 읽을 필요는 없다. @WithMockUser를 커스텀하여 사용하는 방법은 이번 글만 보면된다.

(다만 문제가 발생한 상황 이 궁금하다면 직전 글 도 살펴보길!)

https://blogshine.tistory.com/626

 

[Spring] SpringSecurity에서 @AuthenticationPrincipal 대신 DTO로 받기

1. 문제의 상황 현 술술 애플리케이션에서는 로그인 한 사용자가 Controller에 접근할 때 CustomUser 객체를 인자로 전달받는다. 문제는 CustomUser가 도메인의 순수한 핵심 Entity라는 점이다... 또한 SonarQu

blogshine.tistory.com

 

1. 문제되는 상황

기존의 @WithMockUser를 사용하는 방식은 가장 쉽게 유저를 설정해 테스트를 진행하는 방법이다.

위 소스코드에서 살펴볼 수 있듯, usernamepasswordroles 등 항목을 지정할수 있으며, default값 또한 설정되어 있다.

 

@WithMockUser 테스트에 추가하면 WithMockUserSecurityContextFactory를 통해 Authentication을 생성하고 등록한다.

이때 Authentication의 principal로 등록되는 타입은 User 타입이다.

org.springframework.security.core.userdetails.User

 

다음과 같이 사용하면, test라는 유저명을 가진 mock 유저를 생성한다.

생성된 principal로 User를 저장하고 있는 Authentication이 SecurityContext에도 등록되게 된다.

@WithMockUser(username = "test", roles = "USER")
@Test
void someTest() { ... }

하지만 커스텀된 Authentication 인증 정보는 사용할 수 없다.

 

나는 Authentication으로 User 타입이 아니라, CustomUser객체를 등록시키고 싶다.

 

좀 더 유연하게 원하는 Authentication을 사용하고자 한다면 @WithSecurityContext를 사용해야 한다.

 

2.  해결하기

2 - 1) @WithSecurityContext

우선 우리만의 애노테이션을 다음과 같이 만들어준다.

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = MockCustomUserSecurityContextFactory.class)
public @interface MockCustomUser {

    String value() default "user";

    String username() default "";

    String[] roles() default { "USER" };

    String password() default "password";

    @AliasFor(annotation = WithSecurityContext.class)
    TestExecutionEvent setupBefore() default TestExecutionEvent.TEST_METHOD;
}

 

애노테이션 생성 시 @WithSecurityContext애노테이션을 붙여주어야 한다.


@WithSecurityContext 애노테이션은 스프링 시큐리티 테스트용 SecurityContext를 만들겠다고 명시하는 것 이다.

또한 factory라는 값을 필수로 입력해야 하는데, 해당 클래스에서 우리가 만든 @MockCustomUser을 위한 새로운 SecurityContext를 생성해 전달해야 한다.

 

2 - 2) MockCustomUserSecurityContextFactory

위에서 @WithSecurityContext애노테이션에 전달한 MockCustomUserSecurityContextFactory클래스를 만든다.
이 클래스는 WithSecurityContextFactory<MockCustomUser> 구현하여 작성한다.

public class MockCustomUserSecurityContextFactory implements WithSecurityContextFactory<MockCustomUser> {

    @Override
    public SecurityContext createSecurityContext(MockCustomUser annotation) {

        String username = StringUtils.hasLength(annotation.username()) ? annotation.username() : annotation.value();
        Assert.notNull(username, () -> annotation + " cannot have null username on both username and value properties");

        CustomUser customUser = new CustomUser(username, annotation.password());
        Authentication authentication = UsernamePasswordAuthenticationToken.authenticated(customUser, "", settingRole(annotation));

        SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
        securityContext.setAuthentication(authentication);
        
        return securityContext;
    }

    private List<GrantedAuthority> settingRole(MockCustomUser annotation) {
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();

        for (String role : annotation.roles()) {
            Assert.isTrue(!role.startsWith("ROLE_"), () -> "roles cannot start with ROLE_ Got " + role);
            grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role));
        }

        return grantedAuthorities;
    }
}

 

이제 @MockCustomUser애노테이션을 사용하여 테스트를 수행하면 된다!

지난번 글에서 실패했었던 테스트를 다음과 같이 @MockCustomUser를 통해 해결해봅시다!

@DisplayName("문서화 : 사용자 회원 탈퇴")
@MockCustomUser
@Test
public void user_delete() throws Exception {

    doNothing().when(userDetailsService).delete(any());

    mockMvc.perform(delete("/api/auth")
                    .header("Authorization", "bearer login-jwt-token")
                    .accept(MediaTypes.APPLICATION_JSON)
            )
            .andExpect(status().isOk())
            .andDo(
                    document("user-delete-auth",
                            userDeleteResponseBody())
            );
}

성공적으로 테스트가 통과하는 것 을 확인할수 있게 되었다.

댓글