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;
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");
}
@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);
}
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;
}
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;
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;
public interface LogoutHandler {
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. 참고.