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:
61
context-propagation/docs/03-structured-concurrency.md
Normal file
61
context-propagation/docs/03-structured-concurrency.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# 3. StructuredTaskScope and SecurityContext
|
||||
|
||||
[← Prev: Async + virtual threads](02-async-virtual-threads.md) | [Next: Executor/ExecutorService wrapping →](04-executor-wrapping.md)
|
||||
|
||||
`Demo3StructuredConcurrency.java` asks the Chapter 2 question again, but for
|
||||
`StructuredTaskScope` (JEP 505, fifth preview in JDK 25 -- still preview through the JDK 26
|
||||
sixth preview per JEP 525, so every example here needs `--enable-preview`). A `fork()` call
|
||||
starts a brand new virtual thread for the subtask, same as the executors in Chapter 2, so the
|
||||
Chapter 1 finding applies here too. Full output in
|
||||
[`docs/output/demo3.txt`](output/demo3.txt).
|
||||
|
||||
## What the JEP actually promises
|
||||
|
||||
JEP 525's text is explicit about one kind of context and silent about another:
|
||||
|
||||
> Subtasks forked in a scope inherit `ScopedValue` bindings.
|
||||
|
||||
That is a real, specified guarantee -- and it says nothing about `ThreadLocal`. Spring
|
||||
Security's `SecurityContextHolder` is a `ThreadLocal`/`InheritableThreadLocal`, not a
|
||||
`ScopedValue`. Nothing in the structured concurrency API changes that, and scenario A below
|
||||
proves it: a plain `scope.fork(...)` with the default `MODE_THREADLOCAL` strategy loses the
|
||||
`Authentication` exactly like the unwrapped executor in Chapter 2 did.
|
||||
|
||||
## Four scenarios
|
||||
|
||||
- **A) Plain fork, MODE_THREADLOCAL** -- lost. The default `SecurityContextHolder` strategy
|
||||
isn't inherited by anything, structured concurrency included.
|
||||
- **B) Plain fork, MODE_INHERITABLETHREADLOCAL** -- propagates. Same reasoning as Chapter 2,
|
||||
scenario B: `fork()`'s subtask thread is a fresh virtual thread, so inheritance at
|
||||
construction time works and there is no pooled-thread staleness risk.
|
||||
- **C) Manual capture-and-restore around the forked `Callable`** -- propagates, and does not
|
||||
depend on the global strategy mode at all:
|
||||
|
||||
```java
|
||||
SecurityContext captured = SecurityContextHolder.getContext();
|
||||
Callable<String> task = () -> {
|
||||
SecurityContextHolder.setContext(captured);
|
||||
try { return doWork(); }
|
||||
finally { SecurityContextHolder.clearContext(); }
|
||||
};
|
||||
scope.fork(task);
|
||||
```
|
||||
|
||||
This is the safest pattern for a `StructuredTaskScope` used inside library code, the same
|
||||
way `DelegatingSecurityContextExecutor` is the safest pattern for an `Executor`: it works
|
||||
regardless of what the surrounding application has set `SecurityContextHolder`'s strategy to.
|
||||
|
||||
- **D) `ContextSnapshot.wrap(...)` around the forked `Callable`** -- the Chapter 2 mechanism
|
||||
applied to `fork()` instead of `execute()`. Because `SecurityContextHolderThreadLocalAccessor`
|
||||
is already registered with Micrometer's `ContextRegistry`, `ContextSnapshotFactory.builder()
|
||||
.build().captureAll()` picks up the current `SecurityContext` (and MDC, and tracing context)
|
||||
in one call, and `.wrap(callable)` restores all of them inside the subtask. This is the
|
||||
version worth reaching for once you have more than the `SecurityContext` to carry across the
|
||||
scope boundary.
|
||||
|
||||
## The practical takeaway
|
||||
|
||||
`StructuredTaskScope` does not give `SecurityContextHolder` anything for free. If your
|
||||
`fork()`ed subtasks need to call secured services, wrap them explicitly -- option C if you
|
||||
want zero new dependencies, option D if `context-propagation` is already on the classpath and
|
||||
you have other thread-locals to carry along too.
|
||||
Reference in New Issue
Block a user