WebMvcTest를 적용한 Controller Test Code에 Spring Security 추가하기

Denia·2024년 1월 14일
0

TroubleShooting

목록 보기
12/25
post-custom-banner

처음에는 사이드 프로젝트의 MVP를 빠르게 만들기 위해서 spring security 없이 개발을 했었다.

그 이후에 로그인 기능을 추가하기 위해서 spring security를 적용하고 로그인 및 JWT 관련 로직도 추가했다.

그 후 개발한 로직을 검사하기 위해서 테스트 코드를 추가하던 중 기존의 WebMvcTest를 이용하여 작성한 controller test의 테스트 코드가 다 실패하는 것을 발견했다.

나는 현재 controller testservice test, repository test 이렇게 레이어 별로 테스트를 진행하고 있다.

service test, repository test@SpringBootTest를 사용해서 테스트하고 있으며, controller test@WebMvcTest를 사용해서 테스트하고 있다.

spring security 적용 후 @SpringBootTest 는 모든 Bean을 가져와서 적용하기 때문에 별 문제가 없었는데, @WebMvcTest는 예외적으로 테스트에 필요한 Bean들만 선별적으로 가져오다보니 테스트 코드 실행이 계속해서 실패했다.

해당 글에서는 spring security를 적용한 후에 어떻게 해야 기존의 테스트 코드랑 새로 추가할 테스트 코드를 정상적으로 검증할 수 있는지 자료를 찾아보고 내 프로젝트에 적용한 내용을 기술하겠다.


1. security-test 의존성 추가하기

security 관련 설정을 Test에도 적용하기 위해서는 관련 의존성을 추가해줘야 한다.

testImplementation 'org.springframework.security:spring-security-test'

2. controller Test에서 사용할 security configuration 설정하기

@WebMvcTest 에서 따로 @Configuration를 적용시키지 않으면, security 관련 설정은 spring security 기본 설정 (SpringBootWebSecurityConfiguration.class 에서 생성되는 defaultSecurityFilterChain Bean)으로 적용이 된다.

SpringBootWebSecurityConfiguration.class

//defaultSecurityFilterChain

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {
    SecurityFilterChainConfiguration() {
    }

    @Bean
    @Order(2147483642)
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((requests) -> {
            ((AuthorizeHttpRequestsConfigurer.AuthorizedUrl)requests.anyRequest()).authenticated();
        });
        http.formLogin(Customizer.withDefaults());
        http.httpBasic(Customizer.withDefaults());
        return (SecurityFilterChain)http.build();
    }
}

그렇게 되면 우리가 설정한 허용 url 설정csrf 해제 설정도 풀리기 때문에 여러가지로 귀찮아 진다.

csrf 설정을 따로 해제하지 않으면 매번 mockMvc 요청마다 with(csrf())를 붙여줘야 해서 여간 귀찮은게 아니다. (※ 나는 jwt를 사용중이므로 csrf 설정을 해제해서 사용하고 있다.)

그러므로 테스트 코드에서도 security 설정을 적용시키려면 애플리케이션에서 사용중인 security 설정을 가져와서 다시 설정을 해줘야 한다. (테스트 코드용으로만 별도로 세팅을 해줘도 되지만, 에플리케이션 세팅이랑 동일하게 가져가는게 더 낫다고 판단했다.)

이렇게 따로 Test에서 사용할 Security Configuration 클래스를 생성하고, 기존의 Security Configuration 클래스를 가져와서 Test용 Configuration Bean으로 만들어주자.

외부에서 의존성 주입이 필요한데 만약 따로 생성되지 않는 Bean들이라면 임의로 본인 코드에 따라 적절하게 객체를 만들어서 주입해주자.

내 코드 같은 경우에는 JwtAuthenticationFilter, AuthenticationProvider 가 필요한데, 생성되지 않는 Bean들이라서 내가 직접 주입해주고 있다. (LogoutHandlerMockBean 으로 처리했다.)

MyCustomTestSecurityConfiguration.java

// SecurityConfiguration 클래스는 내가 애플리케이션에서 사용중인 Security 설정 클래스다.
// csrf를 해제 + 특정 url에 대해서 허용 + logout 설정 등등

// 기존에 생성되는 Bean을 덮어씌우기 위해서 defaultSecurityFilterChain 이름으로 Bean을 생성했다.

@Configuration
public class MyCustomTestSecurityConfiguration {
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, LogoutHandler mylogoutHandler) throws Exception {
        final JwtAuthenticationFilter jwtAuthFilterForTest = new JwtAuthenticationFilter(null, null);
        final AuthenticationProvider authenticationProviderForTest = new TestingAuthenticationProvider();

        return new SecurityConfiguration(
                jwtAuthFilterForTest,
                authenticationProviderForTest,
                mylogoutHandler)
                .securityFilterChain(http);
    }
}

ControllerTest.java

@WebMvcTest(
        controllers = {
                AuthenticationController.class
        },
        includeFilters = {
                @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = MyCustomTestSecurityConfiguration.class)
        }
)
public class ControllerTest {
    @Autowired
    protected MockMvc mockMvc;

    @MockBean
    protected MyLogoutHandler myLogoutHandler;
}

3. 401 Unauthorized 에러 처리를 위해 모의 인증용 어노테이션 적용하기

내가 구현한 API 중에는 인증을 받아야지만 접근이 가능한 몇몇 요청이 있다.

이럴 경우 모의 인증 객체를 통해 인증 받았다고 test 프레임워크에게 알려줘야 정상적으로 테스트가 가능하다.

@WithMockUser, @WithUserDetails와 같은 모의 인증용 어노테이션을 붙여주면 된다.

나는 매번 @WithMockUser 에 값을 따로 설정해주기 싫어서 custom한 MockUser를 만들어서 사용했다.

  • ID는 기본 형식으로는 email 형식, default 값으로 "test@test.com" 를 사용했다.
  • Role은 Role.USER 를 default로 사용했다.

WithMyCustomUser.java

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMyCustomUserSecurityContextFactory.class)
public @interface WithMyCustomUser {
    String email() default "test@test.com";

    Role role() default Role.USER;
}

WithMyCustomUser 어노테이션 의 값을 가져와서 Authentication을 생성한다.

WithMyCustomUserSecurityContextFactory.java

public class WithMyCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMyCustomUser> {
    @Override
    public SecurityContext createSecurityContext(WithMyCustomUser annotation) {
        String email = annotation.email();
        Role role = annotation.role();

        Authentication auth = new UsernamePasswordAuthenticationToken(
                email,
                "",
                buildGrantedAuthoritiesFromRole(role)
        );

        SecurityContext context = SecurityContextHolder.getContext();
        context.setAuthentication(auth);

        return context;
    }

    private List<GrantedAuthority> buildGrantedAuthoritiesFromRole(final Role role) {
        return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
    }
}

인증이 꼭 필요한 API를 테스트할 때는 해당 테스트 메서드 위에 @WithMyCustomUser 를 붙여주면 된다.

※ 참고 블로그

Spring Security 추가로 인한 Test Code refactoring

WebMvcTest와 Spring Security 함께 사용하기

@WebMvcTest 에서 Spring Security 적용, 401/403 에러 해결하기 - csrf

[Spring Security + JWT] Spring Security Controller Unit Test하기

profile
HW -> FW -> Web
post-custom-banner

0개의 댓글