Skip to content

Commit 6cf8c53

Browse files
Merge branch '5.7.x' into 5.8.x
2 parents e9ee2ce + 2d52fb8 commit 6cf8c53

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

Lines changed: 12 additions & 1 deletion
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

Lines changed: 85 additions & 1 deletion
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,14 +16,20 @@
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.SecurityContextChangedListenerConfig;
2935
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
@@ -33,9 +39,12 @@
3339
import org.springframework.security.config.test.SpringTestContext;
3440
import org.springframework.security.config.test.SpringTestContextExtension;
3541
import org.springframework.security.core.context.SecurityContextHolderStrategy;
42+
import org.springframework.security.web.SecurityFilterChain;
3643
import org.springframework.security.web.authentication.RememberMeServices;
3744
import org.springframework.security.web.authentication.logout.LogoutFilter;
3845
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
46+
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
47+
import org.springframework.security.web.context.SecurityContextRepository;
3948
import org.springframework.security.web.util.matcher.RequestMatcher;
4049
import org.springframework.test.web.servlet.MockMvc;
4150
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
@@ -45,6 +54,7 @@
4554
import static org.mockito.Mockito.atLeastOnce;
4655
import static org.mockito.Mockito.mock;
4756
import static org.mockito.Mockito.spy;
57+
import static org.mockito.Mockito.times;
4858
import static org.mockito.Mockito.verify;
4959
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
5060
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
@@ -321,6 +331,80 @@ public void logoutWhenDisabledThenLogoutUrlNotFound() throws Exception {
321331
this.mvc.perform(post("/logout").with(csrf())).andExpect(status().isNotFound());
322332
}
323333

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+
324408
@EnableWebSecurity
325409
static class NullLogoutSuccessHandlerConfig extends WebSecurityConfigurerAdapter {
326410

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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.springframework.security.core.context.SecurityContext;
2929
import org.springframework.security.core.context.SecurityContextHolder;
3030
import org.springframework.security.core.context.SecurityContextHolderStrategy;
31+
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
32+
import org.springframework.security.web.context.SecurityContextRepository;
3133
import org.springframework.util.Assert;
3234

3335
/**
@@ -54,6 +56,8 @@ public class SecurityContextLogoutHandler implements LogoutHandler {
5456

5557
private boolean clearAuthentication = true;
5658

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

8288
public boolean isInvalidateHttpSession() {
@@ -115,4 +121,14 @@ public void setClearAuthentication(boolean clearAuthentication) {
115121
this.clearAuthentication = clearAuthentication;
116122
}
117123

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

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

Lines changed: 38 additions & 8 deletions
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.
@@ -150,13 +150,46 @@ public void saveContext(SecurityContext context, HttpServletRequest request, Htt
150150
SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
151151
SaveContextOnUpdateOrErrorResponseWrapper.class);
152152
if (responseWrapper == null) {
153-
boolean httpSessionExists = request.getSession(false) != null;
154-
SecurityContext initialContext = this.securityContextHolderStrategy.createEmptyContext();
155-
responseWrapper = new SaveToSessionResponseWrapper(response, request, httpSessionExists, initialContext);
153+
saveContextInHttpSession(context, request);
154+
return;
156155
}
157156
responseWrapper.saveContext(context);
158157
}
159158

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

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

Lines changed: 40 additions & 2 deletions
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)