15 changed files with 579 additions and 0 deletions
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
/* |
||||
* Copyright 2002-2025 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 io.micrometer.context.ThreadLocalAccessor; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* A {@link ThreadLocalAccessor} for accessing a {@link SecurityContext} with the |
||||
* {@link ReactiveSecurityContextHolder}. |
||||
* <p> |
||||
* This class adapts the {@link ReactiveSecurityContextHolder} to the |
||||
* {@link ThreadLocalAccessor} contract to allow Micrometer Context Propagation to |
||||
* automatically propagate a {@link SecurityContext} in Reactive applications. It is |
||||
* automatically registered with the {@link io.micrometer.context.ContextRegistry} through |
||||
* the {@link java.util.ServiceLoader} mechanism when context-propagation is on the |
||||
* classpath. |
||||
* |
||||
* @author Steve Riesenberg |
||||
* @since 6.5 |
||||
* @see io.micrometer.context.ContextRegistry |
||||
*/ |
||||
public final class ReactiveSecurityContextHolderThreadLocalAccessor |
||||
implements ThreadLocalAccessor<Mono<SecurityContext>> { |
||||
|
||||
private static final ThreadLocal<Mono<SecurityContext>> threadLocal = new ThreadLocal<>(); |
||||
|
||||
@Override |
||||
public Object key() { |
||||
return SecurityContext.class; |
||||
} |
||||
|
||||
@Override |
||||
public Mono<SecurityContext> getValue() { |
||||
return threadLocal.get(); |
||||
} |
||||
|
||||
@Override |
||||
public void setValue(Mono<SecurityContext> securityContext) { |
||||
Assert.notNull(securityContext, "securityContext cannot be null"); |
||||
threadLocal.set(securityContext); |
||||
} |
||||
|
||||
@Override |
||||
public void setValue() { |
||||
threadLocal.remove(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
/* |
||||
* Copyright 2002-2025 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 io.micrometer.context.ThreadLocalAccessor; |
||||
|
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* A {@link ThreadLocalAccessor} for accessing a {@link SecurityContext} with the |
||||
* {@link SecurityContextHolder}. |
||||
* <p> |
||||
* This class adapts the {@link SecurityContextHolder} to the {@link ThreadLocalAccessor} |
||||
* contract to allow Micrometer Context Propagation to automatically propagate a |
||||
* {@link SecurityContext} in Servlet applications. It is automatically registered with |
||||
* the {@link io.micrometer.context.ContextRegistry} through the |
||||
* {@link java.util.ServiceLoader} mechanism when context-propagation is on the classpath. |
||||
* |
||||
* @author Steve Riesenberg |
||||
* @since 6.5 |
||||
* @see io.micrometer.context.ContextRegistry |
||||
*/ |
||||
public final class SecurityContextHolderThreadLocalAccessor implements ThreadLocalAccessor<SecurityContext> { |
||||
|
||||
@Override |
||||
public Object key() { |
||||
return SecurityContext.class.getName(); |
||||
} |
||||
|
||||
@Override |
||||
public SecurityContext getValue() { |
||||
SecurityContext securityContext = SecurityContextHolder.getContext(); |
||||
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext(); |
||||
|
||||
return !securityContext.equals(emptyContext) ? securityContext : null; |
||||
} |
||||
|
||||
@Override |
||||
public void setValue(SecurityContext securityContext) { |
||||
Assert.notNull(securityContext, "securityContext cannot be null"); |
||||
SecurityContextHolder.setContext(securityContext); |
||||
} |
||||
|
||||
@Override |
||||
public void setValue() { |
||||
SecurityContextHolder.clearContext(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
org.springframework.security.core.context.ReactiveSecurityContextHolderThreadLocalAccessor |
||||
org.springframework.security.core.context.SecurityContextHolderThreadLocalAccessor |
||||
@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
|
||||
/* |
||||
* Copyright 2002-2025 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.concurrent.CountDownLatch; |
||||
|
||||
import org.junit.jupiter.api.AfterEach; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor; |
||||
import org.springframework.security.authentication.TestingAuthenticationToken; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
|
||||
/** |
||||
* Tests for {@link ReactiveSecurityContextHolderThreadLocalAccessor}. |
||||
* |
||||
* @author Steve Riesenberg |
||||
*/ |
||||
public class ReactiveSecurityContextHolderThreadLocalAccessorTests { |
||||
|
||||
private ReactiveSecurityContextHolderThreadLocalAccessor threadLocalAccessor; |
||||
|
||||
@BeforeEach |
||||
public void setUp() { |
||||
this.threadLocalAccessor = new ReactiveSecurityContextHolderThreadLocalAccessor(); |
||||
} |
||||
|
||||
@AfterEach |
||||
public void tearDown() { |
||||
this.threadLocalAccessor.setValue(); |
||||
} |
||||
|
||||
@Test |
||||
public void keyAlwaysReturnsSecurityContextClass() { |
||||
assertThat(this.threadLocalAccessor.key()).isEqualTo(SecurityContext.class); |
||||
} |
||||
|
||||
@Test |
||||
public void getValueWhenThreadLocalNotSetThenReturnsNull() { |
||||
assertThat(this.threadLocalAccessor.getValue()).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
public void getValueWhenThreadLocalSetThenReturnsSecurityContextMono() { |
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); |
||||
securityContext.setAuthentication(new TestingAuthenticationToken("user", "password")); |
||||
Mono<SecurityContext> mono = Mono.just(securityContext); |
||||
this.threadLocalAccessor.setValue(mono); |
||||
|
||||
assertThat(this.threadLocalAccessor.getValue()).isSameAs(mono); |
||||
} |
||||
|
||||
@Test |
||||
public void getValueWhenThreadLocalSetOnAnotherThreadThenReturnsNull() throws InterruptedException { |
||||
CountDownLatch threadLocalSet = new CountDownLatch(1); |
||||
CountDownLatch threadLocalRead = new CountDownLatch(1); |
||||
CountDownLatch threadLocalCleared = new CountDownLatch(1); |
||||
|
||||
Runnable task = () -> { |
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); |
||||
securityContext.setAuthentication(new TestingAuthenticationToken("user", "password")); |
||||
Mono<SecurityContext> mono = Mono.just(securityContext); |
||||
this.threadLocalAccessor.setValue(mono); |
||||
threadLocalSet.countDown(); |
||||
try { |
||||
threadLocalRead.await(); |
||||
} |
||||
catch (InterruptedException ignored) { |
||||
} |
||||
finally { |
||||
this.threadLocalAccessor.setValue(); |
||||
threadLocalCleared.countDown(); |
||||
} |
||||
}; |
||||
try (SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor()) { |
||||
taskExecutor.execute(task); |
||||
threadLocalSet.await(); |
||||
assertThat(this.threadLocalAccessor.getValue()).isNull(); |
||||
threadLocalRead.countDown(); |
||||
threadLocalCleared.await(); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void setValueWhenNullThenThrowsIllegalArgumentException() { |
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException() |
||||
.isThrownBy(() -> this.threadLocalAccessor.setValue(null)) |
||||
.withMessage("securityContext cannot be null"); |
||||
// @formatter:on
|
||||
} |
||||
|
||||
@Test |
||||
public void setValueWhenThreadLocalSetThenClearsThreadLocal() { |
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); |
||||
securityContext.setAuthentication(new TestingAuthenticationToken("user", "password")); |
||||
Mono<SecurityContext> mono = Mono.just(securityContext); |
||||
this.threadLocalAccessor.setValue(mono); |
||||
assertThat(this.threadLocalAccessor.getValue()).isSameAs(mono); |
||||
|
||||
this.threadLocalAccessor.setValue(); |
||||
assertThat(this.threadLocalAccessor.getValue()).isNull(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
|
||||
/* |
||||
* Copyright 2002-2025 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.junit.jupiter.api.AfterEach; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
|
||||
/** |
||||
* Tests for {@link SecurityContextHolderThreadLocalAccessor}. |
||||
* |
||||
* @author Steve Riesenberg |
||||
*/ |
||||
public class SecurityContextHolderThreadLocalAccessorTests { |
||||
|
||||
private SecurityContextHolderThreadLocalAccessor threadLocalAccessor; |
||||
|
||||
@BeforeEach |
||||
public void setUp() { |
||||
this.threadLocalAccessor = new SecurityContextHolderThreadLocalAccessor(); |
||||
} |
||||
|
||||
@AfterEach |
||||
public void tearDown() { |
||||
this.threadLocalAccessor.setValue(); |
||||
} |
||||
|
||||
@Test |
||||
public void keyAlwaysReturnsSecurityContextClassName() { |
||||
assertThat(this.threadLocalAccessor.key()).isEqualTo(SecurityContext.class.getName()); |
||||
} |
||||
|
||||
@Test |
||||
public void getValueWhenSecurityContextHolderNotSetThenReturnsNull() { |
||||
assertThat(this.threadLocalAccessor.getValue()).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
public void getValueWhenSecurityContextHolderSetThenReturnsSecurityContext() { |
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); |
||||
securityContext.setAuthentication(new TestingAuthenticationToken("user", "password")); |
||||
SecurityContextHolder.setContext(securityContext); |
||||
assertThat(this.threadLocalAccessor.getValue()).isSameAs(securityContext); |
||||
} |
||||
|
||||
@Test |
||||
public void setValueWhenSecurityContextThenSetsSecurityContextHolder() { |
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); |
||||
securityContext.setAuthentication(new TestingAuthenticationToken("user", "password")); |
||||
this.threadLocalAccessor.setValue(securityContext); |
||||
assertThat(SecurityContextHolder.getContext()).isSameAs(securityContext); |
||||
} |
||||
|
||||
@Test |
||||
public void setValueWhenNullThenThrowsIllegalArgumentException() { |
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException() |
||||
.isThrownBy(() -> this.threadLocalAccessor.setValue(null)) |
||||
.withMessage("securityContext cannot be null"); |
||||
// @formatter:on
|
||||
} |
||||
|
||||
@Test |
||||
public void setValueWhenSecurityContextSetThenClearsSecurityContextHolder() { |
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); |
||||
securityContext.setAuthentication(new TestingAuthenticationToken("user", "password")); |
||||
SecurityContextHolder.setContext(securityContext); |
||||
this.threadLocalAccessor.setValue(); |
||||
|
||||
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext(); |
||||
assertThat(SecurityContextHolder.getContext()).isEqualTo(emptyContext); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
/* |
||||
* Copyright 2002-2025 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.web.server; |
||||
|
||||
import io.micrometer.context.ThreadLocalAccessor; |
||||
|
||||
import org.springframework.util.Assert; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
/** |
||||
* A {@link ThreadLocalAccessor} for accessing a {@link ServerWebExchange}. |
||||
* <p> |
||||
* This class adapts the existing Reactor Context attribute |
||||
* {@code ServerWebExchange.class} to the {@link ThreadLocalAccessor} contract to allow |
||||
* Micrometer Context Propagation to automatically propagate a {@link ServerWebExchange} |
||||
* in Reactive applications. It is automatically registered with the |
||||
* {@link io.micrometer.context.ContextRegistry} through the |
||||
* {@link java.util.ServiceLoader} mechanism when context-propagation is on the classpath. |
||||
* |
||||
* @author Steve Riesenberg |
||||
* @since 6.5 |
||||
* @see io.micrometer.context.ContextRegistry |
||||
*/ |
||||
public final class ServerWebExchangeThreadLocalAccessor implements ThreadLocalAccessor<ServerWebExchange> { |
||||
|
||||
private static final ThreadLocal<ServerWebExchange> threadLocal = new ThreadLocal<>(); |
||||
|
||||
@Override |
||||
public Object key() { |
||||
return ServerWebExchange.class; |
||||
} |
||||
|
||||
@Override |
||||
public ServerWebExchange getValue() { |
||||
return threadLocal.get(); |
||||
} |
||||
|
||||
@Override |
||||
public void setValue(ServerWebExchange exchange) { |
||||
Assert.notNull(exchange, "exchange cannot be null"); |
||||
threadLocal.set(exchange); |
||||
} |
||||
|
||||
@Override |
||||
public void setValue() { |
||||
threadLocal.remove(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
org.springframework.security.web.server.ServerWebExchangeThreadLocalAccessor |
||||
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
/* |
||||
* Copyright 2002-2025 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.web; |
||||
|
||||
import java.util.concurrent.CountDownLatch; |
||||
|
||||
import org.junit.jupiter.api.AfterEach; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor; |
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest; |
||||
import org.springframework.mock.web.server.MockServerWebExchange; |
||||
import org.springframework.security.web.server.ServerWebExchangeThreadLocalAccessor; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
|
||||
/** |
||||
* Tests for {@link ServerWebExchangeThreadLocalAccessor}. |
||||
* |
||||
* @author Steve Riesenberg |
||||
*/ |
||||
public class ServerWebExchangeThreadLocalAccessorTests { |
||||
|
||||
private ServerWebExchangeThreadLocalAccessor threadLocalAccessor; |
||||
|
||||
private ServerWebExchange exchange; |
||||
|
||||
@BeforeEach |
||||
public void setUp() { |
||||
this.threadLocalAccessor = new ServerWebExchangeThreadLocalAccessor(); |
||||
this.exchange = MockServerWebExchange.builder(MockServerHttpRequest.get("/")).build(); |
||||
} |
||||
|
||||
@AfterEach |
||||
public void tearDown() { |
||||
this.threadLocalAccessor.setValue(); |
||||
} |
||||
|
||||
@Test |
||||
public void keyAlwaysReturnsServerWebExchangeClass() { |
||||
assertThat(this.threadLocalAccessor.key()).isEqualTo(ServerWebExchange.class); |
||||
} |
||||
|
||||
@Test |
||||
public void getValueWhenThreadLocalNotSetThenReturnsNull() { |
||||
assertThat(this.threadLocalAccessor.getValue()).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
public void getValueWhenThreadLocalSetThenReturnsServerWebExchange() { |
||||
this.threadLocalAccessor.setValue(this.exchange); |
||||
assertThat(this.threadLocalAccessor.getValue()).isSameAs(this.exchange); |
||||
} |
||||
|
||||
@Test |
||||
public void getValueWhenThreadLocalSetOnAnotherThreadThenReturnsNull() throws InterruptedException { |
||||
CountDownLatch threadLocalSet = new CountDownLatch(1); |
||||
CountDownLatch threadLocalRead = new CountDownLatch(1); |
||||
CountDownLatch threadLocalCleared = new CountDownLatch(1); |
||||
|
||||
Runnable task = () -> { |
||||
this.threadLocalAccessor.setValue(this.exchange); |
||||
threadLocalSet.countDown(); |
||||
try { |
||||
threadLocalRead.await(); |
||||
} |
||||
catch (InterruptedException ignored) { |
||||
} |
||||
finally { |
||||
this.threadLocalAccessor.setValue(); |
||||
threadLocalCleared.countDown(); |
||||
} |
||||
}; |
||||
try (SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor()) { |
||||
taskExecutor.execute(task); |
||||
threadLocalSet.await(); |
||||
assertThat(this.threadLocalAccessor.getValue()).isNull(); |
||||
threadLocalRead.countDown(); |
||||
threadLocalCleared.await(); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void setValueWhenNullThenThrowsIllegalArgumentException() { |
||||
// @formatter:off
|
||||
assertThatIllegalArgumentException() |
||||
.isThrownBy(() -> this.threadLocalAccessor.setValue(null)) |
||||
.withMessage("exchange cannot be null"); |
||||
// @formatter:on
|
||||
} |
||||
|
||||
@Test |
||||
public void setValueWhenThreadLocalSetThenClearsThreadLocal() { |
||||
this.threadLocalAccessor.setValue(this.exchange); |
||||
assertThat(this.threadLocalAccessor.getValue()).isSameAs(this.exchange); |
||||
|
||||
this.threadLocalAccessor.setValue(); |
||||
assertThat(this.threadLocalAccessor.getValue()).isNull(); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue