FilterSecurityInterceptor 바로 위에 위치하며, FilterSecurityInterceptor 실행 중 발생할 수 있는 예외(AccessDeniedException과 AuthenticationException을)를 잡고 처리한다
- FilterSecurityInterceptor는 FilterChainProxy에서 필터 가장 마지막에 위치한다.
- 필터 체인에서 발생하는 AccessDeniedException과 AuthenticationException을 처리하는 필터
- 처리할 수 없는 예외는 rethrow 하여 그냥 앞으로 던져버린다.
FilterSecurityInterceptor 가 AccessDecisionManager 를 통해 AuthenticationException 혹은, AccessDeniedException 을 발생시킨 경우 해당 Exception을 받아 AuthenticationException의 경우 AuthenticationEntryPoint 로 보내주고, AccessDeniedException 의 경우에는 AccessDeniedHandler 로 보내는 역할을 담당한다.
필터 체인(Filter Chain) 상에서 ExceptionTranslationFilter의 위치를 주의해서 볼 필요가 있다.
ExceptionTranslationFilter는 필터 체인 실행 stack에서
자기 아래에 오는 필터들
에서 발생하는 예외들에 대해서만 처리할 수 있다. 커스텀 필터를 추가해야 하는 경우 이 내용을 잘 기억하고, 커스텀 필터를 적당한 위치에 두어야 한다.
- 즉 아무것도 안건드린 default 필터 순서 상에서는, FilterSecurityInterceptor에서 발생하는 에외들만 처리 가능.
- 추가적으로 처리하고 싶다면 ExceptionTranslationFilter 뒤나 앞에 HandlingFilter를 추가하면 된다.
- Security 필터들은 순서가 정해져 있으므로 순서를 잘 맞춰야 한다.
- 이 때, HttpSecurity 객체의 addFilterBefore()와 addFilterAfter() 메소드를 이용해서 필터 순서를 지정할 수 있다.
- HttpSecurity 객체의 addFilterBefore(new 커스텀 필터(), 커스텀 필터 뒤에 둘 필터)
- 뒤 인자는 기존 필터를 받는데, 이 필터 앞에 설정하겠다는것
- HttpSecurity 객체의 addFilterAfter(new 커스텀 필터(), 커스텀 필터 앞에 둘 필터)
- 뒤 인자는 기존 필터를 받는데, 이 필터 뒤에 설정하겠다는것
ExceptionTranslationFitler와 FilterSecurityInterceptor랑은 밀접한 관계가 있다.
- 필터 순서가 ExceptionTranslationFitler가 더 앞에 있어야 한다.
- ExceptionTranslationFilter 보다 아래에 위치한 필터들 실행
- 예외가 발생했을 때, 처리할 수 없는 예외이면 그냥 rethrow해서 밖으로 던짐
- 처리가 가능한 예외면 (AuthenticationException과 AccessDeniedException 예외) 예외 처리
- AuthenticationException 예외 인 경우 handleAuthenticationException() 호출
- AuthenticationException 예외 인 경우 간단하게 사용자를 인증가능한 페이지로 리다이렉트
- AccessDeniedException 예외 인 경우 handleAccessDeniedException() 호출
- 사용자가 인증되지 않은 익명 사용자이거나 RemeberMe를 통한 인증된 사용자 인 경우에는
- 사용자를 인증가능한 페이지로 리다이렉트
- 익명사용자도 아니고 RememberMe로 인증된 사용자가 아니라면
- accessDeniedHandler를 통해 처리 - 403 응답
- 사용자가 인증되지 않은 익명 사용자이거나 RemeberMe를 통한 인증된 사용자 인 경우에는
- AuthenticationException 예외 인 경우 handleAuthenticationException() 호출
AccessDeniedException 예외 발생 순서
- AbstractSecurityInterceptor.beforeInvocation() 호출
- attemptAuthorization(object, attributes, authenticated); 호출
- this.accessDecisionManager.decide(authenticated, object, attributes); 호출
- decide 메소드는 액세스 제어 결정을 확인
- 내부적으로 Voter를 이용한 연산을 하는데, 여기서 권한에 맞지 않으면 throw AccessDeniedException()
Voter
Spring Security는 투표를 기반으로 request에 대한 access에 대한 승인 여부를 결정
인증(Authentication)
- 사용자의 신원을 확인하는 과정
- 아이디/패스워드 기반 로그인
- OAuth2.0 프로토콜을 통한 Social 인증
인가(Authorization)
- 어떤 개체가 어떤 리소스에 접근할 수 있는지 또는 어떤 동작을 수행할 수 있는지를 검증하는 것, 즉 접근 권한을 얻는 일을 의미
- 적절한 권한이 부여된 사용자들만 특정 기능 수행 또는 데이터 접근을 허용함
AuthenticationException과 AccessDeniedException 예외
- AuthenticationException 예외는 인증 관련 예외이며, 사용자를 로그인 페이지로 보냄
- AccessDeniedException 예외는 AccessDecisionManager에 의해 접근 거부가 발생했을 때 접근 거부 페이지를 보여주거나 사용자를 로그인 페이지로 보냄
- AuthenticationException 예외는 AuthenticationEntryPoint를 호출하여 인증 가능하도록(로그인 등) 리다이렉트 등을 한다.
- AccessDeniedException 예외는 AccessDeniedHandler를 호출하고 AccessDeniedHandler의 기본 구현체는 AccessDeniedHandlerImpl이고, 내부 handle() 메소드에서 errorPage가 정의되어 있지 않으면 그냥 response.sendError()로 403응답과 에러 메시지를 리턴한다.
인증 예외 (AuthenticationException) (권한이 요구되는 페이지에 권한없이 접근할 때)
- AuthenticationEntryPoint 호출
- 로그인 페이지 이동, 401 오류 코드 전달, SecurityContext 초기화
- AuthenticationEntryPoint를 직접 구현해서 인증 예외 처리를 할 수도 있다.
- this.authenticationEntryPoint.commence(request, response, reason);
- 인증 예외가 발생하기 전의 요청 정보를 저장
- RequestCache 인터페이스 : 사용자의 이전 요청 정보를 세션에 저장하고 이를 꺼내 오는 캐시 메커니즘
- 사용자가 다시 로그인하여 성공 후 이전 가고자 했던 리소스 정보를 캐시에서 가지고 와서 해당 리소스로 이동하게 됨
- SavedRequest 인터페이스 : 사용자가 요청했던 request paramter, header 값들을 저장
- RequestCache 인터페이스 : 사용자의 이전 요청 정보를 세션에 저장하고 이를 꺼내 오는 캐시 메커니즘
RequestCacheAwareFilter는 requestCache에 값이 존재하는 경우 해당 값을 꺼내서 다음 Filter로 넘겨주는 역할을 한다.
RequestCacheAwareFilter에서 전달 된 requestCache의 값을 가지고 인증 성공 시 이전 요청된 정보를 가지고 해당 리소스로 이동한다.
AuthenticationEntryPoint 는 SpringSecurity가 구현체를 제공한다
- AuthenticationEntryPoint 사용자가 직접 구현해서 호출할수도 있다,이때 인증 예외가 발생하기전에 그 사용자가 가고자 했던 자원정보를 저장한다.
- 그렇게 된다면 로그인에 성공을 하게 되면 이전에 사용자가 접근하려고 했던 서버자원으로 이동하게 처리한다 (RequestCache)
- 인증된 사용자만 서버자원에 접근이 가능한데, 만약 인증없이 서버자원에 접근할려면 로그인 페이지로 이동하게 처리한다.
인가 예외 (AccessDeniedException) (권한이 요구되는 페이지에 요구되는 권한이 아닌걸 가지고 요청할 때)
- 익명 사용자라면 AuthenticationEntryPoint 실행
- 익명 사용자가 아니면(인증된 사용자가 아니라면) AccessDeniedHandler에게 예외를 처리하도록 호출 - 위임
- AccessDeniedException 예외는 AccessDecisionManager에 의해 접근 거부가 발생했을 때 접근 거부 페이지를 보여주거나 사용자를 로그인 페이지로 보냄
- AccessDeniedException -> AccessDeniedHandler ->response.redirect(/denied) 이자원에 접근할수 없다고 확인 하는 페이지를 호출한다
- AccessDeniedHandler의 기본 구현체는 AccessDeniedHandlerImpl이고, 내부 handle() 메소드에서 errorPage가 정의되어 있지 않으면 그냥 response.sendError()로 403응답과 에러 메시지를 리턴한다.
- 에러페이지가 정의되어있다면 에러페이지로 forward 시킨다.
다시한번 정리
- ExceptionTranslationFilter 보다 아래에 위치한 필터들 실행
- 예외가 발생했을 때, 처리할 수 없는 예외이면 그냥 rethrow해서 밖으로 던짐
- 처리가 가능한 예외면 (AuthenticationException과 AccessDeniedException 예외) 예외 처리
- AuthenticationException 예외 인 경우 handleAuthenticationException() 호출
- AuthenticationException 예외 인 경우 간단하게 사용자를 인증가능한 페이지로 리다이렉트
- AccessDeniedException 예외 인 경우 handleAccessDeniedException() 호출
- 사용자가 인증되지 않은 익명 사용자이거나 RemeberMe를 통한 인증된 사용자 인 경우에는
- 사용자를 인증가능한 페이지로 리다이렉트
- 익명사용자도 아니고 RememberMe로 인증된 사용자가 아니라면
- accessDeniedHandler를 통해 처리 - 403 응답
- AccessDeniedHandler의 기본 구현체는 AccessDeniedHandlerImpl이고, 내부 handle() 메소드에서 errorPage가 정의되어 있지 않으면 그냥 response.sendError()로 403응답과 에러 메시지를 리턴한다.
- 에러페이지가 정의되어있다면 에러페이지로 forward 시킨다.
- 사용자가 인증되지 않은 익명 사용자이거나 RemeberMe를 통한 인증된 사용자 인 경우에는
- AuthenticationException 예외 인 경우 handleAuthenticationException() 호출
예외처리 기능 작동할 수 있도록 하는법
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint()) // 인증실패 시 처리
.accessDeniedHandler(accessDeniedHandler()) // 인증실패 시 처리
}
업그레이드 된 버전
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint()) // 인증실패 시 처리
.accessDeniedHandler(accessDeniedHandler()) // 인증실패 시 처리
.build();
}
AccessDeniedHandler 커스텀
@Configuration
@EnableWebSecurity
public class WebSecurityConfigure {
private final Logger log = LoggerFactory.getLogger(WebSecurityConfigure.class);
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests()
.antMatchers("/me").hasAnyRole("USER", "ADMIN")
.antMatchers("/admin").access("hasRole('ADMIN') and isFullyAuthenticated()") // 어드민 권한은 가진 사용자이고 리멤버미를 통하여 인증된 사용자가 아닌사용자만
.and()
.exceptionHandling() // 이부분
.accessDeniedHandler(accessDeniedHandler())
.and()
.build();
}
@Bean
public AccessDeniedHandler accessDeniedHandler() { // 커스텀하기
return (req, res, e) -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication != null ? authentication.getPrincipal() : null;
log.warn("{} is denied", principal, e);
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
res.setContentType("text/plain");
res.getWriter().write("## ACCESS DENIED!! ##");
res.getWriter().flush();
res.getWriter().close();
};
}
}
- httpSecurity.exceptionHandling()
.accessDeniedHandler(커스텀 액세스 디나이 핸들러()) 를 추가하면 된다.
UsernamePasswordAuthenticationFilter에서 발생한 인증 에러는?
AbstractAuthenticationProcessingFilter에서 catch해서
SimpleUrlAuthenticationFailureHandler.가 처리한다.
- public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
- AbstractAuthenticationProcessingFilter.unsuccessfulAuthentication(request, response, failed);
Authentication authResult; try { authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication return; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // Authentication failed unsuccessfulAuthentication(request, response, failed); return; }
- SimpleUrlAuthenticationFailureHandler.saveException(request, exception)
// public class SimpleUrlAuthenticationFailureHandler implements
AuthenticationFailureHandler
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
if (defaultFailureUrl == null) {
logger.debug("No failure URL set, sending 401 Unauthorized error");
response.sendError(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase());
}
else {
saveException(request, exception);
if (forwardToDestination) {
logger.debug("Forwarding to " + defaultFailureUrl);
request.getRequestDispatcher(defaultFailureUrl)
.forward(request, response);
}
else {
logger.debug("Redirecting to " + defaultFailureUrl);
redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
}
}
}
protected final void saveException(HttpServletRequest request,
AuthenticationException exception) {
if (forwardToDestination) {
request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
}
else {
HttpSession session = request.getSession(false);
if (session != null || allowSessionCreation) {
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,
exception);
}
}
}
'Spring > Spring Security' 카테고리의 다른 글
HeaderWriterFilter - 응답헤더에 보안 관련 헤더 추가하는 필터 (0) | 2022.12.19 |
---|---|
AccessDecisionVoter 커스텀 (0) | 2022.12.19 |
AccessDecisionManager, AccessDecisionVoter - 인가 결정 심의자 (0) | 2022.12.19 |
Security 인가 처리 개념 및 과정 (Authorization), AccessDecisionManager (0) | 2022.12.19 |
FilterSecurityInterceptor - 인가(Authorization) 처리 필터 (1) | 2022.12.19 |