인프런/스프링 시큐리티 완전 정복 [6.x 개정판]

6) 인증 프로세스 - logout() ,RequestCache / SavedRequest

backend dev 2024. 10. 11.

로그아웃

• 스프링 시큐리티는 기본적으로 DefaultLogoutPageGeneratingFilter를 통해

로그아웃 페이지를 제공하며 “ GET / logout ” URL 로 접근이 가능하다.

 

• 로그아웃 실행은 기본적으로 “ POST / logout “ 으로만 가능하나

CSRF 기능을 비활성화 할 경우 혹은 RequestMatcher 를 사용할 경우 GET, PUT, DELETE 모두 가능하다

 

• 로그아웃 필터를 거치지 않고 스프링 MVC 에서 커스텀 하게 구현할 수 있으며

로그인 페이지가 커스텀하게 생성될 경우 로그아웃 기능도 커스텀하게 구현해야 한다

 

logout() API

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, HttpSecurity httpSecurity) throws Exception {
        http.authorizeHttpRequests(auth -> auth
                        .anyRequest().authenticated())
                .formLogin(Customizer.withDefaults())
                .logout(httpSecurityLogoutConfigurer -> httpSecurityLogoutConfigurer
                                .logoutUrl("/logoutProc") // HTTP METHOD방식은 POST
                                .logoutRequestMatcher(new AntPathRequestMatcher("/logoutProc", "POST")) // 이 설정이 .logoutUrl()설정보다 우선된다, 뒤에 METHOD방식을 명시하지않으면 get,post 등 다 가능하다.
                                .logoutSuccessUrl("/logoutSuccess")
//                        .logoutSuccessHandler(new LogoutSuccessHandler() {
//                            @Override
//                            public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//                                response.sendRedirect("/logoutSuccess");
//                            }
//                        })
                                .logoutSuccessHandler(((request, response, authentication) -> {
                                    response.sendRedirect("/logoutSuccess");
                                }))
                                .deleteCookies("JSESSIONID", "remember-me")
                                .invalidateHttpSession(true)
                                .clearAuthentication(true)
//                                .addLogoutHandler(new LogoutHandler() {
//                                    @Override
//                                    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
//                                        HttpSession session = request.getSession();
//                                        session.invalidate();
//                                        SecurityContextHolder.getContextHolderStrategy().getContext().setAuthentication(null);
//                                        SecurityContextHolder.getContextHolderStrategy().clearContext();
//                                    }
//                                })
                                .addLogoutHandler(((request, response, authentication) -> {
                                    HttpSession session = request.getSession();
                                    session.invalidate();
                                    SecurityContextHolder.getContextHolderStrategy().getContext().setAuthentication(null);
                                    SecurityContextHolder.getContextHolderStrategy().clearContext();
                                }))
                                .permitAll()
                );
        return http.build();
    }

 

SecurityContextHolder.getContextHolderStrategy().getContext().setAuthentication(null);

이 코드를 단계별로 살펴보겠습니다


a) SecurityContextHolder: Spring Security의 핵심 클래스로, 현재 실행 중인 스레드의 보안 컨텍스트에 대한 접근을 제공합니다.


b) .getContextHolderStrategy(): SecurityContextHolder의 저장 전략을 가져옵니다. 

이 전략은 SecurityContext를 어떻게 저장하고 검색할지 결정합니다 (예: ThreadLocal, 전역 변수 등).


c) .getContext(): 현재 스레드의 SecurityContext를 가져옵니다. 

SecurityContext는 현재 인증된 사용자의 정보를 포함합니다.


d) .setAuthentication(null): SecurityContext 내의 Authentication 객체를 null로 설정합니다. 

이는 현재 인증된 사용자 정보를 제거하는 것과 같습니다.


이 라인은 현재 사용자의 인증 정보만을 제거합니다. 그러나 SecurityContext 자체는 여전히 존재합니다.

SecurityContextHolder.getContextHolderStrategy().clearContext();


이 코드도 단계별로 살펴보겠습니다


a) SecurityContextHolder.getContextHolderStrategy(): 위와 동일하게, SecurityContextHolder의 저장 전략을 가져옵니다.


b) .clearContext(): 현재 스레드의 전체 SecurityContext를 제거합니다. 이는 단순히 Authentication 객체를 null로 설정하는 것을 넘어서, 전체 SecurityContext를 초기화합니다.


