이 시리즈에 나오는 모든 내용은 인프런 인터넷 강의 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 에서 기반된 것입니다. 그리고 여기서 인용되는 PPT 이미지 또한 모두 해당 강의에서 가져왔음을 알립니다.
Spring Security 의 주요 예외인 AuthenticationException
와 AccessDeniedException
을 처리해주는 ExceptionTranslationFilter
, 그리고 기존 Request 를 잠시 캐싱해주는 RequestCacheAwareFilter
를 알아보자.
1. ExceptionTranslationFilter
AuthenticationException
, AccessDeniedException
를 처리해주는 필터이다.
이 예외를 던지는 것은 FilterSecurityInterceptor
필터이다.
FilterSecurityInterceptor
는 Spring Security 가 관리하는 필터들 중에서도
가장 마지막에 존재하며 이 필터 바로 앞에 있는 것이 ExceptionTranslationFilter
이다.
요약하면...
FilterSecurityInterceptor
: 예외 터뜨림ExceptionTranslationFilter
: 예외 처리2. RequestCacheAwareFilter
AuthenticationException
가 터지면 대부분 로그인 페이지로 이동시킨다.
그런데 로그인 페이지에서 로그인 이후에 본래에 가려고 하던 곳으로 갈 때 필요한 필터다.
앞서 AuthenticationException
, AccessDeniedException
예외를 처리해준다고 했다.
그 처리 흐름이 어떻게 이루어지는지 큰 흐름을 파악해보자.
AuthenticationException(인증예외) 처리
AuthenticationEntryPoint 를 호출한다.
기본적으로는 로그인 페이지로 이동 + HTTP 상태코드 401 세팅을 한다.
인증 예외가 발생하기 전의 요청 정보를 세션에 저장한다. 개발자는 RequestCache
를 통해서 SavedRequest
를 추출하여 이전 요청 정보를 꺼내올 수 있다.
AccessDeniedException(인가예외) 처리
참고: AuthenticationEntryPoint를 추가 여부에 따른 디폴트 로그인 페이지 제공
커스텀 AuthenticationEntryPoint 적용 여부에 따라 스프링 시큐리티가
제공하는 기본 로그인 페이지는 활성화/비활성화된다는 점 주의하길 바란다.이러는 이유는 디폴트 로그인 페이지 필터를 제공할지 말지를 결정하는
DefaultLoginPageConfigurer
의 코드를 보면 알 수 있다.
분기문을 자세히 보면 exceptionConf, 즉 예외처리 관련 설정에서 authenticationEntryPoint 에 대한 설정을 읽어오고, 만약 null 이라면 default 로그인 페이지 필터를 시큐리티에 제공하고, 아니면 넣지 않는 것을 확인할 수 있다.
만약 우리가 스프링 시큐리티에 설정을 해주면 null 이 아님을 확인할 수 있다.
전반적인 프로세스를 봤으니 코드로 이 동작을 확인해보자.
일단 Spring Security 설정을 해주자.
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@Slf4j
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().mvcMatchers("/favicon.ico");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user")
.password("{noop}1111").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/user").hasRole("USER")
.anyRequest().authenticated();
http.formLogin()
.successHandler((request, response, authentication) -> {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
// 세션에서 이미 저장되어 있는 이전 요청 정보를 추출!
SavedRequest savedRequest = requestCache.getRequest(request, response);
String redirectUrl = savedRequest.getRedirectUrl();
System.out.println("redirectUrl = " + redirectUrl);
// 그 이전 요청 위치로 이동!
response.sendRedirect(redirectUrl);
});
http.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> {
// 주의!!! AuthenticationEntryPoint를 직접 구현하게 되면
// 우리가 만든 로그인 페이지로 이동하게 된다.
// 스프링 시큐리티가 제공하는 로그인 페이지가 아니다!
response.sendRedirect("/login");
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.sendRedirect("/denied");
});
}
}
그리고 이런 URL 을 지원하는 Controller 를 하나 생성한다.
@RestController
public class SecurityController {
@GetMapping("/")
public String index() {
return "home";
}
@GetMapping("/user")
public String user() {
return "user";
}
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping("/denied")
public String denied() {
return "denied";
}
}
ExceptionTranslationFilter
HttpSessionRequestCache
3-1. ExceptionTranslationFilter 내부
먼저 서버를 켜고 루트 경로에 접급해보자.
ExceptionTranslationFilter.handleAccessDeniedException
메소드에서 디버깅
포인트가 활성화되는 것을 볼 수 있다.
if (isAnonymous || ~~)
에서 true 가 나와서 분기문 내용으로 들어간다.
익명사용자라는 것을 의미한다.
아무튼 익명사용자로 판단이 되어서 sendStartAuthentication 메소드가 호출된다.
sendStartAuthentication
메소드가 하는 일은 크게 2가지임을 알 수 있다.
3-2. HttpSessionRequestCache
sendStartAuthentication 메소드 내부에서 requestCache.saveRequest
호출하면
위 그림의 메소드가 수행된다. 내부적으로 세션에 이전 요청을 저장하는 것을 확인할 수 있다.
3-3. 자신이 작성한 Spring Security 설정 클래스
sendStartAuthentication 메소드 내부에서 authenticationEntryPoint.commence
호출하면 위 그림의 메소드가 수행된다. 그러면 자신이 작성했던 스프링 시큐리티 클래스의
AuthenticationEntryPoint 내용이 수행된다.
3-4. 우리가 만든 로그인 화면 표출
최종적으로 우리가 작성한 로그인 페이지(?)가 표출된다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/user").hasRole("USER")
.antMatchers("/admin").hasRole("ADMIN") // 추가!!!!!!
.anyRequest().authenticated();
http.formLogin()
.successHandler((request, response, authentication) -> {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
SavedRequest savedRequest = requestCache.getRequest(request, response);
String redirectUrl = savedRequest.getRedirectUrl();
System.out.println("redirectUrl = " + redirectUrl);
response.sendRedirect(redirectUrl);
});
http.exceptionHandling()
// 주석 처리!!!!!!!!!!
// .authenticationEntryPoint((request, response, authException) -> {
// // 우리가 만든 로그인 페이지로 이동하게 된다.
// response.sendRedirect("/login");
// })
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.sendRedirect("/denied");
});
}
.antMatchers("/admin").hasRole("ADMIN")
추가.authenticationEntryPoint
부분 전체 주석기존과 달리 authenticationEntryPoint
부분을 전체 주석하고, 스프링 시큐리티가
기본으로 제공하는 로그인 페이지에 접근하고, user 사용자로 로그인 하고나서 인가 예외에
대한 처리를 관찰해보자.
5-1. 루트 페이지 접근
루트 페이지 접근하려고 하면 막힌다. 이 과정은 이전과 마찬가지다.
다만 이번에는 AuthenticationEntryPoint 를 직접 설정하지 않았다.
그러면 어떤 AuthenticationEntryPoint를 사용할까?
LoginUrlAuthenticationEntryPoint
라는 클래스의 commence
메소드를 호출한다.
로그인 페이지로 리다이렉트 시키는 간단한 작업을 수행한다.
5-2. 로그인 페이지 표출 및 로그인
지금은
authenticationEntryPoint
를 커스텀하게 만들지 않아서 스프링 시큐리티
기본 로그인 페이지가 나오는 것이다.
로그인을 해주자.
5-3. 이전요청에 따라 루트 페이지로 이동확인
정상적으로 접근이 됐다.
5-4. 권한 없는 페이지 접근(/admin
) ==> ExceptionTranslationFilter
user 사용자는 ADMIN
이라는 권한이 없어서 ExceptionTranslationFilter
에서
예외처리 하는 구문의 디버깅 포인트가 잡힌다.
하지만 이전의 인증예외와 달리 인가예외라서 this.accessDeniedHandler.handle
메소드가
실행된다. 이 메소드의 내용을 관찰해보자.
5-4. 개인 스프링 시큐리티 설정 클래스
이전에 설정했던 AccessDeniedHandler 가 동작한다.
5-5. redirect 결과 페이지 표출
참고: 만약
AccessDeniedHandler
구현을 안했다면 default 로 아래 클래스가 호출된다.
결과적으로 아래 페이지가 나온다.