Skip to content

Commit 8f10deb

Browse files
committed
Merge remote-tracking branch 'origin/5.8.x'
2 parents fc7f87f + f054505 commit 8f10deb

File tree

4 files changed

+174
-22
lines changed

4 files changed

+174
-22
lines changed

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

+54-10
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import java.util.Arrays;
2020
import java.util.Collection;
21+
import java.util.concurrent.atomic.AtomicBoolean;
22+
import java.util.function.Supplier;
2123

2224
import org.springframework.util.Assert;
2325

@@ -127,9 +129,9 @@ public ListeningSecurityContextHolderStrategy(SecurityContextHolderStrategy dele
127129
*/
128130
@Override
129131
public void clearContext() {
130-
SecurityContext from = getContext();
132+
Supplier<SecurityContext> deferred = this.delegate.getDeferredContext();
131133
this.delegate.clearContext();
132-
publish(from, null);
134+
publish(new SecurityContextChangedEvent(deferred, SecurityContextChangedEvent.NO_CONTEXT));
133135
}
134136

135137
/**
@@ -140,14 +142,28 @@ public SecurityContext getContext() {
140142
return this.delegate.getContext();
141143
}
142144

145+
/**
146+
* {@inheritDoc}
147+
*/
148+
@Override
149+
public Supplier<SecurityContext> getDeferredContext() {
150+
return this.delegate.getDeferredContext();
151+
}
152+
143153
/**
144154
* {@inheritDoc}
145155
*/
146156
@Override
147157
public void setContext(SecurityContext context) {
148-
SecurityContext from = getContext();
149-
this.delegate.setContext(context);
150-
publish(from, context);
158+
setDeferredContext(() -> context);
159+
}
160+
161+
/**
162+
* {@inheritDoc}
163+
*/
164+
@Override
165+
public void setDeferredContext(Supplier<SecurityContext> deferredContext) {
166+
this.delegate.setDeferredContext(new PublishOnceSupplier(getDeferredContext(), deferredContext));
151167
}
152168

153169
/**
@@ -158,14 +174,42 @@ public SecurityContext createEmptyContext() {
158174
return this.delegate.createEmptyContext();
159175
}
160176

161-
private void publish(SecurityContext previous, SecurityContext current) {
162-
if (previous == current) {
163-
return;
164-
}
165-
SecurityContextChangedEvent event = new SecurityContextChangedEvent(previous, current);
177+
private void publish(SecurityContextChangedEvent event) {
166178
for (SecurityContextChangedListener listener : this.listeners) {
167179
listener.securityContextChanged(event);
168180
}
169181
}
170182

183+
class PublishOnceSupplier implements Supplier<SecurityContext> {
184+
185+
private final AtomicBoolean isPublished = new AtomicBoolean(false);
186+
187+
private final Supplier<SecurityContext> old;
188+
189+
private final Supplier<SecurityContext> updated;
190+
191+
PublishOnceSupplier(Supplier<SecurityContext> old, Supplier<SecurityContext> updated) {
192+
if (old instanceof PublishOnceSupplier) {
193+
this.old = ((PublishOnceSupplier) old).updated;
194+
}
195+
else {
196+
this.old = old;
197+
}
198+
this.updated = updated;
199+
}
200+
201+
@Override
202+
public SecurityContext get() {
203+
SecurityContext updated = this.updated.get();
204+
if (this.isPublished.compareAndSet(false, true)) {
205+
SecurityContext old = this.old.get();
206+
if (old != updated) {
207+
publish(new SecurityContextChangedEvent(old, updated));
208+
}
209+
}
210+
return updated;
211+
}
212+
213+
}
214+
171215
}

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

+35-6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

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

19+
import java.util.function.Supplier;
20+
1921
import org.springframework.context.ApplicationEvent;
2022

2123
/**
@@ -26,28 +28,41 @@
2628
*/
2729
public class SecurityContextChangedEvent extends ApplicationEvent {
2830

29-
private final SecurityContext oldContext;
31+
public static final Supplier<SecurityContext> NO_CONTEXT = () -> null;
32+
33+
private final Supplier<SecurityContext> oldContext;
3034

31-
private final SecurityContext newContext;
35+
private final Supplier<SecurityContext> newContext;
3236

3337
/**
3438
* Construct an event
3539
* @param oldContext the old security context
36-
* @param newContext the new security context
40+
* @param newContext the new security context, use
41+
* {@link SecurityContextChangedEvent#NO_CONTEXT} for if the context is cleared
42+
* @since 5.8
3743
*/
38-
public SecurityContextChangedEvent(SecurityContext oldContext, SecurityContext newContext) {
44+
public SecurityContextChangedEvent(Supplier<SecurityContext> oldContext, Supplier<SecurityContext> newContext) {
3945
super(SecurityContextHolder.class);
4046
this.oldContext = oldContext;
4147
this.newContext = newContext;
4248
}
4349

50+
/**
51+
* Construct an event
52+
* @param oldContext the old security context
53+
* @param newContext the new security context
54+
*/
55+
public SecurityContextChangedEvent(SecurityContext oldContext, SecurityContext newContext) {
56+
this(() -> oldContext, (newContext != null) ? () -> newContext : NO_CONTEXT);
57+
}
58+
4459
/**
4560
* Get the {@link SecurityContext} set on the {@link SecurityContextHolder}
4661
* immediately previous to this event
4762
* @return the previous {@link SecurityContext}
4863
*/
4964
public SecurityContext getOldContext() {
50-
return this.oldContext;
65+
return this.oldContext.get();
5166
}
5267

5368
/**
@@ -56,7 +71,21 @@ public SecurityContext getOldContext() {
5671
* @return the current {@link SecurityContext}
5772
*/
5873
public SecurityContext getNewContext() {
59-
return this.newContext;
74+
return this.newContext.get();
75+
}
76+
77+
/**
78+
* Say whether the event is a context-clearing event.
79+
*
80+
* <p>
81+
* This method is handy for avoiding looking up the new context to confirm it is a
82+
* cleared event.
83+
* @return {@code true} if the new context is
84+
* {@link SecurityContextChangedEvent#NO_CONTEXT}
85+
* @since 5.8
86+
*/
87+
public boolean isCleared() {
88+
return this.newContext == NO_CONTEXT;
6089
}
6190

6291
}

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

+70-3
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,36 @@
1616

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

19+
import java.util.function.Supplier;
20+
1921
import org.junit.jupiter.api.Test;
22+
import org.mockito.ArgumentCaptor;
23+
24+
import org.springframework.security.authentication.TestingAuthenticationToken;
2025

26+
import static org.assertj.core.api.Assertions.assertThat;
2127
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2228
import static org.mockito.ArgumentMatchers.any;
2329
import static org.mockito.BDDMockito.given;
2430
import static org.mockito.Mockito.mock;
31+
import static org.mockito.Mockito.reset;
32+
import static org.mockito.Mockito.spy;
2533
import static org.mockito.Mockito.verify;
2634
import static org.mockito.Mockito.verifyNoInteractions;
35+
import static org.mockito.Mockito.verifyNoMoreInteractions;
2736

2837
public class ListeningSecurityContextHolderStrategyTests {
2938

3039
@Test
3140
public void setContextWhenInvokedThenListenersAreNotified() {
32-
SecurityContextHolderStrategy delegate = mock(SecurityContextHolderStrategy.class);
41+
SecurityContextHolderStrategy delegate = spy(new MockSecurityContextHolderStrategy());
3342
SecurityContextChangedListener one = mock(SecurityContextChangedListener.class);
3443
SecurityContextChangedListener two = mock(SecurityContextChangedListener.class);
3544
SecurityContextHolderStrategy strategy = new ListeningSecurityContextHolderStrategy(delegate, one, two);
3645
given(delegate.createEmptyContext()).willReturn(new SecurityContextImpl());
3746
SecurityContext context = strategy.createEmptyContext();
3847
strategy.setContext(context);
39-
verify(delegate).setContext(context);
48+
strategy.getContext();
4049
verify(one).securityContextChanged(any());
4150
verify(two).securityContextChanged(any());
4251
}
@@ -49,8 +58,66 @@ public void setContextWhenNoChangeToContextThenListenersAreNotNotified() {
4958
SecurityContext context = new SecurityContextImpl();
5059
given(delegate.getContext()).willReturn(context);
5160
strategy.setContext(strategy.getContext());
52-
verify(delegate).setContext(context);
61+
strategy.getContext();
62+
verifyNoInteractions(listener);
63+
}
64+
65+
@Test
66+
public void clearContextWhenNoGetContextThenContextIsNotRead() {
67+
SecurityContextHolderStrategy delegate = mock(SecurityContextHolderStrategy.class);
68+
SecurityContextChangedListener listener = mock(SecurityContextChangedListener.class);
69+
SecurityContextHolderStrategy strategy = new ListeningSecurityContextHolderStrategy(delegate, listener);
70+
Supplier<SecurityContext> context = mock(Supplier.class);
71+
ArgumentCaptor<SecurityContextChangedEvent> event = ArgumentCaptor.forClass(SecurityContextChangedEvent.class);
72+
given(delegate.getDeferredContext()).willReturn(context);
73+
given(delegate.getContext()).willAnswer((invocation) -> context.get());
74+
strategy.clearContext();
75+
verifyNoInteractions(context);
76+
verify(listener).securityContextChanged(event.capture());
77+
assertThat(event.getValue().isCleared()).isTrue();
78+
strategy.getContext();
79+
verify(context).get();
80+
strategy.clearContext();
81+
verifyNoMoreInteractions(context);
82+
}
83+
84+
@Test
85+
public void getContextWhenCalledMultipleTimesThenEventPublishedOnce() {
86+
SecurityContextHolderStrategy delegate = new MockSecurityContextHolderStrategy();
87+
SecurityContextChangedListener listener = mock(SecurityContextChangedListener.class);
88+
SecurityContextHolderStrategy strategy = new ListeningSecurityContextHolderStrategy(delegate, listener);
89+
strategy.setContext(new SecurityContextImpl());
90+
verifyNoInteractions(listener);
91+
strategy.getContext();
92+
verify(listener).securityContextChanged(any());
93+
strategy.getContext();
94+
verifyNoMoreInteractions(listener);
95+
}
96+
97+
@Test
98+
public void setContextWhenCalledMultipleTimesThenPublishedEventsAlign() {
99+
SecurityContextHolderStrategy delegate = new MockSecurityContextHolderStrategy();
100+
SecurityContextChangedListener listener = mock(SecurityContextChangedListener.class);
101+
SecurityContextHolderStrategy strategy = new ListeningSecurityContextHolderStrategy(delegate, listener);
102+
SecurityContext one = new SecurityContextImpl(new TestingAuthenticationToken("user", "pass"));
103+
SecurityContext two = new SecurityContextImpl(new TestingAuthenticationToken("admin", "pass"));
104+
ArgumentCaptor<SecurityContextChangedEvent> event = ArgumentCaptor.forClass(SecurityContextChangedEvent.class);
105+
strategy.setContext(one);
106+
strategy.setContext(two);
53107
verifyNoInteractions(listener);
108+
strategy.getContext();
109+
verify(listener).securityContextChanged(event.capture());
110+
assertThat(event.getValue().getOldContext()).isEqualTo(one);
111+
assertThat(event.getValue().getNewContext()).isEqualTo(two);
112+
strategy.getContext();
113+
verifyNoMoreInteractions(listener);
114+
strategy.setContext(one);
115+
verifyNoMoreInteractions(listener);
116+
reset(listener);
117+
strategy.getContext();
118+
verify(listener).securityContextChanged(event.capture());
119+
assertThat(event.getValue().getOldContext()).isEqualTo(two);
120+
assertThat(event.getValue().getNewContext()).isEqualTo(one);
54121
}
55122

56123
@Test

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

+15-3
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,35 @@
1616

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

19+
import java.util.function.Supplier;
20+
1921
public class MockSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
2022

21-
private SecurityContext context;
23+
private Supplier<SecurityContext> context = () -> null;
2224

2325
@Override
2426
public void clearContext() {
25-
this.context = null;
27+
this.context = () -> null;
2628
}
2729

2830
@Override
2931
public SecurityContext getContext() {
32+
return this.context.get();
33+
}
34+
35+
@Override
36+
public Supplier<SecurityContext> getDeferredContext() {
3037
return this.context;
3138
}
3239

3340
@Override
3441
public void setContext(SecurityContext context) {
35-
this.context = context;
42+
this.context = () -> context;
43+
}
44+
45+
@Override
46+
public void setDeferredContext(Supplier<SecurityContext> deferredContext) {
47+
this.context = deferredContext;
3648
}
3749

3850
@Override

0 commit comments

Comments
 (0)