이 라인은 첫 번째 라인보다 더 포괄적입니다.

 SecurityContext 내의 모든 정보(Authentication 객체 포함)를 제거하고, SecurityContext 자체를 초기 상태로 되돌립니다.

 

 

  1. 첫 번째 라인은 명시적으로 Authentication을 null로 설정하여 인증 정보를 제거합니다.
  2. 두 번째 라인은 전체 컨텍스트를 초기화하여 혹시 남아있을 수 있는 다른 보안 관련 정보까지 모두 제거합니다.

 

실제로

SecurityContextHolder.getContextHolderStrategy().clearContext();

이 한 줄만으로도 충분

 

clearContext() 메서드 하나만 사용해도 동일한 결과를 얻을 수 있습니다.

이 방법이 더 간결하고 효율적이며, Spring Security의 권장 방식에 더 부합합니다.

 

 

위와 같이 설정했지만 로그아웃버튼에는 /logout url이 할당되어있다.

.logoutRequestMatcher(new AntPathRequestMatcher("/logoutProc", "POST"))

으로 되어있기에 저 로그아웃버튼을 눌러서 이동해도

404 에러가 발생한다. [ 설정한 url이 아니면 로그아웃 기능이 동작하지않기 때문이다. ]

 

 

LogoutFilter

 

POST /logout 으로 http request가 오면 LogoutFilter의 RequestMatcher가 만족되므로 LogoutFilter안의 LogoutHandler가 동작한다. 각각 LogoutHandler가 동작하고 난뒤 LogoutSuccesHandler가 동작한다.

 

[로그아웃 프로세스]

  1. HTTP POST 요청이 /logout 엔드포인트로 전송됩니다.
  2. LogoutFilter가 이 요청을 가로챕니다.
  3. LogoutFilter는 내부적으로 RequestMatcher를 사용하여 이 요청이 로그아웃 요청인지 확인합니다. 기본적으로 POST /logout 요청을 매치합니다.
  4. 요청이 매치되면, LogoutFilter는 구성된 LogoutHandler들을 순차적으로 실행합니다. 일반적인 LogoutHandler들은 다음과 같습니다:
    • SecurityContextLogoutHandler: SecurityContext를 정리합니다.
    • CookieClearingLogoutHandler: 지정된 쿠키들을 제거합니다.
    • CsrfLogoutHandler: CSRF 토큰을 정리합니다.
    • RememberMeServices: "Remember Me" 기능 관련 데이터를 정리합니다.
  5. 모든 LogoutHandler들의 실행이 완료된 후, LogoutSuccessHandler가 호출됩니다. 이 핸들러는 로그아웃 성공 후의 동작을 정의합니다. 예를 들어:
    • 사용자를 특정 페이지로 리다이렉트
    • 로그아웃 성공 메시지 반환
    • 기타 필요한 후처리 작업 수행

이 프로세스는 커스터마이즈가 가능합니다. 예를 들어, 추가적인 LogoutHandler를 구현하여 로그아웃 시 특정 작업을 수행하게 하거나, LogoutSuccessHandler를 커스텀하여 로그아웃 후 특별한 응답을 반환하게 할 수 있습니다.

 


요청 캐시 - RequestCache / SavedRequest

 

RequestCache

인증 절차 문제로 리다이렉트 된 후에 이전에 했던 요청 정보를 담고 있는 'SavedRequest’ 객체를

쿠키 혹은 세션에 저장하고 필요시 다시 가져와 실행하는 캐시 메카니즘이다

 

SavedRequest

SavedRequest 은 로그인과 같은 인증 절차 후 사용자를 인증 이전의 원래 페이지로 안내하며

이전 요청과 관련된 여러 정보를 저장한다

 

 

흐름도

인증 받지 않은 상태로 접근

=> HttpSessionRequestCache 객체가 DefaultSavedRequest 객체를 saveRequest()를 이용하여 저장한다.

[ HttpSession에 저장된다, SPRING_SECURITY_SAVED_REQUEST라는 키로 HTTP 세션에 저장됨] 

 

 

인증 성공후

AuthenticationSuccessHandler가 동작해서 HttpSessionRequestCache을 이용해 HttpSession에 저장된 DefaultSavedRequest를 가져오고 해당 객체 내부 정보를 이용해 redirect를 진행해준다.

 

