[Spring-security-test] MVC test 401/403 에러
프로젝트를 진행하며 controller 단의 테스트 코드를 작성하던 도중 403 에러가 발생했습니다.
약 3일간 검색과 적용을 반복했지만 403은 사라지지 않았습니다.
결론은 삽질이긴 했습니다.
이후 나 같은 사람이 적어지길 바라며 글을 작성해보겠습니다.
1. 401 에러
401 에러는 Unauthorized로 로그인을 하지 않아 나는 에러입니다.
403과 언 듯 비슷해 보일 수 있으나 403은 로그인 후 권한 부족, 401은 로그인 자체에 대한 에러입니다.
즉 401은 테스트 계정이 없다는 의미입니다.
이를 해결하는 방법은 간단합니다.
@WithMockUser
@WithAnonymousUser (미인증 사용자)
@WithUserDetails (메서드가 principal내부 값을 직접 사용할 경우)
@WithMockCustomUser (자신이 커스텀한 계정)
등을 사용하여 해결하는 것입니다.
자세한 내용은 다른 포스트를 참고해 주시면 감사하겠습니다.
해당 글은 403 에러를 중점으로 하기 때문에 키워드만,,,
2. 403 에러
권한 부족을 뜻하는 403 에러는 계정의 권한에 문제가 있는 경우입니다.
권한 설정을 제대로 했다면 CSRF의 문제입니다.
해결방법은 세 가지가 있습니다.
1. addFilter 해제
@AutoConfigureMockMvc(addFilter=false)
addFilter를 false로 설정하는 방법입니다.
해당 방법은 가장 간단하고 쉽게 해결할 수 있습니다.
하지만 의도적으로 403을 일으키거나 권한 관련, 혹은 spring security의 테스트를 진행할 땐 사용할 수 없습니다.
왜냐하면 addFilter을 false로 설정한다는 건 security의 filter-chain 중 하나를 꺼리는, 쉽게 말해 그냥 보안 기능을 끈다는 것을 의미합니다. 그러므로 근본적인 해결책도 임시로 쓸만한 해결책도 아닌 것 같습니다. (security 테스트 안 하시면 임시론 쓸 수 있을 것 같긴 합니다)
2. csrf 해제
csrf를 securityConfig에서 해제해 버리는 방법입니다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().httpBasic();
http
.cors()
.and()
.authorizeRequests()
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
.antMatchers("/post/**").authenticated()
.antMatchers("/code/**").authenticated()
.antMatchers("/member/**").authenticated()
.antMatchers("/admin/**").hasAnyRole("TIER1", "TIER2") // 해당 권한을 가진 사람만 접근 가능
.antMatchers("/admin/personal-info/**", "/admin/member/set-role/**", "/admin/posts")
.hasAnyRole("TIER2")
.anyRequest().permitAll() // 다른 주소는 모두 허용
.and()
.formLogin()
.usernameParameter("loginId")
.loginProcessingUrl("/login")
.successHandler(authenticationSuccessHandler())
.failureHandler(authenticationFailureHandler())
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(((request, response, authentication) -> {
response.setStatus(HttpServletResponse.SC_OK);
}))
.invalidateHttpSession(true)
.and()
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
.sessionManagement()
.sessionFixation().changeSessionId()
.maximumSessions(1)
.maxSessionsPreventsLogin(true);
}
맨 윗줄에 있는 csrf().disable()을 설정하는 방법입니다.
하지만 이 방법을 설정하기에 앞서 CSRF가 무엇인지 알아야 합니다.
csrf란?
csrf 공격을 막기 위한 설정입니다.
페이크 사이트에서 로그인을 유도한 후 JSESSIONID를 탈취, 이용하는 것을 막는 설정입니다.
csrf는 이러한 SESSION ID 탈취 해킹을 막는 방법 중 하나입니다.
로그인 성공 시 session id와 함께 뷰 페이지에 csrf 토큰을 넣어 보내주는데 session id를 탈취한 해커가 해당 session id로 유저인 척 접근하려 해도 csrf가 일치하지 않아 403이 뜨게 되는 것입니다.
그럼 여기서 "CSRF가 무조건 활성화 돼있어야 하는 게 아닌가?"라는 의문이 들 수 있습니다.
하지만 그것은 아닙니다.
csrf는 말 그대로 sessionID를 이용할 때, 타임리프와 같은 템플릿 엔진을 통해 뷰를 제공한다면 csrf는 매우 중요한 보안 수단입니다. 하지만 restAPI만을 위한 서버처럼 JWT 등으로 세션에 의존하지 않는다면 csrf방식을 사용하지 않아도 무방합니다.
3. csrf를 넣어주기
자신의 사이트는 csrf가 필요한 사이트인 경우엔 csrf를 해제해 버릴 수 없습니다.
그럴 땐 .with(csrf()) 를 사용하면 됩니다.
mvc.perform(MockMvcRequestBuilders
.post("/myLink")
.with(csrf()))
.andExpect(status().isOK());
위와 같이 .with(csrf())를 추가하면 아래와 같이 request에 csrf 토큰 값이 포함되어 갑니다.
이를 통해 403 에러를 해결할 수 있습니다.
3. 삽질 테스트 계정 확인
위의 방법들을 다 했을 때도 403이 계속 뜬다면 계정 자체의 문제일 확률이 높습니다.
저 같은 경우엔 WithMockCustomUser의 문제였습니다.
WithMockCustomUserSecurityContextFactory로 계정 설정을 대부분 하다 보니 WithMockCustomUser는 모르고 있었습니다. 이때 request 로그에 Session Attrs에 role이 눈에 들어왔습니다.
TIER2라고만 나가고 있더군요.
그래서 좀 더 찾아보니 authenticationTocken의 문제였습니다.
final UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(new PrincipalDetails(
member),
"password",
Arrays.asList(new SimpleGrantedAuthority(annotation.role())));
위 코드의 role부분을 주목해 봤습니다.
annotation.role() ... 저부분이 테스트 계정으로 요청 보낼 때 보내주는 권한이기에 annotation이 어디서 오는 건지도 살펴봤습니다.
public SecurityContext createSecurityContext(WithMockCustomUser annotation) {
WithMockCustomUser를 통해 받고 있는 것을 볼 수 있었습니다.
그리고 해당 파일을 열어보니...
String role() default "TIER2";
"ROLE_" 없이 그냥 TIER2만 붙어있더군요.
spring security의 hasAnyRole에 들어가는 인자는 기본 prefix로 ROLE_이 들어가 따로 설정 없이 한 전 ROLE_***로 해야 합니다.
그래서 이 부분만 ROLE_을 붙여 주었더니 짜잔!
위와 같이 잘 나가고 테스트도 모두 통과하는 모습이다!