# `filter-chain` — the Spring Security filter chain, printed from a running application Companion project for [**The Spring Security Filter Chain Explained**](https://ankurm.com/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/`](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 ```bash ./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`](src/main/java/com/ankurm/chain/config/BaselineSecurityConfig.java) | The reference 16-filter chain | | `custom` | [`CustomFiltersSecurityConfig`](src/main/java/com/ankurm/chain/config/CustomFiltersSecurityConfig.java) | Four custom filters at four anchors; the `ExceptionTranslationFilter` boundary | | `misordered` | [`MisorderedSecurityConfig`](src/main/java/com/ankurm/chain/config/MisorderedSecurityConfig.java) | An authentication filter after `AuthorizationFilter` — 401 with a valid credential | | `tie` | [`TieSecurityConfig`](src/main/java/com/ankurm/chain/config/TieSecurityConfig.java) | Two filters on one anchor. `-DTIE_REVERSED=true` flips them | | `doublereg` | [`DoubleRegistrationConfig`](src/main/java/com/ankurm/chain/config/DoubleRegistrationConfig.java) | A filter bean registered twice. Add `,fixed` for the cure | | `multichain` | [`MultiChainSecurityConfig`](src/main/java/com/ankurm/chain/config/MultiChainSecurityConfig.java) | Three chains of three different lengths | | `ignoring` | [`IgnoringSecurityConfig`](src/main/java/com/ankurm/chain/config/IgnoringSecurityConfig.java) | `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 `TenantFilter`s 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/`](docs/README.md), starting with [01 · The two proxies](docs/01-the-two-proxies.md). ## 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 `ChannelProcessingFilter` and `FilterSecurityInterceptor`, both **removed in 7.0**. The slots were kept so no other number moved. - `addFilterBefore` resolves 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.sort` being stable, which is a `java.util.List` guarantee and not a Spring Security one. - Two instances of the same `OncePerRequestFilter` subclass 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 **below** `ExceptionTranslationFilter` (4000), so the `AccessDeniedException` the documented example throws produces a 500, not a 403. - On the `/error` re-dispatch after a rejection, `FilterChainProxy` runs the whole chain again, but the six `OncePerRequestFilter`-based filters skip themselves — so the error page is **authorized but not authenticated**. ## Related modules | Module | Article | |---|---| | [`context-propagation/`](../context-propagation/README.md) | [Spring Security Context Propagation](https://ankurm.com/spring-security-context-propagation-complete-guide/) | | [`method-security/`](../method-security/README.md) | [Method Security in Spring Security 7](https://ankurm.com/spring-security-7-method-security-proxy-traps/) | ## License MIT — see [`../LICENSE`](../LICENSE).