Add the filter-chain module
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.
This commit is contained in:
118
filter-chain/README.md
Normal file
118
filter-chain/README.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# `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).
|
||||
Reference in New Issue
Block a user