[ claude ]

  1. 인증이 성공하면 AuthenticationSuccessHandler가 동작합니다. 기본 구현체로는 SavedRequestAwareAuthenticationSuccessHandler가 주로 사용됩니다.
  2. 이 핸들러는 HttpSessionRequestCache를 사용하여 이전에 저장된 요청 정보를 검색합니다.
  3. HttpSessionRequestCache의 getRequest() 메소드를 호출하여 HTTP 세션에서 DefaultSavedRequest 객체를 가져옵니다.
  4. DefaultSavedRequest 객체에는 원래 요청의 URL, 파라미터, 헤더 등의 정보가 포함되어 있습니다.
  5. 핸들러는 이 정보를 사용하여 사용자를 원래 요청했던 페이지로 리다이렉트합니다. 주로 DefaultSavedRequest의 getRedirectUrl() 메소드를 사용하여 리다이렉트 URL을 결정합니다.
  6. 만약 저장된 요청이 없다면 (즉, 사용자가 직접 로그인 페이지에 접근한 경우), 설정된 기본 타겟 URL로 리다이렉트됩니다.

이 과정을 통해 Spring Security는 사용자가 로그인 전에 접근하려 했던 페이지로 자연스럽게 리다이렉트할 수 있게 되며, 이는 더 나은 사용자 경험을 제공합니다.

 

requestCache() API

 

 

요청 Url 에 customParam=y 라는 이름의 매개 변수가 있는 경우에만

HttpSession 에 저장된 SavedRequest 을 꺼내오도록 설정할 수 있다 (기본값은 "continue" 이다)

 

즉, 특정 파라미터가 존재한다면 HttpSessionRequestCache를 이용하여 Http Session에 저장된  SavedRequest를 꺼내올 수 있도록 설정할 수 있는 메소드가 존재한다.

 

[ 모든 http request마다 HttpSessionRequestCache를 이용하여 Http Session에 저장된  SavedRequest를 꺼내오는 작업을 하는것은 리소스 낭비이기 때문에 특정 파라미터를 가지는 http request일때만 해당 작업을 하기 위해 설정 할 수 있다.]

HttpSessionRequestCacherequestCache=newHttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("customParam=y");
http
        .requestCache((cache)->cache
                .requestCache(requestCache)
        );

 [  Spring Security의 요청 캐시 설정을 커스터마이즈 ] [요청 == request]

 

  • setMatchingRequestParameterName() 메소드의 실제 용도:
    • 이 메소드는 HttpSession에 이미 저장된 SavedRequest를 꺼내올 때 사용되는 조건을 설정합니다.
    • 설정된 파라미터 (예: "customParam=y")가 현재 요청 URL에 있는 경우에만 저장된 SavedRequest를 꺼내옵니다.
  • 기본 동작:
    • 기본값으로 "continue" 파라미터가 사용됩니다.
    • 즉, 요청 URL에 "continue" 파라미터가 있을 때만 저장된 SavedRequest를 사용합니다.

 

  • 특정 조건(이 경우 "customParam=y")을 만족하는 요청만 캐시하고 싶을 때
  • 인증되지 않은 사용자가 보호된 리소스에 접근하려고 할 때, 해당 요청을 저장했다가 인증 후에 원래 요청한 페이지로 리다이렉트하는 기능을 특정 요청에만 적용하고 싶을 때

이 코드는 요청 캐시를 완전히 비활성화하지 않고, 특정 조건에 따라 선택적으로 캐시를 사용하도록 설정합니다.

 

 

요청을 저장하지 않도록하려면 NullRequestCache 구현을 사용할 수 있다

RequestCachenullRequestCache=newNullRequestCache();
http
        .requestCache((cache)->cache
                .requestCache(nullRequestCache)
        );

 

[ Spring Security의 요청 캐시 기능을 비활성화 ]

 

요청 캐시는 보통 인증되지 않은 사용자가 보호된 리소스에 접근하려고 할 때,

해당 요청을 저장했다가 인증 후에 원래 요청한 페이지로 리다이렉트하는 데 사용됩니다.

 

그러나 이 코드는 그러한 기능을 비활성화하여, 인증 후에 특정 페이지로의 자동 리다이렉션을 방지합니다.

이 설정은 주로 RESTful API나 단일 페이지 애플리케이션(SPA)과 같이 클라이언트 측에서

