Skip to content

Commit 04b3d07

Browse files
Merge branch '6.0.x'
2 parents 82a1492 + a484044 commit 04b3d07

File tree

7 files changed

+238
-13
lines changed

7 files changed

+238
-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-2022 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;
@@ -326,6 +328,7 @@ public List<LogoutHandler> getLogoutHandlers() {
326328
*/
327329
private LogoutFilter createLogoutFilter(H http) {
328330
this.contextLogoutHandler.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
331+
this.contextLogoutHandler.setSecurityContextRepository(getSecurityContextRepository(http));
329332
this.logoutHandlers.add(this.contextLogoutHandler);
330333
this.logoutHandlers.add(postProcess(new LogoutSuccessEventPublishingLogoutHandler()));
331334
LogoutHandler[] handlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
@@ -337,6 +340,14 @@ private LogoutFilter createLogoutFilter(H http) {
337340
return result;
338341
}
339342

343+
private SecurityContextRepository getSecurityContextRepository(H http) {
344+
SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
345+
if (securityContextRepository == null) {
346+
securityContextRepository = new HttpSessionSecurityContextRepository();
347+
}
348+
return securityContextRepository;
349+
}
350+
340351
private RequestMatcher getLogoutRequestMatcher(H http) {
341352
if (this.logoutRequestMatcher != null) {
342353
return this.logoutRequestMatcher;

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

+82-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 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,6 +16,8 @@
1616

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

19+
import jakarta.servlet.http.HttpServletRequest;
20+
import jakarta.servlet.http.HttpServletResponse;
1921
import org.apache.http.HttpHeaders;
2022
import org.junit.jupiter.api.Test;
2123
import org.junit.jupiter.api.extension.ExtendWith;
@@ -25,6 +27,8 @@
2527
import org.springframework.context.annotation.Bean;
2628
import org.springframework.context.annotation.Configuration;
2729
import org.springframework.http.MediaType;
30+
import org.springframework.mock.web.MockHttpSession;
31+
import org.springframework.security.config.Customizer;
2832
import org.springframework.security.config.annotation.ObjectPostProcessor;
2933
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
3034
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -39,6 +43,8 @@
3943
import org.springframework.security.web.authentication.RememberMeServices;
4044
import org.springframework.security.web.authentication.logout.LogoutFilter;
4145
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
46+
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
47+
import org.springframework.security.web.context.SecurityContextRepository;
4248
import org.springframework.security.web.util.matcher.RequestMatcher;
4349
import org.springframework.test.web.servlet.MockMvc;
4450
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
@@ -48,6 +54,7 @@
4854
import static org.mockito.Mockito.atLeastOnce;
4955
import static org.mockito.Mockito.mock;
5056
import static org.mockito.Mockito.spy;
57+
import static org.mockito.Mockito.times;
5158
import static org.mockito.Mockito.verify;
5259
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
5360
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
@@ -324,6 +331,80 @@ public void logoutWhenDisabledThenLogoutUrlNotFound() throws Exception {
324331
this.mvc.perform(post("/logout").with(csrf())).andExpect(status().isNotFound());
325332
}
326333

334+
@Test
335+
public void logoutWhenCustomSecurityContextRepositoryThenUses() throws Exception {
336+
CustomSecurityContextRepositoryConfig.repository = mock(SecurityContextRepository.class);
337+
this.spring.register(CustomSecurityContextRepositoryConfig.class).autowire();
338+
// @formatter:off
339+
MockHttpServletRequestBuilder logoutRequest = post("/logout")
340+
.with(csrf())
341+
.with(user("user"))
342+
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
343+
this.mvc.perform(logoutRequest)
344+
.andExpect(status().isFound())
345+
.andExpect(redirectedUrl("/login?logout"));
346+
// @formatter:on
347+
int invocationCount = 2; // 1 from user() post processor and 1 from
348+
// SecurityContextLogoutHandler
349+
verify(CustomSecurityContextRepositoryConfig.repository, times(invocationCount)).saveContext(any(),
350+
any(HttpServletRequest.class), any(HttpServletResponse.class));
351+
}
352+
353+
@Test
354+
public void logoutWhenNoSecurityContextRepositoryThenHttpSessionSecurityContextRepository() throws Exception {
355+
this.spring.register(InvalidateHttpSessionFalseConfig.class).autowire();
356+
MockHttpSession session = mock(MockHttpSession.class);
357+
// @formatter:off
358+
MockHttpServletRequestBuilder logoutRequest = post("/logout")
359+
.with(csrf())
360+
.with(user("user"))
361+
.session(session)
362+
.header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
363+
this.mvc.perform(logoutRequest)
364+
.andExpect(status().isFound())
365+
.andExpect(redirectedUrl("/login?logout"))
366+
.andReturn();
367+
// @formatter:on
368+
verify(session).removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
369+
}
370+
371+
@Configuration
372+
@EnableWebSecurity
373+
static class InvalidateHttpSessionFalseConfig {
374+
375+
@Bean
376+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
377+
// @formatter:off
378+
http
379+
.logout((logout) -> logout.invalidateHttpSession(false))
380+
.securityContext((context) -> context.requireExplicitSave(true));
381+
return http.build();
382+
// @formatter:on
383+
}
384+
385+
}
386+
387+
@Configuration
388+
@EnableWebSecurity
389+
static class CustomSecurityContextRepositoryConfig {
390+
391+
static SecurityContextRepository repository;
392+
393+
@Bean
394+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
395+
// @formatter:off
396+
http
397+
.logout(Customizer.withDefaults())
398+
.securityContext((context) -> context
399+
.requireExplicitSave(true)
400+
.securityContextRepository(repository)
401+
);
402+
return http.build();
403+
// @formatter:on
404+
}
405+
406+
}
407+
327408
@Configuration
328409
@EnableWebSecurity
329410
static class NullLogoutSuccessHandlerConfig {

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

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The default is that accessing the URL `/logout` logs the user out by:
1212
- Invalidating the HTTP Session
1313
- Cleaning up any RememberMe authentication that was configured
1414
- Clearing the `SecurityContextHolder`
15+
- Clearing the `SecurityContextRepository`
1516
- Redirecting to `/login?logout`
1617

1718
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.context.SecurityContext;
2828
import org.springframework.security.core.context.SecurityContextHolder;
2929
import org.springframework.security.core.context.SecurityContextHolderStrategy;
30+
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
31+
import org.springframework.security.web.context.SecurityContextRepository;
3032
import org.springframework.util.Assert;
3133

3234
/**
@@ -53,6 +55,8 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
5355

5456
private boolean clearAuthentication = true;
5557

58+
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
59+
5660
/**
5761
* Requires the request to be passed in.
5862
* @param request from which to obtain a HTTP session (cannot be null)
@@ -76,6 +80,8 @@ public void logout(HttpServletRequest request, HttpServletResponse response, Aut
7680
if (this.clearAuthentication) {
7781
context.setAuthentication(null);
7882
}
83+
SecurityContext emptyContext = this.securityContextHolderStrategy.createEmptyContext();
84+
this.securityContextRepository.saveContext(emptyContext, request, response);
7985
}
8086

8187
public boolean isInvalidateHttpSession() {
@@ -114,4 +120,14 @@ public void setClearAuthentication(boolean clearAuthentication) {
114120
this.clearAuthentication = clearAuthentication;
115121
}
116122

123+
/**
124+
* Sets the {@link SecurityContextRepository} to use. Default is
125+
* {@link HttpSessionSecurityContextRepository}.
126+
* @param securityContextRepository the {@link SecurityContextRepository} to use.
127+
*/
128+
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
129+
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
130+
this.securityContextRepository = securityContextRepository;
131+
}
132+
117133
}

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-2022 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.
@@ -149,13 +149,46 @@ public void saveContext(SecurityContext context, HttpServletRequest request, Htt
149149
SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
150150
SaveContextOnUpdateOrErrorResponseWrapper.class);
151151
if (responseWrapper == null) {
152-
boolean httpSessionExists = request.getSession(false) != null;
153-
SecurityContext initialContext = this.securityContextHolderStrategy.createEmptyContext();
154-
responseWrapper = new SaveToSessionResponseWrapper(response, request, httpSessionExists, initialContext);
152+
saveContextInHttpSession(context, request);
153+
return;
155154
}
156155
responseWrapper.saveContext(context);
157156
}
158157

