From 1a167991bf000b2682838cbf29080e7151ac840a Mon Sep 17 00:00:00 2001 From: Steve Riesenberg <5248162+sjohnr@users.noreply.github.com> Date: Wed, 26 Feb 2025 16:09:02 -0600 Subject: [PATCH] Add support for automatic context-propagation with Micrometer Closes gh-16665 --- core/spring-security-core.gradle | 1 + ...urityContextHolderThreadLocalAccessor.java | 65 +++++++++++++++++++ ...urityContextHolderThreadLocalAccessor.java | 60 +++++++++++++++++ .../io.micrometer.context.ThreadLocalAccessor | 2 + .../spring-security-dependencies.gradle | 1 + gradle/libs.versions.toml | 1 + web/spring-security-web.gradle | 1 + .../ServerWebExchangeThreadLocalAccessor.java | 63 ++++++++++++++++++ .../io.micrometer.context.ThreadLocalAccessor | 1 + 9 files changed, 195 insertions(+) create mode 100644 core/src/main/java/org/springframework/security/core/context/ReactiveSecurityContextHolderThreadLocalAccessor.java create mode 100644 core/src/main/java/org/springframework/security/core/context/SecurityContextHolderThreadLocalAccessor.java create mode 100644 core/src/main/resources/META-INF/spring/io.micrometer.context.ThreadLocalAccessor create mode 100644 web/src/main/java/org/springframework/security/web/server/ServerWebExchangeThreadLocalAccessor.java create mode 100644 web/src/main/resources/META-INF/spring/io.micrometer.context.ThreadLocalAccessor diff --git a/core/spring-security-core.gradle b/core/spring-security-core.gradle index 7a326ed5918..3eb2343e3e3 100644 --- a/core/spring-security-core.gradle +++ b/core/spring-security-core.gradle @@ -16,6 +16,7 @@ dependencies { api 'io.micrometer:micrometer-observation' optional 'com.fasterxml.jackson.core:jackson-databind' + optional 'io.micrometer:context-propagation' optional 'io.projectreactor:reactor-core' optional 'jakarta.annotation:jakarta.annotation-api' optional 'org.aspectj:aspectjrt' diff --git a/core/src/main/java/org/springframework/security/core/context/ReactiveSecurityContextHolderThreadLocalAccessor.java b/core/src/main/java/org/springframework/security/core/context/ReactiveSecurityContextHolderThreadLocalAccessor.java new file mode 100644 index 00000000000..6b7953f95b6 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/context/ReactiveSecurityContextHolderThreadLocalAccessor.java @@ -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}. + *

+ * 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> { + + private static final ThreadLocal> threadLocal = new ThreadLocal<>(); + + @Override + public Object key() { + return SecurityContext.class; + } + + @Override + public Mono getValue() { + return threadLocal.get(); + } + + @Override + public void setValue(Mono securityContext) { + Assert.notNull(securityContext, "securityContext cannot be null"); + threadLocal.set(securityContext); + } + + @Override + public void setValue() { + threadLocal.remove(); + } + +} diff --git a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderThreadLocalAccessor.java b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderThreadLocalAccessor.java new file mode 100644 index 00000000000..dbe17518666 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderThreadLocalAccessor.java @@ -0,0 +1,60 @@ +/* + * 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; + +/** + * A {@link ThreadLocalAccessor} for accessing a {@link SecurityContext} with the + * {@link SecurityContextHolder}. + *

+ * 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 { + + @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 value) { + SecurityContextHolder.setContext(value); + } + + @Override + public void setValue() { + SecurityContextHolder.clearContext(); + } + +} diff --git a/core/src/main/resources/META-INF/spring/io.micrometer.context.ThreadLocalAccessor b/core/src/main/resources/META-INF/spring/io.micrometer.context.ThreadLocalAccessor new file mode 100644 index 00000000000..65b406e6ae0 --- /dev/null +++ b/core/src/main/resources/META-INF/spring/io.micrometer.context.ThreadLocalAccessor @@ -0,0 +1,2 @@ +org.springframework.security.core.context.ReactiveSecurityContextHolderThreadLocalAccessor +org.springframework.security.core.context.SecurityContextHolderThreadLocalAccessor diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 7d69e3ec139..2a2d4332529 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -35,6 +35,7 @@ dependencies { api libs.com.unboundid.unboundid.ldapsdk api libs.commons.collections api libs.io.mockk + api libs.io.micrometer.context.propagation api libs.io.micrometer.micrometer.observation api libs.jakarta.annotation.jakarta.annotation.api api libs.jakarta.inject.jakarta.inject.api diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92b427d1faa..a14c5c7a7e2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,6 +28,7 @@ com-squareup-okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version. com-unboundid-unboundid-ldapsdk = "com.unboundid:unboundid-ldapsdk:6.0.11" com-unboundid-unboundid-ldapsdk7 = "com.unboundid:unboundid-ldapsdk:7.0.1" commons-collections = "commons-collections:commons-collections:3.2.2" +io-micrometer-context-propagation = "io.micrometer:context-propagation:1.1.2" io-micrometer-micrometer-observation = "io.micrometer:micrometer-observation:1.14.4" io-mockk = "io.mockk:mockk:1.13.16" io-projectreactor-reactor-bom = "io.projectreactor:reactor-bom:2023.0.15" diff --git a/web/spring-security-web.gradle b/web/spring-security-web.gradle index a709c95a991..1a87fac63e9 100644 --- a/web/spring-security-web.gradle +++ b/web/spring-security-web.gradle @@ -36,6 +36,7 @@ dependencies { api 'org.springframework:spring-web' optional 'com.fasterxml.jackson.core:jackson-databind' + optional 'io.micrometer:context-propagation' optional 'io.projectreactor:reactor-core' optional 'org.springframework:spring-jdbc' optional 'org.springframework:spring-tx' diff --git a/web/src/main/java/org/springframework/security/web/server/ServerWebExchangeThreadLocalAccessor.java b/web/src/main/java/org/springframework/security/web/server/ServerWebExchangeThreadLocalAccessor.java new file mode 100644 index 00000000000..efc45a201ad --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/ServerWebExchangeThreadLocalAccessor.java @@ -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}. + *

+ * 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 { + + private static final ThreadLocal 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 must not be null"); + threadLocal.set(exchange); + } + + @Override + public void setValue() { + threadLocal.remove(); + } + +} diff --git a/web/src/main/resources/META-INF/spring/io.micrometer.context.ThreadLocalAccessor b/web/src/main/resources/META-INF/spring/io.micrometer.context.ThreadLocalAccessor new file mode 100644 index 00000000000..63959839f8b --- /dev/null +++ b/web/src/main/resources/META-INF/spring/io.micrometer.context.ThreadLocalAccessor @@ -0,0 +1 @@ +org.springframework.security.web.server.ServerWebExchangeThreadLocalAccessor