스프링 시큐리티를 사용하면서 직접 필터와 인증 성공, 실패 핸들러를 만들어서 설정을 하고 API 호출 시 정상적으로 인증과 인가에 대한 처리가 되는지 확인하고 있었다.
/api/messages
는 권한이 ROLE_MANAGER
만 접근이 가능한 API이고 익명 사용자나 인증되었으나 권한이 없는 사용자는 접근할 수 없어야 한다. 실패한 경우에는 실패 사유를 JSON 형태로 전달 받도록 구성하였다.
/api/messages
요청을 보냈을 때, 해당 자원에 대한 적절한 스프링 시큐리티 설정이 동작하고 있다.
http.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.requestMatchers("/api/messages").hasRole("MANAGER")
.anyRequest().authenticated()
)
.exceptionHandling(exceptionHandlingConfigurer -> exceptionHandlingConfigurer.authenticationEntryPoint(ajaxLoginAuthenticationEntryPoint())
.accessDeniedHandler(ajaxAccessDeniedHandler())
);
그러나 문제는 스프링 부트에서는 에러가 발생하면 /error
요청이 발생한다.
이 /error
요청은 필터를 처음부터 다시 수행한다.
여기서부터 삽질했다… 이 프로젝트는 위의 스프링 시큐리티 설정과 아래의 스프링 시큐리티 설정 두 개 존재한다.
//인가 설정
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/users", "/login**").permitAll() // login* : customAuthenticationFailureHandler의 경로 허용을 위한 처리
.requestMatchers("/mypage").hasRole("USER")
.requestMatchers("/api/messages").hasRole("MANAGER")
.requestMatchers("/config").hasRole("ADMIN")
.anyRequest().authenticated()
)
.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(customAccessDeniedHandler())) //접근거부시 사용되는 핸들러 등록
.formLogin(formLoginConfigurer -> formLoginConfigurer
.loginPage("/login")
.loginProcessingUrl("/login_proc")
.defaultSuccessUrl("/")
.failureUrl("/login")
.authenticationDetailsSource(customAuthenticationDetailsSource()) // 인증 객체에 사용자 추가 요청 정보를 저장
.successHandler(customAuthenticationSuccessHandler()) //인증에 성공한 이후에 호출되어 동작
.failureHandler(customAuthenticationFailureHandler()) //인증에 실패한 경우 호출되어 동작
.permitAll()
)
.csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.disable());
/error
이라는 요청이 발생했을 때 필터를 다시 수행하고 있다. 그런데 /api/messages
요청을 했을 때와 다르게 아래의 스프링 시큐리티 설정을 사용하고 있었다.
이로 인해서 http client에서 요청한 API에서 적절한 응답이 오지 않고 해당 구성에서 사용 중인 AuthenticationFailureHandler
가 동작하여 엉뚱하게 /login
페이지를 또 요청하게 되는 현상이 발생되고 있었다.. (에러가 에러를 낳는다)
FilterChainProxy
디버깅 화면에서 처음 /api/messages
를 요청했을 때와는 다른 시큐리티 설정이 사용되고 있는 것을 확인할 수 있다…
해결은 /error
요청일 때도 이전과 동일한 스프링 시큐리티 설정을 사용하도록 처리했다.
http.securityMatcher("/api/**", "/error")
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.requestMatchers("/api/messages").hasRole("MANAGER")
.requestMatchers("/error").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(exceptionHandlingConfigurer -> exceptionHandlingConfigurer.authenticationEntryPoint(ajaxLoginAuthenticationEntryPoint())
.accessDeniedHandler(ajaxAccessDeniedHandler())
);
근본적인 문제는 요청에 대해서 필터가 여러 번 수행되기 때문에 불필요한 자원의 낭비가 발생하고 있다는 점인데 OncePerRequestFilter
를 상속 받아서 구현하면 한 번의 HTTP 요청에 대해 단 한 번만 실행됨을 보장한다고 한다.
즉, 동일한 요청에 대해서는 이 필터가 여러 번 실행되지 않는 것이다.
중복으로 작업을 수행하는 것을 방지할 수 있어서 예기치 않은 동작을 방지할 수 있으니 다음에는 상황에 맞게 OncePerRequestFilter
도 고려해서 사용해봐야겠다.
[참고]
AbstractAuthenticationProcessingFilter로 필터 구현 시, 쿠키 생성 안되는 현상 참고