[ODOP] 97일차 - 로그아웃 처리 구현

[ODOP] 97일차 - 로그아웃 처리 구현

처리 흐름

  • 로그아웃 방법

    • 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 의 세부 구현을 보면 위에서 봤던 코드가 그대로 사용되는 모습을 볼 수 있다.