[ODOP] 97일차 - 로그아웃 처리 구현
![[ODOP] 97일차 - 로그아웃 처리 구현](/content/images/size/w1200/2023/09/odop-1.png)
처리 흐름
-
로그아웃 방법
- form 태그를 이용하여 POST 로 요청
- a 태그를 활용하여 GET 으로 요청 - SecurityContextLogoutHandler 사용
-
인증 여부에 따라 로그인/로그아웃 표현
타임리프에서 지원하는 기능을 활용하여 구현 할 수 있다
-
<li sec:authorize=”isAnonymous()”><a th:href=”@{/login}”>로그인</a></li>
해당 버튼은
-
<li sec:authorize=”isAuthenticated()”><a th:href=”@{/logout}”>로그아웃</a></li>
-
구현
타임리프에서 logout 에 대한 기능을 지원한다. 이를 위해서는 의존성이 필요하다
의존성
Maven
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.1.0.M1</version>
</dependency>
Gradle
implementation group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-springsecurity5', version: '3.1.0.M1'
Code
로그아웃의 실제 처리를 보기 전에 Security 는 어떻게 로그아웃 처리를 하고 있는지 살펴보자
- spring security 는 LogoutFilter 를 통해 로그아웃을 처리
- 로그아웃 필터로 매핑된 uri 는 기본적으로
/logout
.
아래 코드에서 확인 할 수 있다
우선 기본적인 로그아웃 처리를 진행하는 LogoutFilter 의 내용을 살펴보자
package org.springframework.security.web.authentication.logout;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.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.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 RequestMatcher logoutRequestMatcher;
private final LogoutHandler handler;
private final LogoutSuccessHandler logoutSuccessHandler;
...
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");**
}
...
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (requiresLogout(request, response)) {
**Authentication auth = SecurityContextHolder.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);
}
}
- 생성자 마지막 줄을 보면 setFilterProcessesUrl 를 지정 해 주는것을 볼 수 있다.
doFilter 메서드를 확인해보면 다음과 같은 코드가 보인다
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
this.handler.logout(request, response, auth);
this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
실질적으로 로그아웃 처리를 하는 구간으로 확인된다
결국 logout 처리를 위해 SecurityContextHolder 에서 Context 를 가져와 logoutHandler 에게 넘기고, 그 처리가 끝난 후 logoutSuccessHandler 에서 로그아웃 성공 처리를 하는 것
위 구현처럼 구현해주면 되는데, 우리는 이를 api 로 구현 할 것이다.
만약 로그아웃을 Filter 로 구현해야 한다면 위와 같이 GenericFilterBean 을 상속받은 CustomLogoutFilter 를 만들어 LogoutHandler 와 LogoutSuccessHandler 를 넣어주면 될 것 같다.
다음은 SecurityContextLogoutHandler.java 에서 실제로 로그아웃을 처리하는 구간
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (this.invalidateHttpSession) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Invalidated session %s", session.getId()));
}
}
}
SecurityContext context = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
if (this.clearAuthentication) {
context.setAuthentication(null);
}
}
다른 의존성이 없이 해당 코드만으로 로직이 동작하는 것을 알 수 있다.
결국 중요한건 SecurityContextHolder.clearContext()
만약 사용자의 중복 로그인을 방지하는 처리를 위해 사용자 토큰데이터를 영속상태로 만들었다면 해당 구간에서 처리 해 주면 될 것으로 보인다.
우리가 구현할 로그아웃 흐름에서는 LogoutSuccessHandler 는 필요하지 않다
왜냐면 타임리프를 사용했기 때문에 그냥 mvc controller 에서 redirect 시켜주면 되기 때문이다.
구현
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Controller
public class LoginController {
...
@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication != null) {
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
return "redirect:/login";
}
}
Controller 의 세부 구현을 보면 위에서 봤던 코드가 그대로 사용되는 모습을 볼 수 있다.