Java/Spring2019. 3. 10. 23:26
반응형

 Spring Security(스프링 시큐리티)를 사용하는 경우 단위테스트를 하는 방법을 알아보겠습니다. 

※이글에서 소개하는 내용은 Spring Security 4.1에서 추가된 내용입니다.


 Spring Security는 SecurityContext를 통해서 현재 실행중인 스레드와 연관된 인증정보를 관리합니다. 따라서 테스트 실행시에 SecurityContextHolder를 통해서 인증정보를 설정하면 로그인된 상태등의 인증관련 테스트가 가능합니다.


    //...
    @Before
    public void setUp() {
        SecurityContextHolder.getContext()
            .setAuthentication(new UsernamePasswordAuthenticationToken("username", "password", Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))));
    }
    //...

※위와 같이 단위테스트 실행전에 인증정보를 설정하면 로그인된 상태로 테스트가 가능합니다. 하지만 위와 같은 코드는 역시 스프링 스럽지 못하다는 왠지 느낌적인 느낌이 듭니다.



 Spring Security 4.1부터 아래와 같은 테스팅 피처가 지원되기 시작했습니다.


  • @WithAnonymousUser - 익명유저의 인증정보를 설정하기 위한 어노테이션
  • @WithUserDetails - UserDetailsService를 통해서 유저정보를 취득하여 설정하기 위한 어노테이션(4.0부터 지원하고 4.1부터 Spring빈의 이름을 지정할 수 있도록 추가됨)
  • @WithMockUser - 별도의 UserDetailsService와 같은 스텁을 제공하지 않아도 간단하게 인증정보를 설정하기 위한 어노테이션(4.0부터 지원되었으나 4.1부터 작동하는것으로 보임)



그럼 각 어노테이션들의 사용법에 대해서 간단하게 알아보도록 하겠습니다.



 첫번째로 @WithAnonymousUser 의 경우 인증되지 않은 상태를 설정하기 위한 어노테이션입니다. 인증하지 않는 경우를 나타내는 어노테이션이 왜 필요할까하지만 위에서 언급한것 처럼 Spring Security는 현재 실행중인 스레드에 연관지어 인증정보를 관리하므로 한번 인증정보를 설정해서 테스트를 진행한 경우에 인증상태를 초기화 시키는데 필요합니다.

    //...
    @Test
    @WithAnonymousUser
    public void some_test() {
        ...
    }
    //...



 두번째로 @WithUserDetails 의 경우는 Spring Security에서 유저정보를 조회하기 위해서 사용하는 UserDetailsService를 이용하여 인증정보를 설정하는 어노테이션입니다. 


    //...
    @Bean
    @Profile("test")
    public UserDetailsService userDetailsService() {
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                return User
                        .withUsername(username)
                        .password("password")
                        .authorities(new SimpleGrantedAuthority("ROLE_USER"))
                        .build();
            }
        };
    }
    //...
 위와같이 테스트용 UserDetailsService빈을 등록하여 사용합니다.
    //...
    @Test
    @WithUserDetails("test_user")
    public void some_test() {
        ...
    }
    //...



 세번째로 @WithMockUser는 단순하게 username과 role정도만 설정하면 인증정보를 설정해주는 어노테이션입니다. 별다른 설정을 추가하지 않고도 사용할 수 있어서 매우 편리할 듯 합니다.


    //...
    @Test
    @WithMockUser(username = "username", roles = "USER")
    public void some_test() {
        ...
    }
    //...


 대부분의 경우는 위의 어노테이션을 이용하면 테스트가 가능할 것으로 생각되지만 인증주체에 관한정보를 UserDetails를 사용하지 않는 경우에는 별도의 커스텀 어노테이션을 만들어서 사용할 수 있습니다.


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

    String userNo() default "1";

    String userId() default "user";

    String name() default "name";

}
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithCustomMockUser> {
    @Override
    public SecurityContext createSecurityContext(WithCustomMockUser user) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        CustomUser principal = new CustomUser(Long.valueOf(user.memberNo()), user.userId(), user.name());
        Authentication auth = new UsernamePasswordAuthenticationToken(principal, "password", Arrays.asList(new SimpleGrantedAuthority("ROLE_MEMBER")));
        context.setAuthentication(auth);
        return context;
    }
}

 위와같이 커스텀 어노테이션을 만든 후 기본 어노테이션과 동일한 방법으로 사용하면 커스텀 인증정보가 설정된 상태로 테스트를 진행할 수 있습니다.

    //...
    @Test
    @WithCustomMockUser
    public void some_test() {
        ...
    }
    //...


 또한 아래와 같이 어노테이션을 정의하여 반복되는 설정을 간소화 할 수도 있습니다.

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="admin",roles="ADMIN")
public @interface WithCustomMockAdmin {
}
//...
@Test
@WithCustomMockAdmin
public void some_test() {
    ...
}
//...



 기존에는 로그인 상태를 테스트 할때 장황한 코드가 필요했었는데 Spring Security에서 제공하는 테스팅 피처를 이용하면 간단하게 로그인 상태 별로 테스트가 가능하므로 어플리케이션을 좀더 안전한상태로 개발할 수 있을것 같습니다.




Spring Security 레퍼런스

Posted by Reiphiel