Add every example from the post, plus edge cases, to the companion repo
Full companion repo for the ankurm.com post "Spring Security Context Propagation: The Complete Guide" -- every code example the post discusses now has a corresponding runnable, verified demo (JDK 25, Spring Security 7.1.1, Spring Boot 4.1.1 dependency versions), not just the virtual-thread/structured-concurrency sections: - Demo1PlainThreadLocal: InheritableThreadLocal across thread models (no Spring) - Demo2AsyncVirtualThreads: @Async on a virtual-thread SimpleAsyncTaskExecutor (DelegatingSecurityContextExecutor vs ContextPropagatingTaskDecorator) - Demo3StructuredConcurrency: StructuredTaskScope.fork() propagation - Demo4ExecutorWrapping: DelegatingSecurityContextExecutorService/Executor/ AsyncTaskExecutor on a classic pooled platform-thread executor -- the post's "Using @Async" / "Using ExecutorService" / "Using CompletableFuture" sections - Demo5ReactiveContext: ReactiveSecurityContextHolder vs. ThreadLocal across a Reactor scheduler hop -- the post's WebFlux/getProfile() section - Demo6ScheduledSystemIdentity: DelegatingSecurityContextTaskScheduler's actual per-call capture semantics (confirmed via bytecode before writing the demo) and the createSystemContext() pattern -- the post's scheduled-tasks section - Demo7ServletFilterPersistence: SecurityContextHolderFilter (load-only) vs. SecurityContextPersistenceFilter (load+auto-save), against real filter instances and a real HttpSession -- the post's servlet-environment section - SecurityContextPropagationContractTest: 10 JUnit tests pinning the above as assertions instead of printed lines, including a TestSecurityContextHolder-based test reproducing the post's own "Testing Security Context Propagation" section Thirteen edge cases discovered along the way are indexed in docs/08 with links into the chapter that reproduces each one -- a reused pool worker NOT leaking under the Delegating* wrappers (unlike Demo1's InheritableThreadLocal), the common ForkJoinPool trap, why there's no DelegatingSecurityContextStructuredTaskScope and never will be, a real NullPointerException from Reactor's map() hit while writing the reactive test, per-call (not per-construction) context capture in DelegatingSecurityContextTaskScheduler, and the precise load-vs-save split between the two servlet filters, among others. docs/01-08 are numbered, cross-linked chapters with prev/next navigation; README indexes all demos, chapters, captured output, and the edge-case list. scripts/run-all.sh regenerates every docs/output/*.txt and the test suite output from one command.
This commit is contained in:
5
docs/output/demo1.txt
Normal file
5
docs/output/demo1.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
=== Demo 1: InheritableThreadLocal across thread models ===
|
||||
fresh platform thread sees: request-A
|
||||
pool thread, task 1, sees: request-B
|
||||
pool thread, task 2 (reused), sees: request-B <-- stale, not request-C
|
||||
fresh virtual thread sees: request-D
|
||||
6
docs/output/demo2.txt
Normal file
6
docs/output/demo2.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
=== Demo 2: @Async-style virtual thread executor + SecurityContext ===
|
||||
A) MODE_THREADLOCAL, raw SimpleAsyncTaskExecutor(virtual): NO AUTHENTICATION (lost) [VirtualThread[#23,vt-1]/runnable@ForkJoinPool-1-worker-1]
|
||||
B) MODE_INHERITABLETHREADLOCAL, raw SimpleAsyncTaskExecutor(virtual): authenticated as bob [VirtualThread[#26,vt-1]/runnable@ForkJoinPool-1-worker-2]
|
||||
C) DelegatingSecurityContextExecutor around SimpleAsyncTaskExecutor(virtual): authenticated as carol [VirtualThread[#27,vt-1]/runnable@ForkJoinPool-1-worker-1]
|
||||
SecurityContextHolderThreadLocalAccessor present: true
|
||||
D) ContextPropagatingTaskDecorator on SimpleAsyncTaskExecutor(virtual), no Delegating* wrapper: authenticated as dave [VirtualThread[#28,vt-1]/runnable@ForkJoinPool-1-worker-1]
|
||||
5
docs/output/demo3.txt
Normal file
5
docs/output/demo3.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
=== Demo 3: StructuredTaskScope.fork() + SecurityContext ===
|
||||
A) plain fork, MODE_THREADLOCAL: NO AUTHENTICATION (lost)
|
||||
B) plain fork, MODE_INHERITABLETHREADLOCAL: authenticated as frank
|
||||
C) manual capture/restore: authenticated as grace
|
||||
D) ContextSnapshot.wrap: authenticated as heidi
|
||||
9
docs/output/demo4.txt
Normal file
9
docs/output/demo4.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
=== Demo 4: Executor/ExecutorService/AsyncTaskExecutor wrapping on a pooled platform thread ===
|
||||
A) raw ThreadPoolExecutor, no wrapper: NO AUTHENTICATION (lost)
|
||||
B) DelegatingSecurityContextExecutorService.execute(...): authenticated as bob
|
||||
B2) DelegatingSecurityContextExecutorService.submit(Callable): authenticated as bob
|
||||
EDGE) task 1 on possibly-reused worker: authenticated as carol-task1
|
||||
EDGE) task 2, same pool, different caller context: authenticated as dave-task2 <-- correct, NOT stale, unlike plain InheritableThreadLocal on a reused worker
|
||||
C) DelegatingSecurityContextExecutor + CompletableFuture.supplyAsync: authenticated as erin
|
||||
C2) default CompletableFuture executor (common ForkJoinPool), no wrapper: NO AUTHENTICATION (lost)
|
||||
D) DelegatingSecurityContextAsyncTaskExecutor wrapping ThreadPoolTaskExecutor: authenticated as grace
|
||||
6
docs/output/demo5.txt
Normal file
6
docs/output/demo5.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
=== Demo 5: ReactiveSecurityContextHolder vs. ThreadLocal across a scheduler hop ===
|
||||
A) SecurityContextHolder (ThreadLocal), no scheduler hop: authenticated as alice
|
||||
B) SecurityContextHolder (ThreadLocal), AFTER publishOn to a different thread: NO AUTHENTICATION (lost) [proves ThreadLocal doesn't survive a scheduler hop]
|
||||
C) ReactiveSecurityContextHolder + contextWrite, no scheduler hop: Hello, carol
|
||||
D) ReactiveSecurityContextHolder + contextWrite, AFTER publishOn to a different thread: Hello, dave [Context travels with the stream, not the thread]
|
||||
E) getProfile() with no contextWrite() upstream at all: Anonymous [defaultIfEmpty fires; getContext() completes empty, it does not error]
|
||||
6
docs/output/demo6.txt
Normal file
6
docs/output/demo6.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
=== Demo 6: DelegatingSecurityContextTaskScheduler and the synthetic system identity ===
|
||||
A) schedule() called with NO context present on the caller thread: NO AUTHENTICATION (lost) [this is the realistic startup case the post warns about]
|
||||
B1) first schedule() call, caller context = registration-thread-X: authenticated as registration-thread-X
|
||||
B2) second schedule() call on the SAME wrapper, caller context changed to registration-thread-Y: authenticated as registration-thread-Y [independent per-call capture, not frozen at wrapper construction]
|
||||
C) task body sets its own systemContext(), ignoring anything the scheduler wrapper captured: SYSTEM with authorities [ROLE_SYSTEM]
|
||||
EDGE) AuthenticationTrustResolver.isAnonymous(systemContext()): false [false -- SYSTEM is a normal authenticated principal, not Spring Security's anonymous concept]
|
||||
6
docs/output/demo7.txt
Normal file
6
docs/output/demo7.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
=== Demo 7: SecurityContextHolderFilter vs SecurityContextPersistenceFilter -- load vs. load+save ===
|
||||
A) SecurityContextPersistenceFilter, context set mid-chain, auto-saved to session after chain returns: true [true -- this filter saves for you]
|
||||
B) SecurityContextHolderFilter, context set mid-chain, auto-saved to session after chain returns: false [false -- requireExplicitSave's default; nothing persists unless you save it yourself]
|
||||
C) SecurityContextHolderFilter + explicit repository.saveContext(...) inside the chain: true [true -- the workaround the post recommends actually works]
|
||||
D) SecurityContextHolderFilter, context already saved in an existing session, next request: authenticated as dave [it does load -- "only loads, never saves" describes the SAVE side, not the LOAD side]
|
||||
EDGE) brand-new request, no prior session, nothing set: NO AUTHENTICATION (empty context, not an error)
|
||||
6
docs/output/tests.txt
Normal file
6
docs/output/tests.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
mvn test -- SecurityContextPropagationContractTest (10 tests pinning the claims each demo prints above)
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.ankurm.vt.SecurityContextPropagationContractTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.413 s -- in com.ankurm.vt.SecurityContextPropagationContractTest
|
||||
Reference in New Issue
Block a user