Skip to content

Commit 3e87ef8

Browse files
committed
Replace SecurityContextHolder#addListener
Closes gh-10226
1 parent 58d5088 commit 3e87ef8

File tree

7 files changed

+242
-110
lines changed

7 files changed

+242
-110
lines changed

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

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

3232
private static SecurityContext contextHolder;
3333

34-
SecurityContext peek() {
35-
return contextHolder;
36-
}
37-
3834
@Override
3935
public void clearContext() {
4036
contextHolder = null;

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,6 @@ 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-
3632
@Override
3733
public void clearContext() {
3834
contextHolder.remove();

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

Lines changed: 93 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,73 +16,130 @@
1616

1717
package org.springframework.security.core.context;
1818

19-
import java.util.List;
20-
import java.util.concurrent.CopyOnWriteArrayList;
21-
import java.util.function.BiConsumer;
22-
import java.util.function.Supplier;
19+
import java.util.Arrays;
20+
import java.util.Collection;
2321

24-
final class ListeningSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
22+
import org.springframework.util.Assert;
2523

26-
private static final BiConsumer<SecurityContext, SecurityContext> NULL_PUBLISHER = (previous, current) -> {
27-
};
24+
/**
25+
* An API for notifying when the {@link SecurityContext} changes.
26+
*
27+
* Note that this does not notify when the underlying authentication changes. To get
28+
* notified about authentication changes, ensure that you are using {@link #setContext}
29+
* when changing the authentication like so:
30+
*
31+
* <pre>
32+
* SecurityContext context = SecurityContextHolder.createEmptyContext();
33+
* context.setAuthentication(authentication);
34+
* SecurityContextHolder.setContext(context);
35+
* </pre>
36+
*
37+
* To add a listener to the existing {@link SecurityContextHolder}, you can do:
38+
*
39+
* <pre>
40+
* SecurityContextHolderStrategy original = SecurityContextHolder.getContextHolderStrategy();
41+
* SecurityContextChangedListener listener = new YourListener();
42+
* SecurityContextHolderStrategy strategy = new ListeningSecurityContextHolderStrategy(original, listener);
43+
* SecurityContextHolder.setContextHolderStrategy(strategy);
44+
* </pre>
45+
*
46+
* NOTE: Any object that you supply to the {@link SecurityContextHolder} is now part of
47+
* the static context and as such will not get garbage collected. To remove the reference,
48+
* {@link SecurityContextHolder#setContextHolderStrategy reset the strategy} like so:
49+
*
50+
* <pre>
51+
* SecurityContextHolder.setContextHolderStrategy(original);
52+
* </pre>
53+
*
54+
* This will then allow {@code YourListener} and its members to be garbage collected.
55+
*
56+
* @author Josh Cummings
57+
* @since 5.6
58+
*/
59+
public final class ListeningSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
2860

29-
private final Supplier<SecurityContext> peek;
61+
private final Collection<SecurityContextChangedListener> listeners;
3062

3163
private final SecurityContextHolderStrategy delegate;
3264

33-
private final SecurityContextEventPublisher base = new SecurityContextEventPublisher();
34-
35-
private BiConsumer<SecurityContext, SecurityContext> publisher = NULL_PUBLISHER;
65+
/**
66+
* Construct a {@link ListeningSecurityContextHolderStrategy}
67+
* @param listeners the listeners that should be notified when the
68+
* {@link SecurityContext} is {@link #setContext(SecurityContext) set} or
69+
* {@link #clearContext() cleared}
70+
* @param delegate the underlying {@link SecurityContextHolderStrategy}
71+
*/
72+
public ListeningSecurityContextHolderStrategy(SecurityContextHolderStrategy delegate,
73+
Collection<SecurityContextChangedListener> listeners) {
74+
Assert.notNull(delegate, "securityContextHolderStrategy cannot be null");
75+
Assert.notNull(listeners, "securityContextChangedListeners cannot be null");
76+
Assert.notEmpty(listeners, "securityContextChangedListeners cannot be empty");
77+
Assert.noNullElements(listeners, "securityContextChangedListeners cannot contain null elements");
78+
this.delegate = delegate;
79+
this.listeners = listeners;
80+
}
3681

37-
ListeningSecurityContextHolderStrategy(Supplier<SecurityContext> peek, SecurityContextHolderStrategy delegate) {
38-
this.peek = peek;
82+
/**
83+
* Construct a {@link ListeningSecurityContextHolderStrategy}
84+
* @param listeners the listeners that should be notified when the
85+
* {@link SecurityContext} is {@link #setContext(SecurityContext) set} or
86+
* {@link #clearContext() cleared}
87+
* @param delegate the underlying {@link SecurityContextHolderStrategy}
88+
*/
89+
public ListeningSecurityContextHolderStrategy(SecurityContextHolderStrategy delegate,
90+
SecurityContextChangedListener... listeners) {
91+
Assert.notNull(delegate, "securityContextHolderStrategy cannot be null");
92+
Assert.notNull(listeners, "securityContextChangedListeners cannot be null");
93+
Assert.notEmpty(listeners, "securityContextChangedListeners cannot be empty");
94+
Assert.noNullElements(listeners, "securityContextChangedListeners cannot contain null elements");
3995
this.delegate = delegate;
96+
this.listeners = Arrays.asList(listeners);
4097
}
4198

99+
/**
100+
* {@inheritDoc}
101+
*/
42102
@Override
43103
public void clearContext() {
44-
SecurityContext from = this.peek.get();
104+
SecurityContext from = getContext();
45105
this.delegate.clearContext();
46-
this.publisher.accept(from, null);
106+
publish(from, null);
47107
}
48108

109+
/**
110+
* {@inheritDoc}
111+
*/
49112
@Override
50113
public SecurityContext getContext() {
51114
return this.delegate.getContext();
52115
}
53116

117+
/**
118+
* {@inheritDoc}
119+
*/
54120
@Override
55121
public void setContext(SecurityContext context) {
56-
SecurityContext from = this.peek.get();
122+
SecurityContext from = getContext();
57123
this.delegate.setContext(context);
58-
this.publisher.accept(from, context);
124+
publish(from, context);
59125
}
60126

127+
/**
128+
* {@inheritDoc}
129+
*/
61130
@Override
62131
public SecurityContext createEmptyContext() {
63132
return this.delegate.createEmptyContext();
64133
}
65134

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-
}
135+
private void publish(SecurityContext previous, SecurityContext current) {
136+
if (previous == current) {
137+
return;
138+
}
139+
SecurityContextChangedEvent event = new SecurityContextChangedEvent(previous, current);
140+
for (SecurityContextChangedListener listener : this.listeners) {
141+
listener.securityContextChanged(event);
84142
}
85-
86143
}
87144

88145
}

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

Lines changed: 68 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ public class SecurityContextHolder {
5656

5757
public static final String MODE_GLOBAL = "MODE_GLOBAL";
5858

59+
private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
60+
5961
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
6062

6163
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
@@ -69,34 +71,41 @@ public class SecurityContextHolder {
6971
}
7072

7173
private static void initialize() {
74+
initializeStrategy();
75+
initializeCount++;
76+
}
77+
78+
private static void initializeStrategy() {
79+
if (MODE_PRE_INITIALIZED.equals(strategyName)) {
80+
Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
81+
+ ", setContextHolderStrategy must be called with the fully constructed strategy");
82+
return;
83+
}
7284
if (!StringUtils.hasText(strategyName)) {
7385
// Set default
7486
strategyName = MODE_THREADLOCAL;
7587
}
7688
if (strategyName.equals(MODE_THREADLOCAL)) {
77-
ThreadLocalSecurityContextHolderStrategy delegate = new ThreadLocalSecurityContextHolderStrategy();
78-
strategy = new ListeningSecurityContextHolderStrategy(delegate::peek, delegate);
89+
strategy = new ThreadLocalSecurityContextHolderStrategy();
90+
return;
7991
}
80-
else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
81-
InheritableThreadLocalSecurityContextHolderStrategy delegate = new InheritableThreadLocalSecurityContextHolderStrategy();
82-
strategy = new ListeningSecurityContextHolderStrategy(delegate::peek, delegate);
92+
if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
93+
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
94+
return;
8395
}
84-
else if (strategyName.equals(MODE_GLOBAL)) {
85-
GlobalSecurityContextHolderStrategy delegate = new GlobalSecurityContextHolderStrategy();
86-
strategy = new ListeningSecurityContextHolderStrategy(delegate::peek, delegate);
96+
if (strategyName.equals(MODE_GLOBAL)) {
97+
strategy = new GlobalSecurityContextHolderStrategy();
98+
return;
8799
}
88-
else {
89-
// Try to load a custom strategy
90-
try {
91-
Class<?> clazz = Class.forName(strategyName);
92-
Constructor<?> customStrategy = clazz.getConstructor();
93-
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
94-
}
95-
catch (Exception ex) {
96-
ReflectionUtils.handleReflectionException(ex);
97-
}
100+
// Try to load a custom strategy
101+
try {
102+
Class<?> clazz = Class.forName(strategyName);
103+
Constructor<?> customStrategy = clazz.getConstructor();
104+
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
105+
}
106+
catch (Exception ex) {
107+
ReflectionUtils.handleReflectionException(ex);
98108
}
99-
initializeCount++;
100109
}
101110

102111
/**
@@ -118,7 +127,9 @@ public static SecurityContext getContext() {
118127
* Primarily for troubleshooting purposes, this method shows how many times the class
119128
* has re-initialized its <code>SecurityContextHolderStrategy</code>.
120129
* @return the count (should be one unless you've called
121-
* {@link #setStrategyName(String)} to switch to an alternate strategy.
130+
* {@link #setStrategyName(String)} or
131+
* {@link #setContextHolderStrategy(SecurityContextHolderStrategy)} to switch to an
132+
* alternate strategy).
122133
*/
123134
public static int getInitializeCount() {
124135
return initializeCount;
@@ -144,6 +155,41 @@ public static void setStrategyName(String strategyName) {
144155
initialize();
145156
}
146157

158+
/**
159+
* Use this {@link SecurityContextHolderStrategy}.
160+
*
161+
* Call either {@link #setStrategyName(String)} or this method, but not both.
162+
*
163+
* This method is not thread safe. Changing the strategy while requests are in-flight
164+
* may cause race conditions.
165+
*
166+
* {@link SecurityContextHolder} maintains a static reference to the provided
167+
* {@link SecurityContextHolderStrategy}. This means that the strategy and its members
168+
* will not be garbage collected until you remove your strategy.
169+
*
170+
* To ensure garbage collection, remember the original strategy like so:
171+
*
172+
* <pre>
173+
* SecurityContextHolderStrategy original = SecurityContextHolder.getContextHolderStrategy();
174+
* SecurityContextHolder.setContextHolderStrategy(myStrategy);
175+
* </pre>
176+
*
177+
* And then when you are ready for {@code myStrategy} to be garbage collected you can
178+
* do:
179+
*
180+
* <pre>
181+
* SecurityContextHolder.setContextHolderStrategy(original);
182+
* </pre>
183+
* @param strategy the {@link SecurityContextHolderStrategy} to use
184+
* @since 5.6
185+
*/
186+
public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
187+
Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
188+
SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED;
189+
SecurityContextHolder.strategy = strategy;
190+
initialize();
191+
}
192+
147193
/**
148194
* Allows retrieval of the context strategy. See SEC-1188.
149195
* @return the configured strategy for storing the security context.
@@ -159,38 +205,10 @@ public static SecurityContext createEmptyContext() {
159205
return strategy.createEmptyContext();
160206
}
161207

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-
191208
@Override
192209
public String toString() {
193-
return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount=" + initializeCount + "]";
210+
return "SecurityContextHolder[strategy='" + strategy.getClass().getSimpleName() + "'; initializeCount="
211+
+ initializeCount + "]";
194212
}
195213

196214
}

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ 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-
3733
@Override
3834
public void clearContext() {
3935
contextHolder.remove();

0 commit comments

Comments
 (0)