Spring Security - 11. LogoutFilter

하쮸·2025년 2월 8일

1. LogoutFilter.

  • LogoutFilter
    • LogoutFilter는 스프링 시큐리티 의존성을 추가하면 자동으로 등록되는 SecurityFilterChain 안에 소속 되어 있는 필터 중 하나임.
      • 해당 필터의 목적은 사용자가 로그인, 즉 인증이 되면 서버에 사용자에 대한 식별정보들이 저장되는데(세션 or 시큐리티컨텍스트)
        이러한 정보들을 LogoutFilter에 등록되어 있는 여러 개의 LogoutHandler가 순차적으로 실행되면서 정보들을 지우는 역할을 함.
      • 세션 방식에 대한 로그아웃이 디폴트로 설정되어 있으므로, 만약 JWT 방식이나 다른 방식으로 하려면 커스텀해서 구현해야함.
  • Authentication Logout - Handling Logouts
package org.springframework.security.web.authentication.logout;

import java.io.IOException;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.core.log.LogMessage;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

/**
 * Logs a principal out.
 * <p>
 * Polls a series of {@link LogoutHandler}s. The handlers should be specified in the order
 * they are required. Generally you will want to call logout handlers
 * <code>TokenBasedRememberMeServices</code> and <code>SecurityContextLogoutHandler</code>
 * (in that order).
 * <p>
 * After logout, a redirect will be performed to the URL determined by either the
 * configured <tt>LogoutSuccessHandler</tt> or the <tt>logoutSuccessUrl</tt>, depending on
 * which constructor was used.
 *
 * @author Ben Alex
 * @author Eddú Meléndez
 */
public class LogoutFilter extends GenericFilterBean {

	private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
		.getContextHolderStrategy();

	private RequestMatcher logoutRequestMatcher;

	private final LogoutHandler handler;

	private final LogoutSuccessHandler logoutSuccessHandler;

	/**
	 * Constructor which takes a <tt>LogoutSuccessHandler</tt> instance to determine the
	 * target destination after logging out. The list of <tt>LogoutHandler</tt>s are
	 * intended to perform the actual logout functionality (such as clearing the security
	 * context, invalidating the session, etc.).
	 */
	public LogoutFilter(LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... handlers) {
		this.handler = new CompositeLogoutHandler(handlers);
		Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
		this.logoutSuccessHandler = logoutSuccessHandler;
		setFilterProcessesUrl("/logout");
	}

	public LogoutFilter(String logoutSuccessUrl, LogoutHandler... handlers) {
		this.handler = new CompositeLogoutHandler(handlers);
		Assert.isTrue(!StringUtils.hasLength(logoutSuccessUrl) || UrlUtils.isValidRedirectUrl(logoutSuccessUrl),
				() -> logoutSuccessUrl + " isn't a valid redirect URL");
		SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
		if (StringUtils.hasText(logoutSuccessUrl)) {
			urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl);
		}
		this.logoutSuccessHandler = urlLogoutSuccessHandler;
		setFilterProcessesUrl("/logout");
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
	}

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		if (requiresLogout(request, response)) {
			Authentication auth = this.securityContextHolderStrategy.getContext().getAuthentication();
			if (this.logger.isDebugEnabled()) {
				this.logger.debug(LogMessage.format("Logging out [%s]", auth));
			}
			this.handler.logout(request, response, auth);
			this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
			return;
		}
		chain.doFilter(request, response);
	}

	/**
	 * Allow subclasses to modify when a logout should take place.
	 * @param request the request
	 * @param response the response
	 * @return <code>true</code> if logout should occur, <code>false</code> otherwise
	 */
	protected boolean requiresLogout(HttpServletRequest request, HttpServletResponse response) {
		if (this.logoutRequestMatcher.matches(request)) {
			return true;
		}
		if (this.logger.isTraceEnabled()) {
			this.logger.trace(LogMessage.format("Did not match request to %s", this.logoutRequestMatcher));
		}
		return false;
	}

	/**
	 * Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
	 * the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
	 *
	 * @since 5.8
	 */
	public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
		Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
		this.securityContextHolderStrategy = securityContextHolderStrategy;
	}

	public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
		Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
		this.logoutRequestMatcher = logoutRequestMatcher;
	}

	public void setFilterProcessesUrl(String filterProcessesUrl) {
		this.logoutRequestMatcher = new AntPathRequestMatcher(filterProcessesUrl);
	}

}
  • GenericFilterBean을 상속받아 구현되어 있기 때문에 내부에서 포워딩이 이뤄져도 계속 수행될 수 있도록 정의되어 있음.

1-1. 주요 로직.

      private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
              throws IOException, ServletException {
          if (requiresLogout(request, response)) {
              Authentication auth = this.securityContextHolderStrategy.getContext().getAuthentication();
              if (this.logger.isDebugEnabled()) {
                  this.logger.debug(LogMessage.format("Logging out [%s]", auth));
              }
              this.handler.logout(request, response, auth);
              this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
              return;
          }
          chain.doFilter(request, response);
      }
  • if (requiresLogout(request, response))
    • 로그아웃 요청 확인.
    • chain.doFilter(request, response);
      • 로그아웃 요청이 아닐 경우 다음 필터로 넘김.
  • this.handler.logout(request, response, auth);
    • 로그아웃 핸들러들을 실행.
    • 사용자의 세션을 무효화하고, 저장된 인증 정보를 삭제.
  • this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
    • 성공핸들러를 실행시켜서 리다이렉션 or 응답을 처리함.

