Companion project for "The Spring Security Filter Chain Explained". A real Spring Boot 4.1.1 servlet application whose scenarios are Spring profiles, plus a diagnostic controller that prints the live FilterChainProxy, the reflected FilterOrderRegistration table, and the servlet container's own registrations. Twelve captured transcripts under docs/output/, nine cross-linked doc chapters, 21 assertions. Also fixes a broken relative link in method-security/docs/01: the cross-module reference to context-propagation/README.md needed two levels up, not one.
filter-chain — the Spring Security filter chain, printed from a running application
Companion project for The Spring Security Filter Chain Explained on ankurm.com.
Every claim the article makes about filter ordering is produced here by a real Boot application
and captured under docs/output/. The chain tables are read back out of the live
FilterChainProxy bean; the order numbers are reflected out of spring-security-config's own
FilterOrderRegistration. Nothing is transcribed from documentation.
Versions
| Version | Notes | |
|---|---|---|
| JDK | 25 (Temurin 25.0.4.1+1) | current LTS |
| Spring Boot | 4.1.1 | inherited as parent, so every version below is Boot-managed |
| Spring Framework | 7.0.9 | |
| Spring Security | 7.1.1 | latest GA; 7.2.0-M1 is a milestone, not a release |
| Tomcat | 11.0.x | Boot-managed |
| JUnit Jupiter | 6.x / AssertJ 3.x | Boot-managed |
Versions were taken from repo1.maven.org/.../maven-metadata.xml, not from release
announcements.
Quickstart
./scripts/run.sh baseline # start with the reference configuration
curl -s localhost:8080/diag/chains
curl -s localhost:8080/diag/order
TRACE=1 ./scripts/run.sh baseline
curl -s -u alice:password localhost:8080/whoami # then read /tmp/filter-chain-app.log
./scripts/run-all.sh # every scenario, regenerating docs/output/
mvn test # just the 21 assertions
./scripts/stop.sh
Users are alice / password (ROLE_USER) and root / password (ROLE_ADMIN).
Profiles
Each scenario in the article is a profile on this one application.
| Profile | Configuration | Shows |
|---|---|---|
baseline (default) |
BaselineSecurityConfig |
The reference 16-filter chain |
custom |
CustomFiltersSecurityConfig |
Four custom filters at four anchors; the ExceptionTranslationFilter boundary |
misordered |
MisorderedSecurityConfig |
An authentication filter after AuthorizationFilter — 401 with a valid credential |
tie |
TieSecurityConfig |
Two filters on one anchor. -DTIE_REVERSED=true flips them |
doublereg |
DoubleRegistrationConfig |
A filter bean registered twice. Add ,fixed for the cure |
multichain |
MultiChainSecurityConfig |
Three chains of three different lengths |
ignoring |
IgnoringSecurityConfig |
WebSecurity.ignoring() — a chain with zero filters |
Two JVM flags change behaviour rather than configuration:
| Flag | Effect |
|---|---|
-DTIE_REVERSED=true |
Swaps the two addFilterBefore calls in the tie profile |
-DUNIQUE_ONCE_KEY=true |
Gives each TenantFilter its own OncePerRequestFilter key, so the second one stops silently skipping itself |
Endpoints
| Endpoint | Purpose |
|---|---|
GET /diag/chains |
Every SecurityFilterChain in FilterChainProxy, with each chain's filters in order |
GET /diag/order |
The whole FilterOrderRegistration table, marking slots whose class is absent |
GET /diag/servlet-filters |
What the container has registered — where double registration shows up |
GET /whoami |
Who the chain decided you are by the time a controller runs |
GET /public/hello |
permitAll |
GET /static/asset.txt |
Under ignoring() in the ignoring profile |
GET /api/data |
Served by the API chain under multichain |
POST /hello |
For provoking a CSRF rejection |
GET /tenant/doc, GET /tenant/translated |
Guarded by TenantFilters either side of ExceptionTranslationFilter |
GET /markers |
Executed order of the tied marker filters |
The /diag/* endpoints are the interesting part of this project and also the reason you would
never ship it. Delete DiagnosticsController before deploying anything resembling this.
Documentation
Nine chapters under docs/, starting with
01 · The two proxies.
What this module found
Things that are true of Spring Security 7.1.1 and are not in the reference documentation:
- The default chain is sixteen filters, not the fifteen the docs list —
DefaultResourcesFilter(2400) is missing from that sample, and the startup log format has changed too. - Orders 300 and 4100 name
ChannelProcessingFilterandFilterSecurityInterceptor, both removed in 7.0. The slots were kept so no other number moved. addFilterBeforeresolves its anchor against a static table, not against the chain being built — so you can anchor to a filter you have disabled, and it still works.- Two filters on the same anchor get the same order number; the tie is broken by
List.sortbeing stable, which is ajava.util.Listguarantee and not a Spring Security one. - Two instances of the same
OncePerRequestFiltersubclass in one chain share their already-filtered key, and the second one silently never runs. - The documented placement for an authorization filter (after
AnonymousAuthenticationFilter, 3701) is belowExceptionTranslationFilter(4000), so theAccessDeniedExceptionthe documented example throws produces a 500, not a 403. - On the
/errorre-dispatch after a rejection,FilterChainProxyruns the whole chain again, but the sixOncePerRequestFilter-based filters skip themselves — so the error page is authorized but not authenticated.
Related modules
| Module | Article |
|---|---|
context-propagation/ |
Spring Security Context Propagation |
method-security/ |
Method Security in Spring Security 7 |
License
MIT — see ../LICENSE.