ExceptionTranslationFilter
ExceptionTranslationFilter는 AuthenticationException과 AccessDeniedException 이 두가지의 예외를 처리하는데 ExceptionTranslationFilter가 try-catch로 감싸고 catch에 오면 Filtersecurityinterceptor가 받아 예외를 발생시킨다.
AuthenticationException
- AuthenticationEntryPoint 호출한다.
- 인증예외가 발생하기 전에 요청정보를 저장한다.
AccessDeniedException
- 인가 예외 처리 - AccessDeniedHandler에서 예외처리하도록 제공
설정
전체코드
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login").permitAll() // login 페이지는 인증이 안돼도 접근이 가능해야하기 때문에
.antMatchers("/user").hasRole("USER")
.antMatchers("/admin/pay").hasRole("ADMIN")
.antMatchers("/admin/**").access("hasRole('ADMIN') or hasAnyRole('sys')") //표현식을 사용해 두개이상의 권한을 설정
.anyRequest().authenticated();
http
.formLogin()
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
RequestCache requestCache = new HttpSessionRequestCache();
SavedRequest savedRequest = requestCache.getRequest(request, response);
String redirectUrl = savedRequest.getRedirectUrl();
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");
}
});
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailsService() {
List<UserDetails> users = new ArrayList<>();
users.add(User.builder().username("user").password("{noop}1111").roles("USER").build());
users.add(User.builder().username("sys").password("{noop}11111").roles("SYS").build());
users.add(User.builder().username("admin").password("{noop}11111").roles("ADMIN", "USER").build());
return new InMemoryUserDetailsManager(users);
}
}
코드설명
AuthenticationException가 발생하면 RequestCache에 사용자의 이전 요청 정보을 세션에 저장하는데 그 요청했던 url를 불러오는 코드이다.
http
.formLogin()
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
RequestCache requestCache = new HttpSessionRequestCache();
SavedRequest savedRequest = requestCache.getRequest(request, response);
String redirectUrl = savedRequest.getRedirectUrl();
response.sendRedirect(redirectUrl);
}
});
인증 예외(AuthenticationException)이 발생했을 때 AuthenticationEntryPoint 를 호출하기 때문에 오버라이딩
http.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendRedirect("/login");
}
})
인가 예외(AccessDeniedException)이 발생했을 때 AccessDeniedHandler를 호출하기 때문에 오버라이딩
http.exceptionHandling()
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.sendRedirect("/denied");
}
});
코드 플로우
인증 예외(AuthenticationException)
localhost:8080/ 으로 접속하면 인증에러가 발생한다. (1)은 인증에러 , (2)인가에러가 발생할 때 수행하게 된다.
위 코드에서 sendStartAuthentication() 를 호출하면 (1) 캐쉬를 저장하고 (2) AuthenticationEntryPoint 호출한다.
캐쉬를 저장할 때 호출한 saveRequest 이다. (1) 에서 세션에 요청정보를 저장하는 것을 확인할 수 있다.
AuthenticationEntryPoint()가 호출하면 오버라이딩한 메소드가 호출된다.
http.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendRedirect("/login");
}
})
이제 요청한 정보를 인증이후에 잘 캐쉬가 되었는지 확인해야한다. 테스트를 위해 아래처럼 주석처리해준다.
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");
}
});
localhost:8080/user 는 USER 권한을 가진 유저만 접근할 수 있는데 접근하면 인증을 위해 login페이지로 이동한다. 로그인을 시도해보면 (1) onAuthenticationSuccess()가 호출되고 (2) 를 보면 처음 접근한 url를 캐쉬하고 있는걸 확인할 수 있다.
인가 예외(AccessDeniedException)
위 테스트를 이어서 localhost:8080/admin 은 ADMIN 권한을 가진 유저만 접근인 가능하다 접근해보면 (1) 이 호출된 것을 확인할 수 있다.
handle() 가 호출되면 오버라이딩한 메소드가 실행된다.
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.sendRedirect("/denied");
}
RequestCacheAwareFilter
RequestCacheAwareFilter는 요청이 완료된 후, 즉 응답이 클라이언트로 보내진 후에, 보안 컨텍스트를 캐시합니다. 캐시된 보안 컨텍스트는 다음 요청에서 사용됩니다. 이는 캐시된 보안 컨텍스트가 더 이상 필요하지 않은 경우에만 캐시에서 삭제됩니다.
이 필터는 사용자가 로그인 후, 리디렉션된 후 다시 돌아올 때 캐시된 보안 컨텍스트를 사용하여 이전 상태를 복원할 때 유용합니다. 예를 들어, 로그인 하기 전에 요청한 페이지로 다시 리디렉션하는 경우, 캐시된 보안 컨텍스트를 사용하여 이전 상태를 복원할 수 있습니다.
public class RequestCacheAwareFilter extends GenericFilterBean {
private RequestCache requestCache;
public RequestCacheAwareFilter() {
this(new HttpSessionRequestCache());
}
public RequestCacheAwareFilter(RequestCache requestCache) {
Assert.notNull(requestCache, "requestCache cannot be null");
this.requestCache = requestCache;
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
(HttpServletRequest) request, (HttpServletResponse) response);
chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
response);
}
}
- doFilter 부분에서 requestCache.getMatchingRequest를 통해서 session에 저장되어 있는 request를 불러 온다.
- 만약 캐시된 요청이 존재한다면, 캐시된 wrappedSavedRequest를 처리하고, 아닌 경우 request를 처리 한다.
- 좀 더 예시를 들자면, 인증이 필요한 웹사이트에 접근했을 때 로그인창으로 리다이렉트 된 뒤 로그인을 하면 로그인 하기 이전에 페이지로 돌아 가는 것을 종종 볼 수 있다.
- 로그인 하기전 요청을 캐시하고 있다가 로그인에 성공하는 경우 캐시된 요청을 로그인 이후에 처리해 주는 역할을 해주는게 RequestCacheAwareFilter 이다.