인증 흐름을 직접 제어하고자 할 때 유용할 수 있습니다.

 

 

RequestCacheAwareFilter

 

RequestCacheAwareFilter는 이전에 저장했던 웹 요청을 다시 불러오는 역할을 한다

 

SavedRequest가 현재 Request와 일치하면 SavedRequest을 필터 체인의 doFilter 메소드에 전달하고

SavedRequest가 없으면 필터는 현재 Request을 그대로 진행시킨다

 

세션에 저장된 request가 없다면 그냥 doFilter로 넘어간다. [ 현재 request,response 전달하고 ]

 

세션에 저장된 request가 있다면 현재 request랑 일치하는지 비교한다. [ 내용 비교 ==> url 등 ]

 

같다면 세션에 저장된 request를 전달하고, 아니라면 현재 request를 전달한다.

 

간단하게 RequestCacheAwareFilter는 현재 세션에 저장된 request가 있다면 다음 필터에 전달해주는 역할을 한다.

[ 다음 필터 부터는 저장된 request를 활용 할 수 있게끔 ]

 

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;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
          throws IOException, ServletException {
       HttpServletRequest wrappedSavedRequest = this.requestCache.getMatchingRequest((HttpServletRequest) request,
             (HttpServletResponse) response);
       chain.doFilter((wrappedSavedRequest != null) ? wrappedSavedRequest : request, response);
    }

}

RequestCacheAwareFilter는 모든 http 요청에서 동작한다.

 

HttpServletRequest wrappedSavedRequest = this.requestCache.getMatchingRequest((HttpServletRequest) request,
       (HttpServletResponse) response);
getMatchingRequest() 메소드를 살펴보면

HttpSessionRequestCache 클래스

 

 

matchingRequestParameterName의 값을 가지고 요청 url의 쿼리스트링과 같은지 비교하는 작업을 한다.

[ 아무 설정없으면 기본값 == continue ]

비교해서 다르면 null을 반환하고 

null이라면 원래 request를 전달하고, 아니라면 savedRequest를 전달해주는 코드로 구성되어있다.

 

 

 

테스트

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, HttpSecurity httpSecurity) throws Exception {

        HttpSessionRequestCache requestCache = new HttpSessionRequestCache();

        http.authorizeHttpRequests(auth -> auth
                        .requestMatchers("/logoutSuccess").permitAll()
                        .anyRequest().authenticated())
                .formLogin(httpSecurityFormLoginConfigurer -> httpSecurityFormLoginConfigurer
                        .successHandler(new AuthenticationSuccessHandler() {
                            @Override
                            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                                SavedRequest savedRequest = requestCache.getRequest(request, response);
                                String redirectUrl = savedRequest.getRedirectUrl();
                                response.sendRedirect(redirectUrl);

                            }
                        }));
        return http.build();

    }

http://localhost:8080/home 으로 http request를 한뒤 로그인을 하면

요청했던 url을 다시 요청하고 뒤에 파라미터로 continue가 붙게된다.

 

 

 

http://localhost:8080/home으로 요청하게되면

 

  • ExceptionTranslationFilter가 AuthenticationException을 잡아내고 RequestCache를 사용하여 현재 요청을 저장합니다.
  • 이때 HttpSessionRequestCache가 사용됩니다 (기본 구현체).

 

savedRequest가 세션에 생성되고

로그인을 성공하면 

.successHandler(new AuthenticationSuccessHandler() {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        String redirectUrl = savedRequest.getRedirectUrl();
        response.sendRedirect(redirectUrl);

    }
}

 

해당 성공 핸들러가 동작하면서 http://localhost:8080/home?continue라는 url로 리다이렉트된다.

뒤에 continue가 붙는 이유는 

savedRequest.getRedirectUrl()

해당 메소드가

@Override
public String getRedirectUrl() {
    String queryString = createQueryString(this.queryString, this.matchingRequestParameterName);
    return UrlUtils.buildFullRequestUrl(this.scheme, this.serverName, this.serverPort, this.requestURI,
          queryString);
}

 

url 뒤에 matchingRequestParameterName 를 붙여주는 형식으로 리다이렉트할 url을 만들어준다.

 

그렇게 http://localhost:8080/home?continue로 리다이렉트 된다.

이 파라미터가 있으면, Spring Security는 추가 리다이렉션을 하지 않는다.

 

댓글