이전 글에서 Controller
에서 현재 로그인한 유저의 ID
만을 받는 어노테이션을 만들어 사용했다.
현재 인증된 유저의 ID를 컨트롤러에서 받을 수 있는 어노테이션을 컨트롤러 테스트에서 사용하기 위해 시큐리티 컨텍스트를 설정하는 과정에 대해 공부하고 정리한 글이다.
@Controller
계층의 단위테스트를 위해 Mockito
를 사용해 StandaloneSetup
으로 빌드해 사용했다.
@ExtendWith(MockitoExtension.class)
public class AddressControllerTest {
@Mock
private AddressService addressService;
@InjectMocks
private AddressController addressController;
@Captor
private ArgumentCaptor<CreateAddressRequest> saveAddressDtoCaptor;
@Captor
private ArgumentCaptor<User> userCaptor;
private MockMvc mockMvc;
@BeforeEach
public void beforeEach() {
mockMvc = MockMvcBuilders
.standaloneSetup(addressController)
.setControllerAdvice(GlobalExceptionHandler.class)
.build();
}
@Test
public void 정상주소추가() throws Exception {
// given
...
CustomUserDetails customUserDetails = new CustomUserDetails(user);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(customUserDetails, "password", customUserDetails.getAuthorities());
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authToken);
...
// when
ResultActions perform = mockMvc.perform(MockMvcRequestBuilders
.post("/api/address")
.with(securityContext(context))
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody.toString()));
...
}
하지만 이 방법의 경우 컨트롤러에 대한 단위테스트는 가능하지만 다른 기능들을 일체 사용하지 않아 스프링 시큐리티의 기능을 사용할 수 없었다.
mockMvc.perform
에서 아무리 SecurityContext
를 적용하려 해도 적용되지 않았다.
필터와 동일한 방식으로 SecurityContext
를 설정하고, perform 에 이 시큐리티를 적용해 보았지만, 정작 컨트롤러에서는 SecurityContext
를 확인할 수 없었다.
아래 사진은 AddressController
에서 SecurityContext
를 확인한 결과다.
테스트 코드에서 @WithMockUser
, SecurityMockMvcRequestPostProcessors.authentication
, SecurityMockMvcRequestPostProcessors.user
등을 사용해봐도 SecurityContext
는 설정되지 않았다.
Controller
에서 스프링 시큐리티를 사용하기 위해 다른 테스트 방법을 찾아봤다.
@SpringBootTest
는 컨트롤러 테스트에는 너무 무거울것 같았고 스프링 시큐리티를 기본으로 포함하는 @WebMvcTest
가 가장 적당했다.
의존이 필요한 AddressService
는 MockBean
으로 등록해줬다.
@WebMvcTest(AddressController.class)
public class AddressControllerTest2 {
@MockBean
private AddressService addressService;
@Autowired
private MockMvc mockMvc;
Controller의 메서드에서 SecurityContext 가 잡히는지 로그를 찍어보려 했다.
하지만
403 에러와 함께 컨트롤러까지 가지도 못하고 종료되어 버렸다.
원인을 찾아보니, csrf 필터가 동작하는데, 이 토큰정보를 전달해주지 않아 생긴 문제였다.
이 문제를 해결하기 위해 csrf 필터가 disable된 filterChain
을 리턴하는 TestSecurityConfig
를 만들어 사용했다. (추후 알게된 사실이지만, mockMvc.perform에 with(csrf())
를 넣어주면 해결되긴 했다.)
csrf 문제를 해결하고, 진짜 SecurityContext
를 살펴보기 위해 1.
에서 했던 방법과 동일하게 context를 전달해 주었다.
CustomUserDetails customUserDetails = new CustomUserDetails(u);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authToken);
// when
ResultActions perform = mockMvc.perform(MockMvcRequestBuilders
.post("/api/address")
.with(csrf())
.with(securityContext(context))
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody.toString()));
그리고 Controller
에서 SecurityContext
를 잘 받는것을 확인할 수 있었다.
컨트롤러 레이어의 단위테스트를 진행하며, SecurityContext
를 어떻게 설정해줄지에 대한 문제였다.
처음에는 진짜 Controller
하나만 테스트하는 너무 작은 단위 테스트여서 스프링 시큐리티가 동작하지 않았다.
하지만 비즈니스로직에 스프링 시큐리티를 배제할 수 없었기 때문에 스프링 시큐리티를 참여해야 겠다고 생각헀다.
@WebMvcTest
를 쓰기로 하고 겪은 여러가지 문제들도 있었다.
어쨌든 이번 경험으로 스프링 시큐리티에 대한 이해가 한층 는것 같다.
사실
Jwt
구현을 하면서도SecurityContext
의 목적에 대해 잘 모르고 있었고, 컨트롤러 레이어에서도 그냥 받아서 사용할 수 있었기 때문에 큰 생각없이 사용하고 있었다.JwtFilter가 왜 Username필터 앞에 오는지, 만약 토큰이 제대로 되어있지 않아도 왜 doFilter 로 넘기는지, 토큰이 검증되면 왜 Context를 설정하는지에 대한 개념이 와닿지 않았었다.
대충 설명하자면 토큰이 있다면 SecurityContext를 설정해 로그인 핉터(UsernamePassword)에서 로그인 동작을 넘어가고, Context가 없다면 로그인 필터에서 로그인 동작을 수행하기 때문이다.
하지만 테스트를 진행하며 개념에 대한 이해가 필요한 시점이 오니 이게 발목을 잡았다.덕분에 스프링 시큐리티에 대한 이해도도 올라가게 된것 같다.