로그아웃
• 스프링 시큐리티는 기본적으로 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 자체를 초기 상태로 되돌립니다.
- 첫 번째 라인은 명시적으로 Authentication을 null로 설정하여 인증 정보를 제거합니다.
- 두 번째 라인은 전체 컨텍스트를 초기화하여 혹시 남아있을 수 있는 다른 보안 관련 정보까지 모두 제거합니다.
실제로
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가 동작한다.
[로그아웃 프로세스]
- HTTP POST 요청이 /logout 엔드포인트로 전송됩니다.
- LogoutFilter가 이 요청을 가로챕니다.
- LogoutFilter는 내부적으로 RequestMatcher를 사용하여 이 요청이 로그아웃 요청인지 확인합니다. 기본적으로 POST /logout 요청을 매치합니다.
- 요청이 매치되면, LogoutFilter는 구성된 LogoutHandler들을 순차적으로 실행합니다. 일반적인 LogoutHandler들은 다음과 같습니다:
- SecurityContextLogoutHandler: SecurityContext를 정리합니다.
- CookieClearingLogoutHandler: 지정된 쿠키들을 제거합니다.
- CsrfLogoutHandler: CSRF 토큰을 정리합니다.
- RememberMeServices: "Remember Me" 기능 관련 데이터를 정리합니다.
- 모든 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 ]
- 인증이 성공하면 AuthenticationSuccessHandler가 동작합니다. 기본 구현체로는 SavedRequestAwareAuthenticationSuccessHandler가 주로 사용됩니다.
- 이 핸들러는 HttpSessionRequestCache를 사용하여 이전에 저장된 요청 정보를 검색합니다.
- HttpSessionRequestCache의 getRequest() 메소드를 호출하여 HTTP 세션에서 DefaultSavedRequest 객체를 가져옵니다.
- DefaultSavedRequest 객체에는 원래 요청의 URL, 파라미터, 헤더 등의 정보가 포함되어 있습니다.
- 핸들러는 이 정보를 사용하여 사용자를 원래 요청했던 페이지로 리다이렉트합니다. 주로 DefaultSavedRequest의 getRedirectUrl() 메소드를 사용하여 리다이렉트 URL을 결정합니다.
- 만약 저장된 요청이 없다면 (즉, 사용자가 직접 로그인 페이지에 접근한 경우), 설정된 기본 타겟 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() 메소드를 살펴보면
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는 추가 리다이렉션을 하지 않는다.
'인프런 > 스프링 시큐리티 완전 정복 [6.x 개정판]' 카테고리의 다른 글
8) 인증 관리자 - AuthenticationManager (1) | 2024.10.15 |
---|---|
7) 인증 아키텍처 - Authentication , SecurityContext / SecurityContextHolder (1) | 2024.10.14 |
5) 인증프로세스 - BasicAuthenticationFilter , rememberMe() , RememberMeAuthenticationFilter, anonymous() (3) | 2024.10.10 |
4) 인증프로세스 - 폼 인증,폼 인증 필터, HTTP Basic 인증 (0) | 2024.10.08 |
3) 초기화 과정이해 - DelegatingFilterProxy / FilterChainProxy , 사용자 정의 보안 설정하기 (0) | 2024.10.05 |
댓글