개발/Test

Test / Spring Security / 단위 테스트에 Security 추가하기

highright96 2021. 6. 15.

Spring Security / 단위 테스트에 Security 추가하기

프로젝트에 Spring Security를 적용하면 전에는 성공하던 테스트가 실패하게 된다. Spring Security가 인증을 받지 못한 사용자의 접근을 막기 때문이다. 

그럼 어떻게 인증된 사용자를 만들어 테스트를 성공할 수 있을까?

Spring Security는 이를 위해 여러 애노테이션을 제공한다.

1. @WithMockUser

@Test
@WithMockUser
void getMessageWithMockUser() {
  String message = messageService.getMessage();
  ...
}

@WithMockUser 애노테이션을 붙인 테스트 메서드는 username : "user", password : "password"이며, role이 "ROLE_USER"인 인증된 유저로 테스트가 실행된다.

@WithMockUser 애노테이션으로 인증된 유저를 생성하려면 다음과 같은 조건이 있다.

  1. "user"라는 이름을 가진 유저는 실제로 없어도 된다.
  2. UsernamePasswordAuthenticationToken 타입의 인증 객체가 SecurityContext에 생성된다.
  3. 인증 객체 내의 principal은 Spring Security의 User 객체여야 한다.
  4. User 객체는 username이 "user", password가 "password", role이 "ROLE_USER"이다.

만약 다른 username, role을 갖게 하고 싶으면 다음과 같이 작성하면 된다.

@Test
@WithMockUser(username="admin",roles={"USER","ADMIN"})
void getMessageWithMockUserCustomUser() {
    String message = messageService.getMessage();
    ...
}

테스트 클래스에 붙여 속한 모든 테스트 메서드에 적용시킬 수 있다.

@Test
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public void getMessageWithMockUserCustomUser() {
    String message = messageService.getMessage();
    ...
}

주의할 점은 JWT와 같은 커스텀 filter를 생성해 인증(Authentication) 객체를 생성하면 @WithMockUser 애노테이션을 사용할 수 없어, 커스텀한 애노테이션을 만들어야 한다.

2. @WithAnonymousUser

익명의 사용자로 테스트를 하고 싶을 때 사용한다.

@Test
@WithAnonymousUser
void anonymous() throws Exception {

}

3. @WithUserDetails

커스텀한 principal, UserDetailsService를 통해 인증(Authentication) 객체를 생성하면 @WithMockUser 애노테이션을 사용할 수 없으므로, @WithUserDetails 애노테이션을 사용해 커스텀한 userDetailsService 빈 이름을 명시해 테스트 사용자를 만들어야 한다.

@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
public void getMessageWithUserDetailsCustomUsername() {
    String message = messageService.getMessage();
    ...
}

위 테스트는 myUserDetailsService라는 이름의 UserDetailsService 빈을 사용해서 사용자 이름(username)이 customUsername인 사용자를 찾는다.

@WithUserDetails 애노테이션은 @WithMockUser 애노테이션과 다르게 실제로 사용자가 있어야 한다.

따라서 JUnit의 @Before 애노테이션을 붙인 테스트 메서드에서 사용자를 등록한 후 위 테스트를 실행시켜야 한다.

이때 주의할 점이 있다. 기본적으로 SecurityContext에 테스트 사용자를 등록하는 이벤트는 @Before 애노테이션 전에 실행된다. 따라서 @Before 실행 후, 테스트 메서드 실행 전에 테스트 사용자를 등록할 수 있도록 다음과 같이 설정해야 한다.

@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)

4. @WithSecurityContext

위에서 말했듯이 커스텀한 principal, UserDetailsService을 사용해 인증(Authentication) 객체를 만들면 @WithMockUser 애노테이션을 사용할 수 없다.

그럼 @WithUserDetails 애노테이션을 사용해야 할까? 이 방법은 테스트 사용자가 실제로 존재해야 해서 테스트가 복잡해질 수 있다. 

따라서 직접 SecuritContext를 생성해주는 @WithSecurityContext를 사용해 직접 애노테이션을 만들어야 한다. 이 방법이 가장 유연하게 사용할 수 있는 옵션이다.

아래와 같은 @WithMockCustomUser 애노테이션을 생성할 수 있다.

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {

    String username() default "rob";

    String name() default "Rob Winch";
}

위 코드를 보면 WithMockCustomUser에는 @WithSecurityContext 애노테이션을 사용했다. 이는 SecuriyContext를 생성해서 테스트를 실행하라는 신호로 받아들인다.

테스트 메서드에 @WithMockCustomUser 애노테이션을 선언했을 때 SecuriyContext에 인증(Authentication) 객체를 저장할 방식을 정해야 한다.

WithMockCustomUserSecurityContextFactory 구현은 아래와 같다.

public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> {
    @Override
    public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();

        CustomUserDetails principal =
            new CustomUserDetails(customUser.name(), customUser.username());
        Authentication auth =
            new UsernamePasswordAuthenticationToken(principal, "password", principal.getAuthorities());
        context.setAuthentication(auth);
        return context;
    }
}

Spring Security 공식 문서에 작성된 예시이므로 본인의 방식에 맞춰 커스텀하게 구현하면 된다.

5. 참고

'개발 > Test' 카테고리의 다른 글

Test / JUnit5 / 테스트를 위한 애노테이션  (0) 2021.06.15

댓글