저번 게시물 에서, 세션 방식으로 간단히 OAuth2 인증을 처리하는 방법을 소개했다. 간단한 동작 방식은 아래와 같다.
원래는 깃허브 리뷰 할당 프로젝트를 개인 프로젝트로 하려다가, 프론트분들과 디자이너분들도 합류해서 같이 프로젝트를 진행하기로 변경했기 때문에 인증 방식을 좀 바꾸고 싶었다. (세션을 사용하지 않고, JWT에 사용자의 인증 정보를 암호화하여 담는 방식으로)
따라서 Security의 세션 config를 변경해야했다. 그리고 컨피그를 변경하면서, 테스트 코드를 어떻게 변경해야 할 지도 고민했다. (세션이 있는 상태에서, 없는 상태로 변경되었으니까)
그런데, 실패할 줄 알았던 세션을 사용하는 테스트가, 성공하는디?
@WebMvcTest 어노테이션을 활용한 테스트에서는 기본적으로 세션을 생성하지 않는다. 다음 테스트를 보자.
@WebMvcTest(controllers = {AuthController.class})
class StatelessSessionTest {
// ...
@Test
void testWebMvcTestUsesMockSessions() throws Exception {
MvcResult mvcResult = mockMvc.perform(get("/oauth2/login"))
.andExpect(status().isOk())
.andReturn();
HttpSession session = mvcResult.getRequest().getSession(false);
assertThat(session).isNull(); // 아무 설정이 없다면 세션을 생성하지 않는다.
}
}
그러나, 우리가 이전에 만든 커스텀 어노테이션에서 Security Context 를 설정했었다. 기억이 가물가물하니 다시 봐보자.
public class WithMockOAuth2UserSecurityContextFactory implements WithSecurityContextFactory<WithMockOAuth2User> {
@Override
public SecurityContext createSecurityContext(WithMockOAuth2User withMockOAuth2User) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Map<String, Object> attributes = Arrays.stream(withMockOAuth2User.attributes())
.map(attr -> attr.split(":"))
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
CustomOauth2User principal = new CustomOauth2User(
(String) attributes.get(withMockOAuth2User.nameAttributeKey())
);
context.setAuthentication(new OAuth2AuthenticationToken(principal, principal.getAuthorities(), "github"));
return context;
}
}
@WebMvcTest(controllers = {AuthController.class})
class StatelessSessionTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Test
@WithMockOAuth2User(attributes = {"name:99999"})
void testSecurityContextNotStoredInSession_Stateless() throws Exception {
MvcResult mvcResult = mockMvc.perform(get("/oauth2/login"))
.andExpect(status().isOk())
.andReturn();
HttpSession session = mvcResult.getRequest().getSession(false);
assertThat(session).isNull(); // 세션이 존재해야 하므로, 실패해야함.
}
@Test
void testWebMvcTestUsesMockSessions() throws Exception {
MvcResult mvcResult = mockMvc.perform(get("/oauth2/login"))
.andExpect(status().isOk())
.andReturn();
HttpSession session = mvcResult.getRequest().getSession(false);
assertThat(session).isNull(); // 아무 설정이 없다면 세션을 생성하지 않는다.
}
}
위 테스트는 실패한다. 우리가 설정한 어노테이션에서 시큐리티 컨텍스트를 집어넣어줬기 때문에.
이 테스트를 통해서
- @WebMvcTest 를 이용한 테스트 환경은, 별다른 설정이 없다면 시큐리티 관련 컨피그의 영향을 받지 않는다.
- 우리가 직접 만든 어노테이션과 컨텍스트 팩토리를 통해서 시큐리티 컨텍스트를 넣어 줄 수 있다. (MockHttpSession 이라는 객체로 저장된다)
는 사실을 알았다. 그렇다면, 세션이 있을 때와 없을 때를 테스트하기 위해서 어떤 방법을 써야 할까?
예~ 전에, @Import vs @ComponentScan 을 다룬 적이 있었다. 여기서 @Import 어노테이션은 어플리케이션 구동에 필요한 빈 팩토리에 클래스 하나(혹은 여러개) 를 추가 할 수 있다고 했었다.
그렇다면 @Import 어노테이션을 통해서 내가 직접 구성요소를 편집하여, 테스트 환경을 구성 할 수도 있을 것이다.
@TestConfiguration
public class IfRequiredSecurityConfig {
@Bean
@Primary
public SecurityFilterChain config(HttpSecurity http) throws Exception {
return http
.sessionManagement(a -> a.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
.build();
}
}
테스트코드도 작성해보자.
@WebMvcTest(controllers = {AuthController.class})
@Import(IfRequiredSecurityConfig.class)
@DisplayName("세션 상태가 IF_REQUIRED 일 때를 테스트한다.")
class WithSessionTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Test
@WithMockOAuth2User(attributes = {"name:99999"})
@DisplayName("SecurityContext 는 세션에 저장된다. (IF_REQUIRED)")
void testSecurityContextStoredInSession_IfRequired() throws Exception {
MvcResult mvcResult = mockMvc.perform(get("/oauth2/login"))
.andExpect(status().isOk())
.andReturn();
HttpSession session = mvcResult.getRequest().getSession(false);
assertThat(session).isNotNull();
SecurityContext securityContext = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT");
assertThat(securityContext).isNotNull();
OAuth2User principal = (OAuth2User) securityContext.getAuthentication().getPrincipal();
assertThat(principal.getName()).isEqualTo("99999");
}
}
오케이 여기까지는 뭐...그렇다면, STATELESS 설정으로 하면 어떻게될까?
똑같은 방식으로 컨피규레이션을 잡아준다.
@TestConfiguration
public class StatelessSecurityConfig {
@Bean
@Primary
public SecurityFilterChain config(HttpSecurity http) throws Exception {
return http
.sessionManagement(a -> a.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.build();
}
}
테스트코드도 작성한다.
@WebMvcTest(controllers = {AuthController.class})
@Import(StatelessSecurityConfig.class)
@DisplayName("세션 상태가 STATELESS 일 때를 테스트한다.")
class StatelessSessionTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Test
@WithMockOAuth2User(attributes = {"name:99999"})
@DisplayName("SecurityContext 는 세션에 저장되지 않는다. (STATELESS)")
void testSecurityContextNotStoredInSession_Stateless() throws Exception {
MvcResult mvcResult = mockMvc.perform(get("/oauth2/login"))
.andExpect(status().isOk())
.andReturn();
HttpSession session = mvcResult.getRequest().getSession(false);
assertThat(session).isNull(); // Stateless 설정에서는 세션이 null이어야 함
}
@Test
void testWebMvcTestUsesMockSessions() throws Exception {
MvcResult mvcResult = mockMvc.perform(get("/oauth2/login"))
.andExpect(status().isOk())
.andReturn();
HttpSession session = mvcResult.getRequest().getSession(false);
assertThat(session).isNull(); // 아무 설정이 없다면 세션을 생성하지 않는다.
}
}