158+
private void saveContextInHttpSession(SecurityContext context, HttpServletRequest request) {
159+
if (isTransient(context) || isTransient(context.getAuthentication())) {
160+
return;
161+
}
162+
SecurityContext emptyContext = generateNewContext();
163+
if (emptyContext.equals(context)) {
164+
HttpSession session = request.getSession(false);
165+
removeContextFromSession(context, session);
166+
}
167+
else {
168+
boolean createSession = this.allowSessionCreation;
169+
HttpSession session = request.getSession(createSession);
170+
setContextInSession(context, session);
171+
}
172+
}
173+
174+
private void setContextInSession(SecurityContext context, HttpSession session) {
175+
if (session != null) {
176+
session.setAttribute(this.springSecurityContextKey, context);
177+
if (this.logger.isDebugEnabled()) {
178+
this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, session));
179+
}
180+
}
181+
}
182+
183+
private void removeContextFromSession(SecurityContext context, HttpSession session) {
184+
if (session != null) {
185+
session.removeAttribute(this.springSecurityContextKey);
186+
if (this.logger.isDebugEnabled()) {
187+
this.logger.debug(LogMessage.format("Removed %s from HttpSession [%s]", context, session));
188+
}
189+
}
190+
}
191+
159192
@Override
160193
public boolean containsContext(HttpServletRequest request) {
161194
HttpSession session = request.getSession(false);
@@ -392,11 +425,8 @@ protected void saveContext(SecurityContext context) {
392425
// We may have a new session, so check also whether the context attribute
393426
// is set SEC-1561
394427
if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) {
395-
httpSession.setAttribute(springSecurityContextKey, context);
428+
HttpSessionSecurityContextRepository.this.saveContextInHttpSession(context, this.request);
396429
this.isSaveContextInvoked = true;
397-
if (this.logger.isDebugEnabled()) {
398-
this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, httpSession));
399-
}
400430
}
401431
}
402432
}

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)