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,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

View 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]

View 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

View 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

View 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]

View 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]

View 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)

View 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