Skip to content

Commit 2d52fb8

Browse files
Clear Repository on Logout
1 parent 37d8846 commit 2d52fb8

File tree

7 files changed

+241
-13
lines changed

7 files changed

+241
-13
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -35,6 +35,8 @@
3535
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
3636
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
3737
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
38+
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
39+
import org.springframework.security.web.context.SecurityContextRepository;
3840
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
3941
import org.springframework.security.web.util.matcher.OrRequestMatcher;
4042
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -325,6 +327,7 @@ public List<LogoutHandler> getLogoutHandlers() {
325327
* @return the {@link LogoutFilter} to use.
326328
*/
327329
private LogoutFilter createLogoutFilter(H http) {
330+
this.contextLogoutHandler.setSecurityContextRepository(getSecurityContextRepository(http));
328331
this.logoutHandlers.add(this.contextLogoutHandler);
329332
this.logoutHandlers.add(postProcess(new LogoutSuccessEventPublishingLogoutHandler()));
330333
LogoutHandler[] handlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
@@ -334,6 +337,14 @@ private LogoutFilter createLogoutFilter(H http) {
334337
return result;
335338
}
336339

340+
private SecurityContextRepository getSecurityContextRepository(H http) {
341+
SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
342+
if (securityContextRepository == null) {
343+
securityContextRepository = new HttpSessionSecurityContextRepository();
344+
}
345+
return securityContextRepository;
346+
}
347+
337348
private RequestMatcher getLogoutRequestMatcher(H http) {
338349
if (this.logoutRequestMatcher != null) {
339350
return this.logoutRequestMatcher;

config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java

+85-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,24 +16,33 @@
1616

1717
package org.springframework.security.config.annotation.web.configurers;
1818

19+
import javax.servlet.http.HttpServletRequest;
20+
import javax.servlet.http.HttpServletResponse;
21+
1922
import org.apache.http.HttpHeaders;
2023
import org.junit.jupiter.api.Test;
2124
import org.junit.jupiter.api.extension.ExtendWith;
2225

2326
import org.springframework.beans.factory.BeanCreationException;
2427
import org.springframework.beans.factory.annotation.Autowired;
2528
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Configuration;
2630
import org.springframework.http.MediaType;
31+
import org.springframework.mock.web.MockHttpSession;
32+
import org.springframework.security.config.Customizer;
2733
import org.springframework.security.config.annotation.ObjectPostProcessor;
2834
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
2935
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3036
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
3137
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
3238
import org.springframework.security.config.test.SpringTestContext;
3339
import org.springframework.security.config.test.SpringTestContextExtension;
40+
import org.springframework.security.web.SecurityFilterChain;
3441
import org.springframework.security.web.authentication.RememberMeServices;
3542
import org.springframework.security.web.authentication.logout.LogoutFilter;
3643
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
44+
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
45+
import org.springframework.security.web.context.SecurityContextRepository;
3746
import org.springframework.security.web.util.matcher.RequestMatcher;
3847
import org.springframework.test.web.servlet.MockMvc;
3948
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
@@ -42,6 +51,7 @@
4251
import static org.mockito.ArgumentMatchers.any;
4352
import static org.mockito.Mockito.mock;
4453
import static org.mockito.Mockito.spy;
54+
import static org.mockito.Mockito.times;
4555
import static org.mockito.Mockito.verify;
4656
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
4757
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
@@ -302,6 +312,80 @@ public void logoutWhenDisabledThenLogoutUrlNotFound() throws Exception {
302312
this.mvc.perform(post("/logout").with(csrf())).andExpect(status().isNotFound());
303313
}
304314

315+
@Test
316+
public void logoutWhenCustomSecurityContextRepositoryThenUses() throws Exception {
317+
CustomSecurityContextRepositoryConfig.repository = mock(SecurityContextRepository.class);
318+
this.spring.register(CustomSecurityContextRepositoryConfig.class).autowire();
319+
// @formatter:off
320+
MockHttpServletRequestBuilder logoutRequest = post("/logout")
321+
.with(csrf())
322+
.with(user("user"))
323+
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
324+
this.mvc.perform(logoutRequest)
325+
.andExpect(status().isFound())
326+
.andExpect(redirectedUrl("/login?logout"));
327+
// @formatter:on
328+
int invocationCount = 2; // 1 from user() post processor and 1 from
329+
// SecurityContextLogoutHandler
330+
verify(CustomSecurityContextRepositoryConfig.repository, times(invocationCount)).saveContext(any(),
331+
any(HttpServletRequest.class), any(HttpServletResponse.class));
332+
}
333+
334+
@Test
335+
public void logoutWhenNoSecurityContextRepositoryThenHttpSessionSecurityContextRepository() throws Exception {
336+
this.spring.register(InvalidateHttpSessionFalseConfig.class).autowire();
337+
MockHttpSession session = mock(MockHttpSession.class);
338+
// @formatter:off
339+
MockHttpServletRequestBuilder logoutRequest = post("/logout")
340+
.with(csrf())
341+
.with(user("user"))
342+
.session(session)
343+
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
344+
this.mvc.perform(logoutRequest)
345+
.andExpect(status().isFound())
346+
.andExpect(redirectedUrl("/login?logout"))
347+
.andReturn();
348+
// @formatter:on
349+
verify(session).removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
350+
}
351+
352+
@Configuration
353+
@EnableWebSecurity
354+
static class InvalidateHttpSessionFalseConfig {
355+
356+
@Bean
357+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
358+
// @formatter:off
359+
http
360+
.logout((logout) -> logout.invalidateHttpSession(false))
361+
.securityContext((context) -> context.requireExplicitSave(true));
362+
return http.build();
363+
// @formatter:on
364+
}
365+
366+
}
367+
368+
@Configuration
369+
@EnableWebSecurity
370+
static class CustomSecurityContextRepositoryConfig {
371+
372+
static SecurityContextRepository repository;
373+
374+
@Bean
375+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
376+
// @formatter:off
377+
http
378+
.logout(Customizer.withDefaults())
379+
.securityContext((context) -> context
380+
.requireExplicitSave(true)
381+
.securityContextRepository(repository)
382+
);
383+
return http.build();
384+
// @formatter:on
385+
}
386+
387+
}
388+
305389
@EnableWebSecurity
306390
static class NullLogoutSuccessHandlerConfig extends WebSecurityConfigurerAdapter {
307391

docs/modules/ROOT/pages/servlet/authentication/logout.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The default is that accessing the URL `/logout` will log the user out by:
1010
- Invalidating the HTTP Session
1111
- Cleaning up any RememberMe authentication that was configured
1212
- Clearing the `SecurityContextHolder`
13+
- Clearing the `SecurityContextRepository`
1314
- Redirect to `/login?logout`
1415

1516
Similar to configuring login capabilities, however, you also have various options to further customize your logout requirements:

web/src/main/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.java

+16
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import org.springframework.security.core.Authentication;
2828
import org.springframework.security.core.context.SecurityContext;
2929
import org.springframework.security.core.context.SecurityContextHolder;
30+
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
31+
import org.springframework.security.web.context.SecurityContextRepository;
3032
import org.springframework.util.Assert;
3133

3234
/**
@@ -50,6 +52,8 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
5052

5153
private boolean clearAuthentication = true;
5254

55+
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
56+
5357
/**
5458
* Requires the request to be passed in.
5559
* @param request from which to obtain a HTTP session (cannot be null)
@@ -73,6 +77,8 @@ public void logout(HttpServletRequest request, HttpServletResponse response, Aut
7377
if (this.clearAuthentication) {
7478
context.setAuthentication(null);
7579
}
80+
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
81+
this.securityContextRepository.saveContext(emptyContext, request, response);
7682
}
7783

7884
public boolean isInvalidateHttpSession() {
@@ -100,4 +106,14 @@ public void setClearAuthentication(boolean clearAuthentication) {
100106
this.clearAuthentication = clearAuthentication;
101107
}
102108

109+
/**
110+
* Sets the {@link SecurityContextRepository} to use. Default is
111+
* {@link HttpSessionSecurityContextRepository}.
112+
* @param securityContextRepository the {@link SecurityContextRepository} to use.
113+
*/
114+
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
115+
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
116+
this.securityContextRepository = securityContextRepository;
117+
}
118+
103119
}

web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java

+38-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -137,13 +137,46 @@ public void saveContext(SecurityContext context, HttpServletRequest request, Htt
137137
SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
138138
SaveContextOnUpdateOrErrorResponseWrapper.class);
139139
if (responseWrapper == null) {
140-
boolean httpSessionExists = request.getSession(false) != null;
141-
SecurityContext initialContext = SecurityContextHolder.createEmptyContext();
142-
responseWrapper = new SaveToSessionResponseWrapper(response, request, httpSessionExists, initialContext);
140+
saveContextInHttpSession(context, request);
141+
return;
143142
}
144143
responseWrapper.saveContext(context);
145144
}
146145

