From 3181e796956e75154a8d9dd7040bc11855c10612 Mon Sep 17 00:00:00 2001 From: Yusuf Hussien Date: Tue, 20 Oct 2026 19:22:46 +0300 Subject: [PATCH 1/2] Document using Spring Security without a servlet request Add a dedicated entry point under Features > Integrations that explains how to apply Spring Security in non-web contexts such as scheduled jobs, CLI runners, message listeners, and plain unit tests: populating and clearing the SecurityContextHolder, propagating the context to other threads, securing methods with @PreAuthorize, and testing without MockMvc. Cross-reference the existing concurrency, method security, and testing sections instead of duplicating them. Closes gh-11910 Signed-off-by: Yusuf Hussien --- docs/modules/ROOT/nav.adoc | 1 + .../integrations/outside-servlet.adoc | 190 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 docs/modules/ROOT/pages/features/integrations/outside-servlet.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index e1cf5e9e141..62e1b8b7a20 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -21,6 +21,7 @@ *** xref:features/exploits/headers.adoc[HTTP Headers] *** xref:features/exploits/http.adoc[HTTP Requests] ** xref:features/integrations/index.adoc[Integrations] +*** xref:features/integrations/outside-servlet.adoc[Outside of a Servlet Request] *** REST Client **** xref:features/integrations/rest/http-service-client.adoc[HTTP Service Clients] *** xref:features/integrations/cryptography.adoc[Cryptography] diff --git a/docs/modules/ROOT/pages/features/integrations/outside-servlet.adoc b/docs/modules/ROOT/pages/features/integrations/outside-servlet.adoc new file mode 100644 index 00000000000..0e41cebfbc9 --- /dev/null +++ b/docs/modules/ROOT/pages/features/integrations/outside-servlet.adoc @@ -0,0 +1,190 @@ +[[outside-servlet]] += Spring Security Outside of a Servlet Request + +Much of the reference documentation assumes that Spring Security is protecting an HTTP request handled by a Servlet or Reactive application. +However, a significant amount of code runs without any request at all, such as: + +* scheduled jobs (`@Scheduled`, Quartz) +* command-line runners (`CommandLineRunner`, `ApplicationRunner`) +* message listeners (JMS, Kafka, RabbitMQ, Spring Integration) +* background threads and `Executor` tasks +* plain unit tests that exercise secured services directly + +This page describes the pieces that are useful in those contexts. +If you do have an HTTP request, start instead from the xref:servlet/index.adoc[Servlet] or xref:reactive/index.adoc[Reactive] sections. + +[[outside-servlet-securitycontext]] +== The `SecurityContextHolder` and Threads + +The central abstraction is the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`], which stores the current `SecurityContext`. +By default it uses a `ThreadLocal`, so it works the same way with or without an HTTP request: whatever `Thread` you are running on, `SecurityContextHolder.getContext()` returns the `SecurityContext` associated with that thread. + +This has two consequences that matter outside of a web request: + +1. You can populate the context yourself before invoking secured code. +2. When you hand work to another thread, the context is lost unless you propagate it explicitly. + +You can set and clear the context manually: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +SecurityContext context = SecurityContextHolder.createEmptyContext(); +context.setAuthentication(authentication); +SecurityContextHolder.setContext(context); +try { + // invoke secured code +} finally { + SecurityContextHolder.clearContext(); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +val context = SecurityContextHolder.createEmptyContext() +context.authentication = authentication +SecurityContextHolder.setContext(context) +try { + // invoke secured code +} finally { + SecurityContextHolder.clearContext() +} +---- +====== + +[[outside-servlet-concurrency]] +== Propagating the Context to Other Threads + +Because the context is thread-bound, work submitted to another thread (for example, a `ThreadPoolTaskExecutor`) does not see the caller's `SecurityContext`. +Spring Security provides delegating wrappers so that you do not have to manage this by hand. +For example, `DelegatingSecurityContextRunnable` and `DelegatingSecurityContextExecutor` capture the current context and restore it around the delegate: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +Runnable task = new DelegatingSecurityContextRunnable(originalRunnable); +Executor executor = new DelegatingSecurityContextExecutor(delegateExecutor); + +executor.execute(task); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +val task = DelegatingSecurityContextRunnable(originalRunnable) +val executor: Executor = DelegatingSecurityContextExecutor(delegateExecutor) + +executor.execute(task) +---- +====== + +See xref:features/integrations/concurrency.adoc[Concurrency Support] for the full set of wrappers. + +[[outside-servlet-method-security]] +== Method Security + +xref:servlet/authorization/method-security.adoc[Method security] does not require an HTTP request. +Once it is enabled, annotations such as `@PreAuthorize`, `@PostAuthorize` and `@PreFilter` are evaluated against the `Authentication` found in the `SecurityContextHolder`, regardless of what triggered the invocation: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@EnableMethodSecurity +public class MethodSecurityConfig { +} + +@Service +public class MessageService { + + @PreAuthorize("hasRole('ADMIN')") + public void process(String message) { + // ... + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@EnableMethodSecurity +class MethodSecurityConfig + +@Service +class MessageService { + + @PreAuthorize("hasRole('ADMIN')") + fun process(message: String) { + // ... + } +} +---- +====== + +This is often the most convenient way to secure non-web code: the security decision travels with the method, so a scheduled job or message listener is protected without any request-scoped machinery. +When you invoke such a method from a background thread, remember to propagate the context as described in the previous section if the `Authentication` is not already available on that thread. + +[[outside-servlet-testing]] +== Testing Without a Web Request + +You can test secured services without starting `MockMvc` or an HTTP server. +Spring Security integrates with Spring Test through `@WithMockUser` and `@WithSecurityContext`, which set up a `SecurityContext` for the duration of a test: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@SpringBootTest +class MessageServiceTests { + + @Autowired + MessageService messageService; + + @Test + @WithMockUser(roles = "ADMIN") + void adminCanProcess() { + messageService.process("hello"); + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@SpringBootTest +class MessageServiceTests { + + @Autowired + lateinit var messageService: MessageService + + @Test + @WithMockUser(roles = ["ADMIN"]) + fun adminCanProcess() { + messageService.process("hello") + } +} +---- +====== + +See xref:servlet/test/method.adoc#test-method[Testing Method Security] for `@WithMockUser`, `@WithUserDetails`, and using xref:servlet/test/method.adoc#test-method-withsecuritycontext[`@WithSecurityContext`] with a custom `Authentication`. + +[[outside-servlet-reactive]] +== Reactive Environments + +If you are on a reactive stack, `SecurityContextHolder` is not the idiomatic mechanism; Spring Security instead carries the context through Reactor's `Context`. +Refer to xref:reactive/index.adoc[Reactive Applications] for details. From f52ecc6042810e8c1de4954919a902052421b537 Mon Sep 17 00:00:00 2001 From: Yusuf Hussien Date: Tue, 20 Oct 2026 20:24:44 +0300 Subject: [PATCH 2/2] Apply review feedback to outside-request page - Rename the page to "Outside of a Request" and the anchors to outside-request-*, since it also covers reactive environments - Add @Configuration to the method security samples so @EnableMethodSecurity actually takes effect - Use a lightweight Spring Test slice instead of @SpringBootTest - Link directly to ReactiveSecurityContextHolder - Move the nav entry below the existing integration entries Closes gh-11910 Signed-off-by: Yusuf Hussien --- docs/modules/ROOT/nav.adoc | 2 +- ...side-servlet.adoc => outside-request.adoc} | 27 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) rename docs/modules/ROOT/pages/features/integrations/{outside-servlet.adoc => outside-request.adoc} (88%) diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 62e1b8b7a20..50061989bc4 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -21,7 +21,6 @@ *** xref:features/exploits/headers.adoc[HTTP Headers] *** xref:features/exploits/http.adoc[HTTP Requests] ** xref:features/integrations/index.adoc[Integrations] -*** xref:features/integrations/outside-servlet.adoc[Outside of a Servlet Request] *** REST Client **** xref:features/integrations/rest/http-service-client.adoc[HTTP Service Clients] *** xref:features/integrations/cryptography.adoc[Cryptography] @@ -29,6 +28,7 @@ *** xref:features/integrations/concurrency.adoc[Java's Concurrency APIs] *** xref:features/integrations/jackson.adoc[Jackson] *** xref:features/integrations/localization.adoc[Localization] +*** xref:features/integrations/outside-request.adoc[Outside of a Request] * xref:modules.adoc[Project Modules] * xref:samples.adoc[Samples] * xref:servlet/index.adoc[Servlet Applications] diff --git a/docs/modules/ROOT/pages/features/integrations/outside-servlet.adoc b/docs/modules/ROOT/pages/features/integrations/outside-request.adoc similarity index 88% rename from docs/modules/ROOT/pages/features/integrations/outside-servlet.adoc rename to docs/modules/ROOT/pages/features/integrations/outside-request.adoc index 0e41cebfbc9..df5436f1b21 100644 --- a/docs/modules/ROOT/pages/features/integrations/outside-servlet.adoc +++ b/docs/modules/ROOT/pages/features/integrations/outside-request.adoc @@ -1,5 +1,5 @@ -[[outside-servlet]] -= Spring Security Outside of a Servlet Request +[[outside-request]] += Spring Security Outside of a Request Much of the reference documentation assumes that Spring Security is protecting an HTTP request handled by a Servlet or Reactive application. However, a significant amount of code runs without any request at all, such as: @@ -13,7 +13,7 @@ However, a significant amount of code runs without any request at all, such as: This page describes the pieces that are useful in those contexts. If you do have an HTTP request, start instead from the xref:servlet/index.adoc[Servlet] or xref:reactive/index.adoc[Reactive] sections. -[[outside-servlet-securitycontext]] +[[outside-request-securitycontext]] == The `SecurityContextHolder` and Threads The central abstraction is the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`], which stores the current `SecurityContext`. @@ -57,7 +57,7 @@ try { ---- ====== -[[outside-servlet-concurrency]] +[[outside-request-concurrency]] == Propagating the Context to Other Threads Because the context is thread-bound, work submitted to another thread (for example, a `ThreadPoolTaskExecutor`) does not see the caller's `SecurityContext`. @@ -89,7 +89,7 @@ executor.execute(task) See xref:features/integrations/concurrency.adoc[Concurrency Support] for the full set of wrappers. -[[outside-servlet-method-security]] +[[outside-request-method-security]] == Method Security xref:servlet/authorization/method-security.adoc[Method security] does not require an HTTP request. @@ -101,6 +101,7 @@ Java:: + [source,java,role="primary"] ---- +@Configuration @EnableMethodSecurity public class MethodSecurityConfig { } @@ -119,6 +120,7 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- +@Configuration @EnableMethodSecurity class MethodSecurityConfig @@ -136,11 +138,12 @@ class MessageService { This is often the most convenient way to secure non-web code: the security decision travels with the method, so a scheduled job or message listener is protected without any request-scoped machinery. When you invoke such a method from a background thread, remember to propagate the context as described in the previous section if the `Authentication` is not already available on that thread. -[[outside-servlet-testing]] +[[outside-request-testing]] == Testing Without a Web Request You can test secured services without starting `MockMvc` or an HTTP server. -Spring Security integrates with Spring Test through `@WithMockUser` and `@WithSecurityContext`, which set up a `SecurityContext` for the duration of a test: +Spring Security integrates with Spring Test through `@WithMockUser` and `@WithSecurityContext`, which set up a `SecurityContext` for the duration of a test. +These annotations work with any Spring Test context, so you can use a lightweight slice instead of booting the whole application: [tabs] ====== @@ -148,7 +151,8 @@ Java:: + [source,java,role="primary"] ---- -@SpringBootTest +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = MethodSecurityConfig.class) class MessageServiceTests { @Autowired @@ -166,7 +170,8 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -@SpringBootTest +@ExtendWith(SpringExtension::class) +@ContextConfiguration(classes = [MethodSecurityConfig::class]) class MessageServiceTests { @Autowired @@ -183,8 +188,8 @@ class MessageServiceTests { See xref:servlet/test/method.adoc#test-method[Testing Method Security] for `@WithMockUser`, `@WithUserDetails`, and using xref:servlet/test/method.adoc#test-method-withsecuritycontext[`@WithSecurityContext`] with a custom `Authentication`. -[[outside-servlet-reactive]] +[[outside-request-reactive]] == Reactive Environments -If you are on a reactive stack, `SecurityContextHolder` is not the idiomatic mechanism; Spring Security instead carries the context through Reactor's `Context`. +If you are on a reactive stack, `SecurityContextHolder` is not the idiomatic mechanism; Spring Security instead carries the context through Reactor's `Context` via xref:reactive/authorization/method.adoc[`ReactiveSecurityContextHolder`]. Refer to xref:reactive/index.adoc[Reactive Applications] for details.