1-2. CompositeLogoutHandler.

public class LogoutFilter extends GenericFilterBean {

	private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
		.getContextHolderStrategy();

	private RequestMatcher logoutRequestMatcher;

	private final LogoutHandler handler;

	private final LogoutSuccessHandler logoutSuccessHandler;

	public LogoutFilter(LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... handlers) {
		this.handler = new CompositeLogoutHandler(handlers);
		Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
		this.logoutSuccessHandler = logoutSuccessHandler;
		setFilterProcessesUrl("/logout");
	}

	public LogoutFilter(String logoutSuccessUrl, LogoutHandler... handlers) {
		this.handler = new CompositeLogoutHandler(handlers);
		Assert.isTrue(!StringUtils.hasLength(logoutSuccessUrl) || UrlUtils.isValidRedirectUrl(logoutSuccessUrl),
				() -> logoutSuccessUrl + " isn't a valid redirect URL");
		SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
		if (StringUtils.hasText(logoutSuccessUrl)) {
			urlLogoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl);
		}
		this.logoutSuccessHandler = urlLogoutSuccessHandler;
		setFilterProcessesUrl("/logout");
	}

			....
}
  • LogoutFilter클래스 내부에 필드로 LogoutHandler가 선언되어 있고 생성자를 통해서 new CompositeLogoutHandler(handlers)주입되어 있음.
package org.springframework.security.web.authentication.logout;

import java.util.Arrays;
import java.util.List;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;

/**
 * Performs a logout through all the {@link LogoutHandler} implementations. If any
 * exception is thrown by
 * {@link #logout(HttpServletRequest, HttpServletResponse, Authentication)}, no additional
 * LogoutHandler are invoked.
 *
 * @author Eddú Meléndez
 * @since 4.2.0
 */
public final class CompositeLogoutHandler implements LogoutHandler {

	private final List<LogoutHandler> logoutHandlers;

	public CompositeLogoutHandler(LogoutHandler... logoutHandlers) {
		Assert.notEmpty(logoutHandlers, "LogoutHandlers are required");
		this.logoutHandlers = Arrays.asList(logoutHandlers);
	}

	public CompositeLogoutHandler(List<LogoutHandler> logoutHandlers) {
		Assert.notEmpty(logoutHandlers, "LogoutHandlers are required");
		this.logoutHandlers = logoutHandlers;
	}

	@Override
	public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
		for (LogoutHandler handler : this.logoutHandlers) {
			handler.logout(request, response, authentication);
		}
	}

}
  • for문을 통해서 순차적으로 호출하면서 LogoutHandler 클래스들을 수행.

1-3. LogoutHandler

package org.springframework.security.web.authentication.logout;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;

/**
 * Indicates a class that is able to participate in logout handling.
 *
 * <p>
 * Called by {@link LogoutFilter}.
 *
 * @author Ben Alex
 */
public interface LogoutHandler {

	/**
	 * Causes a logout to be completed. The method must complete successfully.
	 * @param request the HTTP request
	 * @param response the HTTP response
	 * @param authentication the current principal details
	 */
	void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication);

}
  • 로그아웃 핸들러들은 LogoutHandler 인터페이스를 구현(implements)한 클래스들로 이루어져 있음.
    • 커스텀 핸들러를 만들 때 또한 마찬가지로 위 인터페이스를 기반으로 작성해야함.

  • AbstractRememberMeServices
    • Remember-Me(자동 로그인) 기능을 지원하는 추상 클래스.
    • 쿠키를 기반으로 한 Remember-Me 인증을 제공하며, 로그아웃 시 해당 쿠키를 삭제하는 역할 수행.
  • CookieClearingLogoutHandler
    • 로그아웃 시 특정 쿠키를 삭제하는 역할.
  • HeaderWriterLogoutHandler
    • 로그아웃 시 특정 HTTP 응답 헤더를 추가하는 역할.
  • SecurityContextLogoutHandler
    • 로그아웃 시 SecurityContext를 정리하고(사용자 인증정보를 제거) 세션을 무효화하는 기본적인 핸들러.
  • LogoutSuccessEventPublishingLogoutHandler
    • 로그아웃 성공 후 특정 이벤트를 발생시키는 역할.
  • 기본적으로 등록되어 있는 핸들러는 SecurityContextLogoutHandler, LogoutSuccessEventPublishingLogoutHandler 2개임.

1-4. 커스텀 LogoutHandler

CookieClearingLogoutHandler cookies = new CookieClearingLogoutHandler("our-custom-cookie");
http
    .logout((logout) -> logout.addLogoutHandler(cookies))
  • 로그아웃시 특정 쿠키(our-custom-cookie)를 삭제.
HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter());
http
    .logout((logout) -> logout.addLogoutHandler(clearSiteData))
  • 위 코드는 모든 사이트 데이터를 지우지만, 아래와 같이 쿠키만 제거하도록 구성할 수도 있음.
HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directive.COOKIES));
http
    .logout((logout) -> logout.addLogoutHandler(clearSiteData))

2. 참고.

profile
Every cloud has a silver lining.

0개의 댓글