146+
private void saveContextInHttpSession(SecurityContext context, HttpServletRequest request) {
147+
if (isTransient(context) || isTransient(context.getAuthentication())) {
148+
return;
149+
}
150+
SecurityContext emptyContext = generateNewContext();
151+
if (emptyContext.equals(context)) {
152+
HttpSession session = request.getSession(false);
153+
removeContextFromSession(context, session);
154+
}
155+
else {
156+
boolean createSession = this.allowSessionCreation;
157+
HttpSession session = request.getSession(createSession);
158+
setContextInSession(context, session);
159+
}
160+
}
161+
162+
private void setContextInSession(SecurityContext context, HttpSession session) {
163+
if (session != null) {
164+
session.setAttribute(this.springSecurityContextKey, context);
165+
if (this.logger.isDebugEnabled()) {
166+
this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, session));
167+
}
168+
}
169+
}
170+
171+
private void removeContextFromSession(SecurityContext context, HttpSession session) {
172+
if (session != null) {
173+
session.removeAttribute(this.springSecurityContextKey);
174+
if (this.logger.isDebugEnabled()) {
175+
this.logger.debug(LogMessage.format("Removed %s from HttpSession [%s]", context, session));
176+
}
177+
}
178+
}
179+
147180
@Override
148181
public boolean containsContext(HttpServletRequest request) {
149182
HttpSession session = request.getSession(false);
@@ -369,11 +402,8 @@ protected void saveContext(SecurityContext context) {
369402
// We may have a new session, so check also whether the context attribute
370403
// is set SEC-1561
371404
if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) {
372-
httpSession.setAttribute(springSecurityContextKey, context);
405+
HttpSessionSecurityContextRepository.this.saveContextInHttpSession(context, this.request);
373406
this.isSaveContextInvoked = true;
374-
if (this.logger.isDebugEnabled()) {
375-
this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, httpSession));
376-
}
377407
}
378408
}
379409
}

