diff --git a/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java index d8367c4ebd..0aaf696f0d 100644 --- a/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java +++ b/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java @@ -31,6 +31,10 @@ final class GlobalSecurityContextHolderStrategy implements SecurityContextHolder private static SecurityContext contextHolder; + SecurityContext peek() { + return contextHolder; + } + @Override public void clearContext() { contextHolder = null; diff --git a/core/src/main/java/org/springframework/security/core/context/InheritableThreadLocalSecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/InheritableThreadLocalSecurityContextHolderStrategy.java index cb415500ca..7ce665c2a1 100644 --- a/core/src/main/java/org/springframework/security/core/context/InheritableThreadLocalSecurityContextHolderStrategy.java +++ b/core/src/main/java/org/springframework/security/core/context/InheritableThreadLocalSecurityContextHolderStrategy.java @@ -29,6 +29,10 @@ final class InheritableThreadLocalSecurityContextHolderStrategy implements Secur private static final ThreadLocal contextHolder = new InheritableThreadLocal<>(); + SecurityContext peek() { + return contextHolder.get(); + } + @Override public void clearContext() { contextHolder.remove(); diff --git a/core/src/main/java/org/springframework/security/core/context/ListeningSecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/ListeningSecurityContextHolderStrategy.java new file mode 100644 index 0000000000..24c0fbd17e --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/context/ListeningSecurityContextHolderStrategy.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.core.context; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +final class ListeningSecurityContextHolderStrategy implements SecurityContextHolderStrategy { + + private static final BiConsumer NULL_PUBLISHER = (previous, current) -> { + }; + + private final Supplier peek; + + private final SecurityContextHolderStrategy delegate; + + private final SecurityContextEventPublisher base = new SecurityContextEventPublisher(); + + private BiConsumer publisher = NULL_PUBLISHER; + + ListeningSecurityContextHolderStrategy(Supplier peek, SecurityContextHolderStrategy delegate) { + this.peek = peek; + this.delegate = delegate; + } + + @Override + public void clearContext() { + SecurityContext from = this.peek.get(); + this.delegate.clearContext(); + this.publisher.accept(from, null); + } + + @Override + public SecurityContext getContext() { + return this.delegate.getContext(); + } + + @Override + public void setContext(SecurityContext context) { + SecurityContext from = this.peek.get(); + this.delegate.setContext(context); + this.publisher.accept(from, context); + } + + @Override + public SecurityContext createEmptyContext() { + return this.delegate.createEmptyContext(); + } + + void addListener(SecurityContextChangedListener listener) { + this.base.listeners.add(listener); + this.publisher = this.base; + } + + private static class SecurityContextEventPublisher implements BiConsumer { + + private final List listeners = new CopyOnWriteArrayList<>(); + + @Override + public void accept(SecurityContext previous, SecurityContext current) { + if (previous == current) { + return; + } + SecurityContextChangedEvent event = new SecurityContextChangedEvent(previous, current); + for (SecurityContextChangedListener listener : this.listeners) { + listener.securityContextChanged(event); + } + } + + } + +} diff --git a/core/src/main/java/org/springframework/security/core/context/SecurityContextChangedEvent.java b/core/src/main/java/org/springframework/security/core/context/SecurityContextChangedEvent.java new file mode 100644 index 0000000000..34f7be989e --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/context/SecurityContextChangedEvent.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.core.context; + +import org.springframework.context.ApplicationEvent; + +/** + * An event that represents a change in {@link SecurityContext} + * + * @author Josh Cummings + * @since 5.6 + */ +public class SecurityContextChangedEvent extends ApplicationEvent { + + private final SecurityContext previous; + + private final SecurityContext current; + + /** + * Construct an event + * @param previous the old security context + * @param current the new security context + */ + public SecurityContextChangedEvent(SecurityContext previous, SecurityContext current) { + super(SecurityContextHolder.class); + this.previous = previous; + this.current = current; + } + + /** + * Get the {@link SecurityContext} set on the {@link SecurityContextHolder} + * immediately previous to this event + * @return the previous {@link SecurityContext} + */ + public SecurityContext getPreviousContext() { + return this.previous; + } + + /** + * Get the {@link SecurityContext} set on the {@link SecurityContextHolder} as of this + * event + * @return the current {@link SecurityContext} + */ + public SecurityContext getCurrentContext() { + return this.current; + } + +} diff --git a/core/src/main/java/org/springframework/security/core/context/SecurityContextChangedListener.java b/core/src/main/java/org/springframework/security/core/context/SecurityContextChangedListener.java new file mode 100644 index 0000000000..c0244a7095 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/context/SecurityContextChangedListener.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.core.context; + +/** + * A listener for {@link SecurityContextChangedEvent}s + * + * @author Josh Cummings + * @since 5.6 + */ +@FunctionalInterface +public interface SecurityContextChangedListener { + + void securityContextChanged(SecurityContextChangedEvent event); + +} diff --git a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java index 810edef7c9..cfce45ad25 100644 --- a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java +++ b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java @@ -18,6 +18,7 @@ package org.springframework.security.core.context; import java.lang.reflect.Constructor; +import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -73,13 +74,16 @@ public class SecurityContextHolder { strategyName = MODE_THREADLOCAL; } if (strategyName.equals(MODE_THREADLOCAL)) { - strategy = new ThreadLocalSecurityContextHolderStrategy(); + ThreadLocalSecurityContextHolderStrategy delegate = new ThreadLocalSecurityContextHolderStrategy(); + strategy = new ListeningSecurityContextHolderStrategy(delegate::peek, delegate); } else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) { - strategy = new InheritableThreadLocalSecurityContextHolderStrategy(); + InheritableThreadLocalSecurityContextHolderStrategy delegate = new InheritableThreadLocalSecurityContextHolderStrategy(); + strategy = new ListeningSecurityContextHolderStrategy(delegate::peek, delegate); } else if (strategyName.equals(MODE_GLOBAL)) { - strategy = new GlobalSecurityContextHolderStrategy(); + GlobalSecurityContextHolderStrategy delegate = new GlobalSecurityContextHolderStrategy(); + strategy = new ListeningSecurityContextHolderStrategy(delegate::peek, delegate); } else { // Try to load a custom strategy @@ -155,6 +159,35 @@ public class SecurityContextHolder { return strategy.createEmptyContext(); } + /** + * Register a listener to be notified when the {@link SecurityContext} changes. + * + * Note that this does not notify when the underlying authentication changes. To get + * notified about authentication changes, ensure that you are using + * {@link #setContext} when changing the authentication like so: + * + *
+	 *	SecurityContext context = SecurityContextHolder.createEmptyContext();
+	 *	context.setAuthentication(authentication);
+	 *	SecurityContextHolder.setContext(context);
+	 * 
+ * + * To integrate this with Spring's + * {@link org.springframework.context.ApplicationEvent} support, you can add a + * listener like so: + * + *
+	 *	SecurityContextHolder.addListener(this.applicationContext::publishEvent);
+	 * 
+ * @param listener a listener to be notified when the {@link SecurityContext} changes + * @since 5.6 + */ + public static void addListener(SecurityContextChangedListener listener) { + Assert.isInstanceOf(ListeningSecurityContextHolderStrategy.class, strategy, + "strategy must be of type ListeningSecurityContextHolderStrategy to add listeners"); + ((ListeningSecurityContextHolderStrategy) strategy).addListener(listener); + } + @Override public String toString() { return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount=" + initializeCount + "]"; diff --git a/core/src/main/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategy.java index 801f5c8207..a3094bfa70 100644 --- a/core/src/main/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategy.java +++ b/core/src/main/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategy.java @@ -30,6 +30,10 @@ final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextH private static final ThreadLocal contextHolder = new ThreadLocal<>(); + SecurityContext peek() { + return contextHolder.get(); + } + @Override public void clearContext() { contextHolder.remove(); diff --git a/core/src/test/java/org/springframework/security/core/context/SecurityContextHolderTests.java b/core/src/test/java/org/springframework/security/core/context/SecurityContextHolderTests.java index 11fd9be02b..d1eaf70909 100644 --- a/core/src/test/java/org/springframework/security/core/context/SecurityContextHolderTests.java +++ b/core/src/test/java/org/springframework/security/core/context/SecurityContextHolderTests.java @@ -23,6 +23,10 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * Tests {@link SecurityContextHolder}. @@ -58,4 +62,17 @@ public class SecurityContextHolderTests { assertThatIllegalArgumentException().isThrownBy(() -> SecurityContextHolder.setContext(null)); } + @Test + public void addListenerWhenInvokedThenListenersAreNotified() { + SecurityContextChangedListener one = mock(SecurityContextChangedListener.class); + SecurityContextChangedListener two = mock(SecurityContextChangedListener.class); + SecurityContextHolder.addListener(one); + SecurityContextHolder.addListener(two); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + SecurityContextHolder.setContext(context); + SecurityContextHolder.clearContext(); + verify(one, times(2)).securityContextChanged(any(SecurityContextChangedEvent.class)); + verify(two, times(2)).securityContextChanged(any(SecurityContextChangedEvent.class)); + } + } diff --git a/web/src/main/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.java b/web/src/main/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.java index 5dcd61ad97..1e84ab64eb 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.java +++ b/web/src/main/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.java @@ -68,14 +68,11 @@ public class SecurityContextLogoutHandler implements LogoutHandler { } } } + SecurityContext context = SecurityContextHolder.getContext(); + SecurityContextHolder.clearContext(); if (this.clearAuthentication) { - SecurityContext context = SecurityContextHolder.getContext(); - SecurityContextHolder.clearContext(); context.setAuthentication(null); } - else { - SecurityContextHolder.clearContext(); - } } public boolean isInvalidateHttpSession() {