Skip to content

Commit 6f3e346

Browse files
shirosakijzheaux
authored andcommitted
Add SecurityContextHolder#addListener
Closes gh-10032
1 parent b8d5172 commit 6f3e346

File tree

9 files changed

+247
-8
lines changed

9 files changed

+247
-8
lines changed

core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ final class GlobalSecurityContextHolderStrategy implements SecurityContextHolder
3131

3232
private static SecurityContext contextHolder;
3333

34+
SecurityContext peek() {
35+
return contextHolder;
36+
}
37+
3438
@Override
3539
public void clearContext() {
3640
contextHolder = null;

core/src/main/java/org/springframework/security/core/context/InheritableThreadLocalSecurityContextHolderStrategy.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ final class InheritableThreadLocalSecurityContextHolderStrategy implements Secur
2929

3030
private static final ThreadLocal<SecurityContext> contextHolder = new InheritableThreadLocal<>();
3131

32+
SecurityContext peek() {
33+
return contextHolder.get();
34+
}
35+
3236
@Override
3337
public void clearContext() {
3438
contextHolder.remove();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.core.context;
18+
19+
import java.util.List;
20+
import java.util.concurrent.CopyOnWriteArrayList;
21+
import java.util.function.BiConsumer;
22+
import java.util.function.Supplier;
23+
24+
final class ListeningSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
25+
26+
private static final BiConsumer<SecurityContext, SecurityContext> NULL_PUBLISHER = (previous, current) -> {
27+
};
28+
29+
private final Supplier<SecurityContext> peek;
30+
31+
private final SecurityContextHolderStrategy delegate;
32+
33+
private final SecurityContextEventPublisher base = new SecurityContextEventPublisher();
34+
35+
private BiConsumer<SecurityContext, SecurityContext> publisher = NULL_PUBLISHER;
36+
37+
ListeningSecurityContextHolderStrategy(Supplier<SecurityContext> peek, SecurityContextHolderStrategy delegate) {
38+
this.peek = peek;
39+
this.delegate = delegate;
40+
}
41+
42+
@Override
43+
public void clearContext() {
44+
SecurityContext from = this.peek.get();
45+
this.delegate.clearContext();
46+
this.publisher.accept(from, null);
47+
}
48+
49+
@Override
50+
public SecurityContext getContext() {
51+
return this.delegate.getContext();
52+
}
53+
54+
@Override
55+
public void setContext(SecurityContext context) {
56+
SecurityContext from = this.peek.get();
57+
this.delegate.setContext(context);
58+
this.publisher.accept(from, context);
59+
}
60+
61+
@Override
62+
public SecurityContext createEmptyContext() {
63+
return this.delegate.createEmptyContext();
64+
}
65+
66+
void addListener(SecurityContextChangedListener listener) {
67+
this.base.listeners.add(listener);
68+
this.publisher = this.base;
69+
}
70+
71+
private static class SecurityContextEventPublisher implements BiConsumer<SecurityContext, SecurityContext> {
72+
73+
private final List<SecurityContextChangedListener> listeners = new CopyOnWriteArrayList<>();
74+
75+
@Override
76+
public void accept(SecurityContext previous, SecurityContext current) {
77+
if (previous == current) {
78+
return;
79+
}
80+
SecurityContextChangedEvent event = new SecurityContextChangedEvent(previous, current);
81+
for (SecurityContextChangedListener listener : this.listeners) {
82+
listener.securityContextChanged(event);
83+
}
84+
}
85+
86+
}
87+
88+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.core.context;
18+
19+
import org.springframework.context.ApplicationEvent;
20+
21+
/**
22+
* An event that represents a change in {@link SecurityContext}
23+
*
24+
* @author Josh Cummings
25+
* @since 5.6
26+
*/
27+
public class SecurityContextChangedEvent extends ApplicationEvent {
28+
29+
private final SecurityContext previous;
30+
31+
private final SecurityContext current;
32+
33+
/**
34+
* Construct an event
35+
* @param previous the old security context
36+
* @param current the new security context
37+
*/
38+
public SecurityContextChangedEvent(SecurityContext previous, SecurityContext current) {
39+
super(SecurityContextHolder.class);
40+
this.previous = previous;
41+
this.current = current;
42+
}
43+
44+
/**
45+
* Get the {@link SecurityContext} set on the {@link SecurityContextHolder}
46+
* immediately previous to this event
47+
* @return the previous {@link SecurityContext}
48+
*/
49+
public SecurityContext getPreviousContext() {
50+
return this.previous;
51+
}
52+
53+
/**
54+
* Get the {@link SecurityContext} set on the {@link SecurityContextHolder} as of this
55+
* event
56+
* @return the current {@link SecurityContext}
57+
*/
58+
public SecurityContext getCurrentContext() {
59+
return this.current;
60+
}
61+
62+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.core.context;
18+
19+
/**
20+
* A listener for {@link SecurityContextChangedEvent}s
21+
*
22+
* @author Josh Cummings
23+
* @since 5.6
24+
*/
25+
@FunctionalInterface
26+
public interface SecurityContextChangedListener {
27+
28+
void securityContextChanged(SecurityContextChangedEvent event);
29+
30+
}

core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.lang.reflect.Constructor;
2020

21+
import org.springframework.util.Assert;
2122
import org.springframework.util.ReflectionUtils;
2223
import org.springframework.util.StringUtils;
2324

@@ -73,13 +74,16 @@ private static void initialize() {
7374
strategyName = MODE_THREADLOCAL;
7475
}
7576
if (strategyName.equals(MODE_THREADLOCAL)) {
76-
strategy = new ThreadLocalSecurityContextHolderStrategy();
77+
ThreadLocalSecurityContextHolderStrategy delegate = new ThreadLocalSecurityContextHolderStrategy();
78+
strategy = new ListeningSecurityContextHolderStrategy(delegate::peek, delegate);
7779
}
7880
else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
79-
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
81+
InheritableThreadLocalSecurityContextHolderStrategy delegate = new InheritableThreadLocalSecurityContextHolderStrategy();
82+
strategy = new ListeningSecurityContextHolderStrategy(delegate::peek, delegate);
8083
}
8184
else if (strategyName.equals(MODE_GLOBAL)) {
82-
strategy = new GlobalSecurityContextHolderStrategy();
85+
GlobalSecurityContextHolderStrategy delegate = new GlobalSecurityContextHolderStrategy();
86+
strategy = new ListeningSecurityContextHolderStrategy(delegate::peek, delegate);
8387
}
8488
else {
8589
// Try to load a custom strategy
@@ -155,6 +159,35 @@ public static SecurityContext createEmptyContext() {
155159
return strategy.createEmptyContext();
156160
}
157161

162+
/**
163+
* Register a listener to be notified when the {@link SecurityContext} changes.
164+
*
165+
* Note that this does not notify when the underlying authentication changes. To get
166+
* notified about authentication changes, ensure that you are using
167+
* {@link #setContext} when changing the authentication like so:
168+
*
169+
* <pre>
170+
* SecurityContext context = SecurityContextHolder.createEmptyContext();
171+
* context.setAuthentication(authentication);
172+
* SecurityContextHolder.setContext(context);
173+
* </pre>
174+
*
175+
* To integrate this with Spring's
176+
* {@link org.springframework.context.ApplicationEvent} support, you can add a
177+
* listener like so:
178+
*
179+
* <pre>
180+
* SecurityContextHolder.addListener(this.applicationContext::publishEvent);
181+
* </pre>
182+
* @param listener a listener to be notified when the {@link SecurityContext} changes
183+
* @since 5.6
184+
*/
185+
public static void addListener(SecurityContextChangedListener listener) {
186+
Assert.isInstanceOf(ListeningSecurityContextHolderStrategy.class, strategy,
187+
"strategy must be of type ListeningSecurityContextHolderStrategy to add listeners");
188+
((ListeningSecurityContextHolderStrategy) strategy).addListener(listener);
189+
}
190+
158191
@Override
159192
public String toString() {
160193
return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount=" + initializeCount + "]";

core/src/main/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategy.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextH
3030

3131
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
3232

33+
SecurityContext peek() {
34+
return contextHolder.get();
35+
}
36+
3337
@Override
3438
public void clearContext() {
3539
contextHolder.remove();

core/src/test/java/org/springframework/security/core/context/SecurityContextHolderTests.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323

2424
import static org.assertj.core.api.Assertions.assertThat;
2525
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
26+
import static org.mockito.ArgumentMatchers.any;
27+
import static org.mockito.Mockito.mock;
28+
import static org.mockito.Mockito.times;
29+
import static org.mockito.Mockito.verify;
2630

2731
/**
2832
* Tests {@link SecurityContextHolder}.
@@ -58,4 +62,17 @@ public void testRejectsNulls() {
5862
assertThatIllegalArgumentException().isThrownBy(() -> SecurityContextHolder.setContext(null));
5963
}
6064

65+
@Test
66+
public void addListenerWhenInvokedThenListenersAreNotified() {
67+
SecurityContextChangedListener one = mock(SecurityContextChangedListener.class);
68+
SecurityContextChangedListener two = mock(SecurityContextChangedListener.class);
69+
SecurityContextHolder.addListener(one);
70+
SecurityContextHolder.addListener(two);
71+
SecurityContext context = SecurityContextHolder.createEmptyContext();
72+
SecurityContextHolder.setContext(context);
73+
SecurityContextHolder.clearContext();
74+
verify(one, times(2)).securityContextChanged(any(SecurityContextChangedEvent.class));
75+
verify(two, times(2)).securityContextChanged(any(SecurityContextChangedEvent.class));
76+
}
77+
6178
}

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,11 @@ public void logout(HttpServletRequest request, HttpServletResponse response, Aut
6868
}
6969
}
7070
}
71+
SecurityContext context = SecurityContextHolder.getContext();
72+
SecurityContextHolder.clearContext();
7173
if (this.clearAuthentication) {
72-
SecurityContext context = SecurityContextHolder.getContext();
73-
SecurityContextHolder.clearContext();
7474
context.setAuthentication(null);
7575
}
76-
else {
77-
SecurityContextHolder.clearContext();
78-
}
7976
}
8077

8178
public boolean isInvalidateHttpSession() {

0 commit comments

Comments
 (0)