1. ExceptionTranslationFilter
ExceptionTranslationFilter는 예외처리용 필터이다. 로그인 시 다뤄지는 예외는 크게 2가지로 구분할 수 있다.
AuthenticationException : 인증 예외 처리
- AuthenticationEntryPoint 호출
인증이 실패했을 때 어떻게 handling 할지를 결정한다. 로그인 페이지로 리다이렉트, 401 HttpStatusCode 전달 등의 작업을 수행할 수 있다. 다음 장 코드 실습에서 직접 해본다.
- RequestCache와 SavedRequest
로그인 전에 어떤 url로 접속했는데 인증이 안되어서 로그인 페이지로 가는 경우, 사용자가 접근한 정보를 RequestCache와 SavedRequest 내에 담아두었다가, 사용자가 로그인하면 담아둔 정보를 바탕으로 사용자가 로그인 전에 요청했던 자원을 전달해주는 방식이다. RequestCache에는 사용자의 요청 정보를 '세션'에 저장한다. SavedRequest에는 사용자의 request 파라미터, 헤더값 등을 저장한다.
AccessDeniedException : 인가 예외 처리
해당 자원에 사용자의 권한이 없는 경우 예외처리를 한다. 이 경우도 Handler를 통해서 예외 처리할 코드를 직접 작성할 수 있다. 다음 장에서 직접 해본다.
2. 예외 처리 실습
실습용 코드 작성
SecurityConfig 파일에 이전 글에서 실습했던 권한 정보 설정을 위한 configure(AuthenticationManagerBuilder auth) 메서드는 그대로 두고, 아래 configure(HttpSecurity http) 메서드를 작성해본다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() //요청에 대한 보안 설정을 시작함
.antMatchers("/login").permitAll() //로그인 페이지에는 인증 받지 않아도 접근 가능하도록 설정
.antMatchers("/user").hasRole("USER")
.antMatchers("/admin/expenses").hasRole("ADMIN")
.antMatchers("/admin/**").access("hasRole('ADMIN') or hasRole('SYS')")
.anyRequest().authenticated(); //어떤 요청이든 authentication(인가) 과정을 거치도록 한다.
http
.formLogin()
// .loginPage("/loginPage")
.defaultSuccessUrl("/")
.failureUrl("/login")
.usernameParameter("userId")
.passwordParameter("passwd")
.loginProcessingUrl("/login_proc")
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
SavedRequest savedRequest = requestCache.getRequest(request, response);
String redirectUrl = savedRequest.getRedirectUrl(); //redirectUrl 말고도 header, locale 등 사용자의 요청 정보들을 조회할 수 있다.
response.sendRedirect(redirectUrl);
}
});
http
.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendRedirect("/login");
}
})
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.sendRedirect("/denied");
}
});
authenticationEntryPoint
우선 http로 구분되는 세 문단 중, 맨 마지막 부분이 exceptionHandling 부분이다. .authenticationEntryPoint 메서드를 호출하여 new AuthenticationEntryPoint를 입력하면 commence 메서드를 오버라이드 하도록 해준다. 여기서 request, response 정보를 바탕으로 인증 예외 발생 시 처리할 코드를 작성할 수 있다. response.redirect를 통해서 로그인 페이지로 이동하도록 해주었다.
accessDeniedHandler
여기서는 인가 관련 예외처리를 한다. /denied 페이지로 리다이렉트 되도록 해준다.
requestCahce
http 두 번째 문단에서 맨 처음 살펴봤었던 formLogin의 .successHandler에서 AuthenticationSuccessHandler를 호출하낟. 여기서 new HttpSessionRequestChache로 호출하면 requestCache 객체를 얻어올 수 있다. 요청 정보를 얻어와서 리다이렉트를 해주면 인증 전에 사용자의 요청을 바탕으로 요청왔던 페이지로 리다이렉트 처리를 해줄 수 있다.
마지막으로 첫 번째 http 구문에서 "/login" 페이지에서 대해서 permitAll()을 해주어야 한다는 것도 잊지말자.
3. 실습 : 필터 동작 살펴보기
ExceptionTranslationFilter
예외가 발생하면 ExceptionTranslationFilter에 도달한다. 따라서 ExceptionTranslationFilter에서 디버깅을 해본다.
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
if (!isAnonymous && !this.authenticationTrustResolver.isRememberMe(authentication)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Sending %s to access denied handler since access is denied", authentication), exception);
}
this.accessDeniedHandler.handle(request, response, exception);
} else {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied", authentication), exception);
}
this.sendStartAuthentication(request, response, chain, new InsufficientAuthenticationException(this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource")));
}
}
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(context);
this.requestCache.saveRequest(request, response);
this.authenticationEntryPoint.commence(request, response, reason);
}
주요 메서드는 위 코드 2개이다. 인증 및 인가와 관련된 발생할 수 있는 경우에 따라 동작 방식을 이해해보자.
1. 익명의 사용자가 "/" 페이지로 접근
루트 페이지로 익명의 사용자가 접근 시, 인증(로그인)은 안됐지만 anonymous로 Authentication 객체가 생성된 상태이므로 AuthenticationException이 아니라 우선 AccessDeniedException이 발생한다. 그러므로 isAnonymous를 검사하는 구문에서 else문에 걸려서 this.sendStartAuthentication 메서드를 호출하게 된다.
sendStartAuthentication 메서드에서는 requestCache를 담고, authenticationEntryPoint.commence를 실행한다. 따라서 맨 처음 SpringConfig 파일의 configure에서 commence가 실행되어 "/login" 페이지로 이동하게 된다.
2. 일반 유저가 권한을 가진 페이지에 접근
user로 로그인 해본다. 로그인을 위해서 authenticationEntryPoint 부분을 주석처리하고, Controller에서 login 관련 메서드도 주석처리를 해서 다시 form login을 할 수 있도록 설정한다. 그리고 root 페이지("/")로 접근한 이후 ExceptionTranslationFilter 부분을 다시 보면 익명의 사용자가 접근했을때와 같은 과정을 거치는 것을 확인할 수 있다. 그러나 디버깅 단계를 다 넘기고, 로그인 화면에서 로그인을 하면 다시 root 페이지로 접속된다. onAuthenticationSuccess 부분에 디버깅 포인트를 잡으면 SavedRequest에 사용자의 요청 정보가 담겨져 있는 것을 확인할 수 있다.
3. 일반 유저가 권한을 갖지 않은 페이지에 접근
/admin 페이지에 접근해본다. 그러면 ExceptionTranslationFilter의 handleAccessDeniedException 메서드의 첫 if문에 걸려서 accessDeniedHandler.handle 메서드를 호출하게 된다. 처음에 SecurityContext 파일에서 handler 메서드를 Override 해줬으므로 사용자를 "/denied" 페이지로 이동시킨다.
참조
1) 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 정수원님 강의
'Programming-[Backend] > Spring Security' 카테고리의 다른 글
[스프링 시큐리티][작성중][메모] 9. 주요 아키텍처 이해 (0) | 2022.01.09 |
---|---|
[스프링 시큐리티] 8. 위조 방지 : CsrfFilter (0) | 2022.01.09 |
[스프링 시큐리티] [작성중] 6. 권한 설정, 표현식 (0) | 2022.01.08 |
[스프링 시큐리티] [작성중] 5. 익명 사용자 인증 필터, 세션 제어 필터 (0) | 2022.01.07 |
[스프링 시큐리티] 4. Logout, Rember me 필터 (0) | 2022.01.05 |