1
0

Split into per-article modules and add the method-security module

Moves the existing virtual-thread/context-propagation project into
context-propagation/ and adds method-security/ for the Spring Security 7
method-security article: nine runnable demos, fourteen assertions, and every
transcript the article quotes, regenerated by scripts/run-all.sh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSrsDSRKVsY588yFiMJMo9
This commit is contained in:
2026-08-25 02:01:29 +00:00
parent 9f950bffa9
commit 5e9e7f1b12
65 changed files with 4088 additions and 119 deletions

View File

@@ -0,0 +1,67 @@
# 2. @Async, DelegatingSecurityContextExecutor, and virtual threads on Boot 4.1
[&larr; Prev: InheritableThreadLocal](01-inheritable-threadlocal.md) | [Next: Structured concurrency &rarr;](03-structured-concurrency.md)
`Demo2AsyncVirtualThreads.java` reproduces the exact executor bean Spring Boot 4.1 wires up
when you set `spring.threads.virtual.enabled=true`: a `SimpleAsyncTaskExecutor` with
`setVirtualThreads(true)`. That bean backs `@Async`, MVC async request handling, and WebFlux's
blocking-execution support. It is not a `ThreadPoolTaskExecutor` and never has a fixed pool of
workers to reuse -- see [Chapter 1](01-inheritable-threadlocal.md) for why that matters.
Four scenarios, same question each time: does the async task see the `Authentication` that was
active on the calling thread? Full output in [`docs/output/demo2.txt`](output/demo2.txt).
## A) Default mode, unwrapped executor -- loses it
`SecurityContextHolder`'s default strategy, `MODE_THREADLOCAL`, does not travel to any new
thread, virtual or not. This is the exact symptom reported against Spring Security as
[gh-15040](https://github.com/spring-projects/spring-security/issues/15040): swap in a raw
virtual-thread executor and `@Async` methods start throwing `AccessDeniedException` because
`SecurityContextHolder.getContext().getAuthentication()` is `null`.
## B) MODE_INHERITABLETHREADLOCAL, unwrapped executor -- works
This is the finding from Chapter 1 applied to Spring Security directly. Because the virtual
thread the executor spins up is fresh every time, `MODE_INHERITABLETHREADLOCAL` propagates the
context correctly with **zero extra wrapping code**. The reference docs' warning against this
mode predates virtual threads and is about pooled platform threads specifically -- it does not
apply to this executor shape. This is still a global JVM-wide setting, so weigh that against the
next two options, which are scoped to one executor bean.
## C) DelegatingSecurityContextExecutor -- still works, unconditionally
`DelegatingSecurityContextExecutor` doesn't rely on thread-local inheritance at all -- it wraps
the submitted `Runnable`, and the wrapper explicitly calls
`SecurityContextHolder.setContext(...)` / `clearContext()` around the delegate's `run()`,
wherever that `run()` happens to execute. That is why it has worked, unchanged, since long
before virtual threads existed, and why it is still the correct choice for library code that
cannot assume the application has set `MODE_INHERITABLETHREADLOCAL` globally.
## D) ContextPropagatingTaskDecorator -- the mechanism that's actually new
This is the one that did not exist when the [original version of this
post](https://ankurm.com/spring-security-context-propagation-complete-guide/) went up.
Spring Security 6.5 (GA 2025-05-19) added `SecurityContextHolderThreadLocalAccessor`, which
self-registers with Micrometer's `ContextRegistry` via `ServiceLoader` the moment
`io.micrometer:context-propagation` is on the classpath -- no bean, no configuration. Spring
Framework's `ContextPropagatingTaskDecorator` (since 6.1) uses that registry to snapshot and
restore every registered `ThreadLocalAccessor` around a task. Set it as the executor's task
decorator and `@Async` methods get the `SecurityContext` back **without any
`DelegatingSecurityContext*` wrapper at all** -- and the same decorator simultaneously restores
MDC and tracing context, which the `Delegating*` classes never touched.
```java
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
executor.setVirtualThreads(true);
executor.setTaskDecorator(new ContextPropagatingTaskDecorator());
```
Spring Security's own [Concurrency Support
page](https://docs.spring.io/spring-security/reference/features/integrations/concurrency.html)
still only documents the `Delegating*` family as of 7.1.1 -- this pattern is real and shipped,
just not yet reflected in that page.
`io.micrometer:context-propagation` is already on the classpath of any Boot 4.1 app that pulls
in `micrometer-observation` (actuator, tracing, or `spring-boot-starter-micrometer-*`). If your
app doesn't have a Micrometer dependency anywhere, add
`io.micrometer:context-propagation:1.2.1` (the version Boot 4.1.1's BOM manages) explicitly.