web/src/test/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandlerTests.java

+40-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,12 +27,19 @@
2727
import org.springframework.security.core.authority.AuthorityUtils;
2828
import org.springframework.security.core.context.SecurityContext;
2929
import org.springframework.security.core.context.SecurityContextHolder;
30+
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
31+
import org.springframework.security.web.context.SecurityContextRepository;
32+
import org.springframework.test.util.ReflectionTestUtils;
3033

3134
import static org.assertj.core.api.Assertions.assertThat;
35+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
36+
import static org.mockito.ArgumentMatchers.any;
37+
import static org.mockito.ArgumentMatchers.eq;
38+
import static org.mockito.Mockito.mock;
39+
import static org.mockito.Mockito.verify;
3240

3341
/**
3442
* @author Rob Winch
35-
*
3643
*/
3744
public class SecurityContextLogoutHandlerTests {
3845

@@ -76,4 +83,35 @@ public void disableClearsAuthentication() {
7683
assertThat(beforeContext.getAuthentication()).isSameAs(beforeAuthentication);
7784
}
7885

86+
@Test
87+
public void logoutWhenSecurityContextRepositoryThenSaveEmptyContext() {
88+
SecurityContextRepository repository = mock(SecurityContextRepository.class);
89+
this.handler.setSecurityContextRepository(repository);
90+
this.handler.logout(this.request, this.response, SecurityContextHolder.getContext().getAuthentication());
91+
verify(repository).saveContext(eq(SecurityContextHolder.createEmptyContext()), any(), any());
92+
}
93+
94+
@Test
95+
public void logoutWhenClearAuthenticationFalseThenSaveEmptyContext() {
96+
SecurityContextRepository repository = mock(SecurityContextRepository.class);
97+
this.handler.setSecurityContextRepository(repository);
98+
this.handler.setClearAuthentication(false);
99+
this.handler.logout(this.request, this.response, SecurityContextHolder.getContext().getAuthentication());
100+
verify(repository).saveContext(eq(SecurityContextHolder.createEmptyContext()), any(), any());
101+
}
102+
103+
@Test
104+
public void constructorWhenDefaultSecurityContextRepositoryThenHttpSessionSecurityContextRepository() {
105+
SecurityContextRepository securityContextRepository = (SecurityContextRepository) ReflectionTestUtils
106+
.getField(this.handler, "securityContextRepository");
107+
assertThat(securityContextRepository).isInstanceOf(HttpSessionSecurityContextRepository.class);
108+
}
109+
110+
@Test
111+
public void setSecurityContextRepositoryWhenNullThenException() {
112+
assertThatExceptionOfType(IllegalArgumentException.class)
113+
.isThrownBy(() -> this.handler.setSecurityContextRepository(null))
114+
.withMessage("securityContextRepository cannot be null");
115+
}
116+
79117
}

0 commit comments

Comments
 (0)