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:
141
context-propagation/README.md
Normal file
141
context-propagation/README.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# context-propagation
|
||||
|
||||
Companion module for [Spring Security Context Propagation: The Complete
|
||||
Guide](https://ankurm.com/spring-security-context-propagation-complete-guide/) on ankurm.com.
|
||||
|
||||
Part of [spring-security-demo](../README.md); the sibling module is
|
||||
[method-security](../method-security/README.md), which covers what `@PreAuthorize` does with
|
||||
the `Authentication` once it has reached the right thread -- and the cases where it silently
|
||||
does nothing.
|
||||
|
||||
Eight small, dependency-light programs and one JUnit test suite that answer one question each:
|
||||
does a Spring Security `SecurityContext` survive a specific thread, scheduler, or request
|
||||
hand-off? Every scenario sets an `Authentication` somewhere and checks whether the other side
|
||||
of the hand-off can see it -- against real executors, a real Reactor pipeline, a real
|
||||
`TaskScheduler`, and real servlet filter classes (via Spring Test's mock request/response, no
|
||||
running server needed).
|
||||
|
||||
Every example the post shows -- `@Async`, `ExecutorService`, `CompletableFuture`, virtual
|
||||
threads, `StructuredTaskScope`, `ReactiveSecurityContextHolder`/WebFlux,
|
||||
`DelegatingSecurityContextTaskScheduler` and scheduled tasks, and the
|
||||
`SecurityContextHolderFilter`/`SecurityContextPersistenceFilter` servlet distinction -- has a
|
||||
runnable demo here, plus edge cases the post doesn't have room for. See the [edge-case
|
||||
index](docs/08-testing-contract.md#edge-case-index) for the full list with links.
|
||||
|
||||
## Verified versions
|
||||
|
||||
| Component | Version |
|
||||
|---|---|
|
||||
| JDK | 25 (Temurin 25.0.4.1+1), LTS, GA 2025-09-16 |
|
||||
| Spring Boot (reference target) | 4.1.1 |
|
||||
| Spring Framework | 7.0.9 |
|
||||
| Spring Security | 7.1.1 |
|
||||
| Spring Security Test | 7.1.1 |
|
||||
| `io.micrometer:context-propagation` | 1.2.1 (as managed by Boot 4.1.1's BOM) |
|
||||
| Reactor Core / Reactor Test | 3.8.7 (as managed by Boot 4.1.1's `reactor-bom` 2025.0.7) |
|
||||
| `jakarta.servlet-api` | 6.1.0 |
|
||||
| JUnit Jupiter | 6.0.3 |
|
||||
| AssertJ | 3.27.7 |
|
||||
|
||||
`StructuredTaskScope` is a **preview API** on JDK 25 (JEP 505, fifth preview) and remains
|
||||
preview through JDK 26 (JEP 525, sixth preview) -- every build/run/test command below needs
|
||||
`--enable-preview`.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
mvn dependency:build-classpath -Dmdep.outputFile=cp.txt
|
||||
javac --release 25 --enable-preview -cp "$(cat cp.txt)" -d target/classes $(find src/main -name '*.java')
|
||||
java --enable-preview -cp "target/classes:$(cat cp.txt)" com.ankurm.vt.Demo1PlainThreadLocal
|
||||
```
|
||||
|
||||
Or just run everything -- all seven demos plus the test suite -- and regenerate the captured
|
||||
output: `scripts/run-all.sh`. To run only the JUnit contract tests: `mvn test` (the
|
||||
`--enable-preview` flag is already wired into `pom.xml`'s surefire `argLine`, no extra flags
|
||||
needed).
|
||||
|
||||
## What each demo shows
|
||||
|
||||
| Demo | Question | Chapter |
|
||||
|---|---|---|
|
||||
| `Demo1PlainThreadLocal` | Does `InheritableThreadLocal` behave differently for a pooled platform thread vs. a fresh virtual thread? (No Spring.) | [docs/01](docs/01-inheritable-threadlocal.md) |
|
||||
| `Demo2AsyncVirtualThreads` | Does the Boot-4.1-style virtual-thread `@Async` executor propagate `SecurityContext`, and what four fixes change? | [docs/02](docs/02-async-virtual-threads.md) |
|
||||
| `Demo3StructuredConcurrency` | Does a `StructuredTaskScope.fork()` subtask see the parent's `SecurityContext`? | [docs/03](docs/03-structured-concurrency.md) |
|
||||
| `Demo4ExecutorWrapping` | Do `DelegatingSecurityContextExecutorService`/`Executor`/`AsyncTaskExecutor` propagate context on a classic *pooled platform-thread* executor, and does a reused worker leak between tasks the way `InheritableThreadLocal` did in Demo 1? | [docs/04](docs/04-executor-wrapping.md) |
|
||||
| `Demo5ReactiveContext` | Does `ReactiveSecurityContextHolder` survive a scheduler hop that kills plain `ThreadLocal`-based `SecurityContextHolder`? | [docs/05](docs/05-reactive-context.md) |
|
||||
| `Demo6ScheduledSystemIdentity` | What does `DelegatingSecurityContextTaskScheduler` actually capture, and when -- and how does the post's `createSystemContext()` pattern fix the fact that there's no real caller to propagate from? | [docs/06](docs/06-scheduled-tasks.md) |
|
||||
| `Demo7ServletFilterPersistence` | Does `SecurityContextHolderFilter` really never save, while `SecurityContextPersistenceFilter` does -- proven against real filter instances and a real `HttpSession`? | [docs/07](docs/07-servlet-filter-persistence.md) |
|
||||
| `SecurityContextPropagationContractTest` (JUnit, `src/test`) | Same ten claims above, pinned as assertions instead of printed lines; includes a `TestSecurityContextHolder`-based test reproducing the post's own "Testing Security Context Propagation" section | [docs/08](docs/08-testing-contract.md) |
|
||||
|
||||
## Endpoints / entry points
|
||||
|
||||
There's no web server in this repo (see the top of this file), so "entry points" means: every
|
||||
class above has a runnable `main()`, and the whole suite runs end to end via
|
||||
`scripts/run-all.sh`. `Demo5ReactiveContext` reproduces the post's `/profile`
|
||||
(`ReactiveSecurityContextHolder`) behavior as a plain `Mono` chain rather than a bound HTTP
|
||||
route, and `Demo7ServletFilterPersistence` reproduces the filter chain's request-scoped
|
||||
behavior against `MockHttpServletRequest`/`MockHttpServletResponse` rather than a bound
|
||||
servlet container -- both keep the "no web server, no HTTP" property the original three demos
|
||||
established, so the whole repo still runs in well under a second with zero open ports.
|
||||
|
||||
## Captured output
|
||||
|
||||
Every number and log line in the blog post traces back to one of these, produced by
|
||||
`scripts/run-all.sh`, not retyped:
|
||||
|
||||
- [docs/output/demo1.txt](docs/output/demo1.txt)
|
||||
- [docs/output/demo2.txt](docs/output/demo2.txt)
|
||||
- [docs/output/demo3.txt](docs/output/demo3.txt)
|
||||
- [docs/output/demo4.txt](docs/output/demo4.txt)
|
||||
- [docs/output/demo5.txt](docs/output/demo5.txt)
|
||||
- [docs/output/demo6.txt](docs/output/demo6.txt)
|
||||
- [docs/output/demo7.txt](docs/output/demo7.txt)
|
||||
- [docs/output/tests.txt](docs/output/tests.txt) -- `mvn test` surefire summary for the ten
|
||||
contract tests
|
||||
|
||||
## Doc chapters
|
||||
|
||||
Numbered, cross-linked, each with prev/next navigation at the top:
|
||||
|
||||
1. [InheritableThreadLocal across thread models](docs/01-inheritable-threadlocal.md)
|
||||
2. [@Async, DelegatingSecurityContextExecutor, and virtual threads](docs/02-async-virtual-threads.md)
|
||||
3. [StructuredTaskScope and SecurityContext](docs/03-structured-concurrency.md)
|
||||
4. [Executor, ExecutorService, and AsyncTaskExecutor wrapping](docs/04-executor-wrapping.md)
|
||||
5. [ReactiveSecurityContextHolder and Reactor Context](docs/05-reactive-context.md)
|
||||
6. [DelegatingSecurityContextTaskScheduler and the synthetic system identity](docs/06-scheduled-tasks.md)
|
||||
7. [SecurityContextHolderFilter vs. SecurityContextPersistenceFilter](docs/07-servlet-filter-persistence.md)
|
||||
8. [Testing contract + edge-case index](docs/08-testing-contract.md)
|
||||
|
||||
## Edge cases
|
||||
|
||||
Thirteen reproducible edge cases were found building this repository -- pooled-worker leaks
|
||||
the `Delegating*` classes don't have, the common `ForkJoinPool` trap, why
|
||||
`MODE_INHERITABLETHREADLOCAL` is a JVM-wide instrument, why there's no
|
||||
`DelegatingSecurityContextStructuredTaskScope` and never will be, a real `NullPointerException`
|
||||
hit writing the reactive test, `ReactiveSecurityContextHolder` completing empty rather than
|
||||
erroring, per-call (not per-construction) context capture in
|
||||
`DelegatingSecurityContextTaskScheduler`, why a synthetic `SYSTEM` principal isn't
|
||||
"anonymous", and the precise load-vs-save split between the two servlet filters. Full list,
|
||||
each with the chapter that reproduces it: [docs/08 § Edge-case
|
||||
index](docs/08-testing-contract.md#edge-case-index).
|
||||
|
||||
## The one-line summary of all eight chapters
|
||||
|
||||
`SecurityContextHolder` is a `ThreadLocal`. Nothing about virtual threads, structured
|
||||
concurrency, reactive streams, schedulers, or servlet filters changes that fact -- what changes
|
||||
between them is *how* (or whether) anything carries that `ThreadLocal`'s value across the
|
||||
boundary each one introduces. Virtual threads are never pooled, so
|
||||
`MODE_INHERITABLETHREADLOCAL`'s old danger (stale context on a reused pool worker) doesn't
|
||||
apply to them, but it's still a JVM-wide setting. The `Delegating*` wrapper family solves the
|
||||
same pooled-worker problem by a completely different mechanism -- explicit push/pop per task,
|
||||
never thread inheritance -- which is why it has worked, unchanged, since long before virtual
|
||||
threads existed. Reactive code doesn't have a `ThreadLocal`-compatible thread to begin with, so
|
||||
`ReactiveSecurityContextHolder` uses Reactor's own `Context` instead. Scheduled tasks have no
|
||||
caller at all, so the fix isn't propagation, it's minting an identity. And the servlet filter
|
||||
that used to auto-save the context for you was replaced by one that only loads -- a change
|
||||
worth knowing about before it's the reason a custom filter's write silently doesn't survive to
|
||||
the next request.
|
||||
|
||||
## License
|
||||
|
||||
MIT -- see [LICENSE](../LICENSE).
|
||||
Reference in New Issue
Block a user