1
0

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:
2026-08-26 01:59:35 +00:00
parent 5e9e7f1b12
commit 73ab67b171
52 changed files with 3562 additions and 10 deletions

View File

@@ -0,0 +1,93 @@
[← docs index](README.md) · **01 · The two proxies** · [02 · The default chain →](02-the-default-chain.md)
# 01 · The two proxies, and why there are two
Spring Security is a single servlet filter. Everything else it does in a web application happens
inside that one filter.
The container knows about exactly one entry:
```
countingFilter com.ankurm.chain.filter.CountingFilter urls=[/*]
springSecurityFilterChain org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1 urls=[/*]
```
That comes from [`docs/output/demo12-servlet-filters.txt`](output/demo12-servlet-filters.txt),
printed live from `ServletContext.getFilterRegistrations()`. Note what is *not* in it: none of
the sixteen filters that actually make the security decision. The container cannot see them.
## DelegatingFilterProxy
The container instantiates filters itself, long before Spring's `ApplicationContext` exists, and
it has no idea what a bean is. `DelegatingFilterProxy` bridges that: it is a real
`jakarta.servlet.Filter` registered with the container, and on the first request it looks up a
Spring bean by name and forwards to it.
In Boot the registration is not literally a `DelegatingFilterProxy` — it is an anonymous
subclass created by `DelegatingFilterProxyRegistrationBean`, which is why the class name above
ends in `$1`. Functionally it is the same thing.
Two consequences worth internalising:
- The security chain has a **position in the container's filter list**, and things registered
ahead of it run before any of Spring Security exists for that request. Boot registers it at
order **-100** (`SecurityFilterProperties.DEFAULT_FILTER_ORDER`, settable with
`spring.security.filter.order`), behind `characterEncodingFilter` and `formContentFilter`
(`OrderedFormContentFilter.DEFAULT_ORDER` is -9900).
- It is registered for **every** `DispatcherType`. `SecurityFilterProperties.dispatcherTypes`
defaults to `EnumSet.allOf(DispatcherType.class)`, which is exactly why the whole chain runs a
second time on the internal `/error` dispatch after a rejection — see
[06 · Reading the TRACE output](06-reading-the-trace.md#misleading-thing-3--the-rejected-request-runs-the-chain-twice).
Narrow it with `spring.security.filter.dispatcher-types=request` if you want the security
chain out of error and forward dispatches.
- A `Filter` bean you declare is registered with the *container* automatically. That is the
mechanism behind the double-registration trap in
[05 · Failure modes](05-failure-modes.md#a-custom-filter-that-runs-twice).
> **Boot 4 moved these constants.** `SecurityProperties.DEFAULT_FILTER_ORDER` and
> `SecurityProperties.BASIC_AUTH_ORDER` — the latter a common `@Order(...)` argument — are now
> on `org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterProperties`.
> The values are unchanged (-100 and `Integer.MAX_VALUE - 5`); `SecurityProperties` in 4.1.1
> retains only `getUser()`. `IGNORED_ORDER` does not appear anywhere in the Boot 4.1.1 jars.
## FilterChainProxy
The bean `DelegatingFilterProxy` forwards to is `FilterChainProxy`. It is not a chain; it holds
a list of chains. On each request it walks its `SecurityFilterChain` list in order, asks each one
whether it matches, and **invokes the first one that says yes**. The rest are never consulted —
there is no "and then the next chain too".
It also does three things nothing else does:
1. Applies the `HttpFirewall` before any chain runs. A request rejected here never reaches your
filters, which is why a `RequestRejectedException` looks like it came from nowhere.
2. Clears `SecurityContextHolder` in a `finally` block, so a thread returned to the pool does
not carry someone else's identity. (The
[context-propagation module](../../context-propagation/README.md) is about what happens when you
leave that thread.)
3. Wraps each filter in a decorator. With Micrometer on the classpath — Boot's actuator starter
pulls it in — that decorator is `ObservationFilterChainDecorator`, and you will see two extra
frames per filter in every stack trace:
```
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(...)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(...)
```
That is from [`demo4-trace-csrf-403.txt`](output/demo4-trace-csrf-403.txt), a real stack
trace from this application. Without Micrometer you get the plain `VirtualFilterChain` and
half the frames.
## SecurityFilterChain
A `RequestMatcher` plus an ordered `List<Filter>`. That is the whole interface. `HttpSecurity`
is a builder that produces one; `@Bean SecurityFilterChain` is how you register it.
The list is ordered by `HttpSecurity.performBuild()`, which sorts with `OrderComparator` and
then throws the ordering wrappers away. By the time `FilterChainProxy` sees the chain, the order
is baked in and no longer inspectable — which is why this module reads the numbers back out of
the [order table](03-the-order-table.md) instead.
---
[← docs index](README.md) · **01 · The two proxies** · [02 · The default chain →](02-the-default-chain.md)

View File

@@ -0,0 +1,69 @@
[← 01 · The two proxies](01-the-two-proxies.md) · **02 · The default chain** · [03 · The order table →](03-the-order-table.md)
# 02 · The default chain, filter by filter
The configuration in [`BaselineSecurityConfig`](../src/main/java/com/ankurm/chain/config/BaselineSecurityConfig.java)
turns on four things — CSRF, HTTP Basic, form login, request authorization — and gets
**sixteen filters**. Captured live in
[`docs/output/demo2-default-chain.txt`](output/demo2-default-chain.txt) and pinned by
`FilterChainContractTest.Baseline.defaultChainIsSixteenFilters`.
| # | Order | Filter | What it does | Skip it and… |
|---|---|---|---|---|
| 1 | 100 | `DisableEncodeUrlFilter` | Stops the container appending `;jsessionid=` to URLs | Session ids leak into logs, referrers and bookmarks |
| 2 | 600 | `WebAsyncManagerIntegrationFilter` | Bridges the `SecurityContext` into `Callable` async MVC | `@Async`-style MVC returns lose the principal |
| 3 | 700 | `SecurityContextHolderFilter` | Installs a **deferred** context supplier; clears the holder afterwards | Nothing loads the stored context |
| 4 | 900 | `HeaderWriterFilter` | `X-Content-Type-Options`, `X-XSS-Protection`, cache headers, HSTS on TLS | No security headers |
| 5 | 1100 | `CsrfFilter` | Validates the token on state-changing methods | CSRF |
| 6 | 1200 | `LogoutFilter` | Matches `POST /logout`, clears the context and session | `/logout` 404s |
| 7 | 2100 | `UsernamePasswordAuthenticationFilter` | Matches `POST /login`, authenticates form credentials | Form login does nothing |
| 8 | 2400 | `DefaultResourcesFilter` | Serves the CSS the generated login page links to | Unstyled default login page |
| 9 | 2500 | `DefaultLoginPageGeneratingFilter` | Renders `GET /login` when you have not supplied one | No login page |
| 10 | 2600 | `DefaultLogoutPageGeneratingFilter` | Renders `GET /logout` confirmation | No logout page |
| 11 | 3100 | `BasicAuthenticationFilter` | Reads the `Authorization: Basic` header | HTTP Basic does nothing |
| 12 | 3300 | `RequestCacheAwareFilter` | Replays the request that triggered a login redirect | Users land on `/` after login instead of where they were |
| 13 | 3400 | `SecurityContextHolderAwareRequestFilter` | Wraps the request so `getUserPrincipal()`/`isUserInRole()` work | Servlet-API security methods return null |
| 14 | 3700 | `AnonymousAuthenticationFilter` | Substitutes an `AnonymousAuthenticationToken` when nothing authenticated | `getAuthentication()` returns `null` and `.anonymous()` rules break |
| 15 | 4000 | `ExceptionTranslationFilter` | Turns `AuthenticationException`/`AccessDeniedException` into 401/403 or a redirect | Denials surface as **500** |
| 16 | 4200 | `AuthorizationFilter` | Runs `authorizeHttpRequests` and denies | Nothing is authorized |
## Three things about that table
**`DefaultResourcesFilter` is the sixteenth.** The list in the reference documentation has
fifteen and omits it; that sample also uses the older `Will secure any request with [ … ]`
wording, which 7.1.1 no longer prints. The current wording is
`Will secure any request with filters: …` — verified against the constant pool of
`DefaultSecurityFilterChain` in 6.5.1, 7.0.7 and 7.1.1, all three of which use the newer form.
If you grep your logs for the bracketed version you will find nothing.
**`SessionManagementFilter` is not there.** Nor is `SecurityContextPersistenceFilter`. Plenty of
articles still list both. `SessionManagementFilter` appears only when you configure
`sessionManagement(…)` explicitly — see the API chain in
[`demo10-multichain.txt`](output/demo10-multichain.txt), where asking for `STATELESS` *adds* a
filter at 3900. `SecurityContextPersistenceFilter` was superseded by `SecurityContextHolderFilter`
in 6.0 and is deprecated.
**Filter 3 does not read the session.** `SecurityContextHolderFilter` installs a
`SupplierDeferredSecurityContext`; the session read happens on first access. In
[`demo3-trace-authenticated.txt`](output/demo3-trace-authenticated.txt) the line
`HttpSessionSecurityContextRepository : No HttpSession currently exists` appears under filter
**11**, not filter 3, because `BasicAuthenticationFilter` was the first thing to ask. That is a
feature — a request that never touches the context never touches the session — and it is also
why reading the TRACE log as a strict timeline misleads. See
[06 · Reading the TRACE output](06-reading-the-trace.md).
## The four events
Everything above collapses into four events, in this order:
1. The `SecurityContext` is made available (filter 3)
2. The request is protected from exploits (filters 45)
3. The request is authenticated (filters 614)
4. The request is authorized (filter 16, with 15 standing by to translate the refusal)
Where a custom filter goes is entirely a question of which of those four have already happened.
That is [04 · Where custom filters land](04-where-custom-filters-land.md).
---
[← 01 · The two proxies](01-the-two-proxies.md) · **02 · The default chain** · [03 · The order table →](03-the-order-table.md)

View File

@@ -0,0 +1,117 @@
[← 02 · The default chain](02-the-default-chain.md) · **03 · The order table** · [04 · Where custom filters land →](04-where-custom-filters-land.md)
# 03 · The order table
The order of the chain is not emergent and it is not the order you write your configuration in.
It is a hard-coded table in a package-private class,
`org.springframework.security.config.annotation.web.builders.FilterOrderRegistration`, built once
in its constructor:
```java
Step order = new Step(100, 100); // INITIAL_ORDER = 100, ORDER_STEP = 100
put(DisableEncodeUrlFilter.class, order.next()); // 100
put(ForceEagerSessionCreationFilter.class, order.next()); // 200
```
`Step.next()` returns the current value and then adds the step, so slots run **100, 200, 300 …**
with 100 free numbers between neighbours. [`FilterOrderTable`](../src/main/java/com/ankurm/chain/support/FilterOrderTable.java)
reads that map back by reflection rather than transcribing it, and
[`demo1-order-table.txt`](output/demo1-order-table.txt) is its output for 7.1.1.
## The full table
```
100 DisableEncodeUrlFilter 2200 OneTimeTokenAuthenticationFilter
200 ForceEagerSessionCreationFilter 2300 -- reserved, nothing registered --
300 ChannelProcessingFilter (removed) 2400 DefaultResourcesFilter
400 HttpsRedirectFilter 2500 DefaultLoginPageGeneratingFilter
500 -- reserved, nothing registered -- 2600 DefaultLogoutPageGeneratingFilter
600 WebAsyncManagerIntegrationFilter 2700 DefaultOneTimeTokenSubmitPageGeneratingFilter
700 SecurityContextHolderFilter 2800 ConcurrentSessionFilter
800 SecurityContextPersistenceFilter 2900 DigestAuthenticationFilter
900 HeaderWriterFilter 3000 BearerTokenAuthenticationFilter
1000 CorsFilter (org.springframework.web) 3100 BasicAuthenticationFilter
1100 CsrfFilter 3200 AuthenticationFilter
1200 LogoutFilter 3300 RequestCacheAwareFilter
1300 OAuth2AuthorizationRequestRedirectFilter 3400 SecurityContextHolderAwareRequestFilter
1400 Saml2WebSsoAuthenticationRequestFilter 3500 JaasApiIntegrationFilter
1500 GenerateOneTimeTokenFilter 3600 RememberMeAuthenticationFilter
1600 X509AuthenticationFilter 3700 AnonymousAuthenticationFilter
1700 AbstractPreAuthenticatedProcessingFilter 3800 OAuth2AuthorizationCodeGrantFilter
1800 CasAuthenticationFilter 3900 SessionManagementFilter
1900 OAuth2LoginAuthenticationFilter 4000 ExceptionTranslationFilter
2000 Saml2WebSsoAuthenticationFilter 4100 FilterSecurityInterceptor (removed)
2100 UsernamePasswordAuthenticationFilter 4200 AuthorizationFilter
4300 SwitchUserFilter
```
Forty-one registered slots, two reserved gaps, orders 100 through 4300.
## Two slots name classes that no longer exist
The map is keyed by **class-name string**, not by `Class`, because many of its entries live in
optional modules — `oauth2-client`, `saml2-service-provider`, `cas`,
`oauth2-resource-server` — that may not be on the classpath. Registering by name means the
table can mention them without failing to load.
Two entries exploit that for a different reason. `ChannelProcessingFilter` (300) and
`FilterSecurityInterceptor` (4100) are both `org.springframework.security.web` classes, and
`spring-security-web` **is** on the classpath — but the classes are not in it:
```
$ unzip -l spring-security-web-6.5.1.jar | grep -c 'access/intercept/FilterSecurityInterceptor.class'
1
$ unzip -l spring-security-web-7.1.1.jar | grep -c 'access/intercept/FilterSecurityInterceptor.class'
0
```
Both were removed in Spring Security 7.0. Their slots were left in the table so that no other
number had to move — the 6.5.1 and 7.1.1 tables are byte-for-byte identical in content.
`FilterOrderTableTest.removedFiltersStillOccupyTheirSlots` asserts exactly this.
The practical consequence is a compile error, not a runtime one. Every pre-7 tutorial that says
```java
http.addFilterBefore(myFilter, FilterSecurityInterceptor.class);
```
no longer compiles. The replacement anchor is `AuthorizationFilter.class`, one slot later at
4200 — so a filter that was at 4099 is now at 4199, and if anything of yours sat between them,
check it.
## Resolution walks up the superclass chain
`getOrder(Class)` does not do a map lookup and give up. It looks the class up, and on a miss
takes `getSuperclass()` and tries again, until it runs out of superclasses:
```java
for (Class<?> type = filterType; type != null; type = type.getSuperclass()) {
Integer order = table.get(type.getName());
if (order != null) return order;
}
return null;
```
That is why `http.addFilter(myFilter)` — the single-argument form, with no anchor — works at all.
It only works if some ancestor of your filter is in the table. A subclass of
`UsernamePasswordAuthenticationFilter` inherits slot 2100 and needs no anchor. A subclass of
`AbstractPreAuthenticatedProcessingFilter` inherits 1700.
`OncePerRequestFilter` is **not** in the table, which is what most people extend. So:
```java
http.addFilter(new RequestIdFilter());
// IllegalArgumentException: The Filter class com.ankurm.chain.filter.RequestIdFilter does not
// have a registered order and cannot be added without a specified order. Consider using
// addFilterBefore or addFilterAfter instead.
```
(Message read out of `HttpSecurity`'s constant pool with `javap -v`, not paraphrased.)
`addFilterBefore` and `addFilterAfter` raise the shorter sibling of that message,
`The Filter class … does not have a registered order`, when the **anchor** is unknown. Use the
two-argument form and name an anchor that is in the table.
---
[← 02 · The default chain](02-the-default-chain.md) · **03 · The order table** · [04 · Where custom filters land →](04-where-custom-filters-land.md)

View File

@@ -0,0 +1,112 @@
[← 03 · The order table](03-the-order-table.md) · **04 · Where custom filters land** · [05 · Failure modes →](05-failure-modes.md)
# 04 · Where custom filters land
`HttpSecurity` gives you four ways to add a filter. Three of them take an anchor:
| Call | Order your filter gets |
|---|---|
| `addFilterBefore(f, Anchor.class)` | `order(Anchor) - 1` |
| `addFilterAt(f, Anchor.class)` | `order(Anchor)` |
| `addFilterAfter(f, Anchor.class)` | `order(Anchor) + 1` |
| `addFilter(f)` | `order(f.getClass())`, resolved up the superclass chain |
All three anchored forms go through one private method:
```java
private HttpSecurity addFilterAtOffsetOf(Filter filter, int offset, Class<? extends Filter> registeredFilter) {
Integer registeredFilterOrder = this.filterOrders.getOrder(registeredFilter);
if (registeredFilterOrder == null) { throw new IllegalArgumentException(); }
int order = registeredFilterOrder + offset;
this.filters.add(new OrderedFilter(filter, order));
this.filterOrders.put(filter.getClass(), order); // <- your filter is now an anchor too
return this;
}
```
Four things follow from those seven lines, and none of them are documented.
## 1 · The offset is exactly ±1, not "the next free slot"
`addFilterBefore(f, CsrfFilter.class)` gives **1099**. It does not give 1050, or "somewhere
between `HeaderWriterFilter` and `CsrfFilter`". Pinned by
`FilterOrderTableTest.offsetsAreExactlyOne`.
## 2 · Your filter becomes an anchor
The last-but-one line registers *your* class at the order it just computed. So this works:
```java
http.addFilterAfter(new ApiKeyFilter(), LogoutFilter.class) // 1201
.addFilterAfter(new AuditFilter(), ApiKeyFilter.class); // 1202
```
And so does the accident: add two instances of the same class against different anchors and the
second `put` overwrites the first in the table. The chain is still correct — the `OrderedFilter`
wrappers already hold their own numbers — but any *later* `addFilterBefore(x, ThatClass.class)`
resolves against whichever registration happened last.
## 3 · The anchor does not have to be in the chain
`getOrder` consults the static table, not the chain being built. The `tie` profile calls
`csrf().disable()` and then still adds filters *before* `CsrfFilter.class`:
```
=== chain 1/12 (profile: tie)
4/12 HeaderWriterFilter order=900
5/12 MarkerFilterA order=? (not in the registration table)
6/12 MarkerFilterB order=? (not in the registration table)
7/12 LogoutFilter order=1200
```
There is no `CsrfFilter`. The markers still land on 1099, between 900 and 1200. Asserted by
`FilterChainContractTest.Tie.theAnchorNeedNotBePresent`. This is convenient — you can anchor to
a filter you have disabled — and it is a trap, because "before the CSRF filter" stops meaning
anything if someone later removes it and moves the surrounding filters.
## 4 · Two filters on the same anchor get the same number
`addFilterBefore(a, CsrfFilter.class)` and `addFilterBefore(b, CsrfFilter.class)` both produce
1099. `performBuild()` sorts with
```java
this.filters.sort(OrderComparator.INSTANCE);
```
`List.sort` on an `ArrayList` is a stable sort, so equal orders keep insertion order — the order
of your `addFilterBefore` calls. [`demo8-tie.txt`](output/demo8-tie.txt) shows both directions:
```
--- A registered first ---
executed order: [A, B]
--- B registered first (-DTIE_REVERSED=true), nothing else changed ---
executed order: [B, A]
```
That guarantee comes from `java.util.List`, not from Spring Security. Nothing in the API
promises it. If the relative order of two custom filters matters, anchor the second to the
first rather than relying on it.
## Where to put what
The four events from [02](02-the-default-chain.md#the-four-events) give the rule:
| Your filter is… | Anchor it after | Because by then |
|---|---|---|
| exploit protection (headers, tenancy, rate limits) | `SecurityContextHolderFilter` (701) | the context is available |
| authentication | `LogoutFilter` (1201) | headers and CSRF have run, nothing has authenticated yet |
| authorization, or anything that throws `AccessDeniedException` | `ExceptionTranslationFilter` (4001) | the principal is settled **and the throw will be translated** |
That last row differs from the reference documentation, which suggests
`AnonymousAuthenticationFilter` (3701) for authorization filters. 3701 is below
`ExceptionTranslationFilter` at 4000, so an `AccessDeniedException` thrown there is not
translated. See [05 · Failure modes](05-failure-modes.md#a-denial-that-comes-back-as-500).
The `custom` profile puts one filter at each of those three placements plus the documented one,
and [`demo5-custom-placement.txt`](output/demo5-custom-placement.txt) shows all four in the
live chain.
---
[← 03 · The order table](03-the-order-table.md) · **04 · Where custom filters land** · [05 · Failure modes →](05-failure-modes.md)

View File

@@ -0,0 +1,166 @@
[← 04 · Where custom filters land](04-where-custom-filters-land.md) · **05 · Failure modes** · [06 · Reading the TRACE output →](06-reading-the-trace.md)
# 05 · Failure modes
Six ways a filter chain goes wrong. Every one of them is reproducible in this module and every
transcript below is a real run.
## An authentication filter after `AuthorizationFilter`
Profile `misordered`, transcript [`demo7-misordered.txt`](output/demo7-misordered.txt).
The same `ApiKeyAuthenticationFilter` as the working `custom` profile, anchored to
`AuthorizationFilter` instead of `LogoutFilter` — order 4201 instead of 1201:
```
11/12 AuthorizationFilter order=4200
12/12 ApiKeyAuthenticationFilter order=? (not in the registration table)
$ curl -i -H "X-Api-Key: let-me-in" localhost:8080/whoami
HTTP/1.1 401
```
The key is valid. The filter is in the chain and demonstrably executes — it is the last thing
that runs. It sets the `SecurityContext` half a millisecond after the authorization decision was
already taken against the anonymous principal.
**Fingerprint:** a 401 or 403 with correct credentials, no exception, and TRACE showing your
filter running *after* `AuthorizationFilter (n/n)`. Anchor authentication filters to
`LogoutFilter`.
## A denial that comes back as 500
Profile `custom`, transcript [`demo6-exception-translation.txt`](output/demo6-exception-translation.txt).
`ExceptionTranslationFilter` (4000) is what turns `AccessDeniedException` into a 403 and
`AuthenticationException` into a 401 or a redirect. It does that by wrapping the *rest of the
chain* in a try/catch. A filter that throws from **below** 4000 is not inside that try block:
```
$ curl -i -u alice:password localhost:8080/tenant/doc # filter at order 3701
HTTP/1.1 500
$ curl -i -u alice:password localhost:8080/tenant/translated # filter at order 4001
HTTP/1.1 403
```
Identical filter code. Identical exception. 300 order slots apart.
3701 is the placement the reference documentation's own `TenantFilter` example uses
(`addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class)`), and the example
throws `AccessDeniedException`. If you follow it literally you get a 500.
**Fingerprint:** a stack trace in the container log with `AccessDeniedException` at the top and
no Spring Security frames below your own filter.
## The second instance of one `OncePerRequestFilter` subclass never runs
This one cost a test failure to find, and it is the reason the transcript above needs a JVM flag.
`OncePerRequestFilter` guards against double execution with a request attribute named
`getFilterName() + ".FILTERED"`. For a filter that is not a Spring bean, `getFilterName()` falls
back to the **class name**. Two instances of the same subclass therefore answer the same
attribute name, and the second one to run finds it already set and skips itself entirely:
```
$ curl -i -u alice:password localhost:8080/tenant/translated # filter at order 4001
HTTP/1.1 200 <- with both TenantFilters sharing one key
HTTP/1.1 403 <- with -DUNIQUE_ONCE_KEY=true
```
Both filters are in the chain either way — [`demo5-custom-placement.txt`](output/demo5-custom-placement.txt)
lists `TenantFilter` at positions 17 and 19. One of them is inert. There is no log line for it
at any level.
**Fingerprint:** a filter that is visibly in the chain and visibly does nothing. Override
`getAlreadyFilteredAttributeName()`, or give each instance its own class, or register them as
beans with distinct names.
## A custom filter that runs twice
Profile `doublereg`, transcript [`demo9-double-registration.txt`](output/demo9-double-registration.txt).
Spring Boot registers every `jakarta.servlet.Filter` **bean** with the servlet container. Add
the same bean to the security chain and it is in two chains at once:
```
--- profile: doublereg ---
X-Counting-Filter-Invocations: 2
countingFilter com.ankurm.chain.filter.CountingFilter urls=[/*]
--- profile: doublereg,fixed (FilterRegistrationBean.setEnabled(false)) ---
X-Counting-Filter-Invocations: 1
(not registered with the container)
```
Extending `OncePerRequestFilter` **hides** this rather than fixing it: the second pass
short-circuits, the counter reads 1, and you conclude there is no problem. The filter is still
registered twice, still wraps every request the container serves — including paths no
`SecurityFilterChain` matches, and including the internal `/error` dispatch.
**Fix:** declare a `FilterRegistrationBean` for it with `setEnabled(false)`, or do not make the
filter a bean at all and construct it inline in the `SecurityFilterChain` method.
## Two chains where the broad one shadows the narrow one
Profile `multichain`, transcript [`demo10-multichain.txt`](output/demo10-multichain.txt).
`FilterChainProxy` invokes the **first** matching chain and stops. A chain matching `any request`
declared ahead of a chain matching `/api/**` makes the second one dead code.
`WebSecurityFilterChainValidator` catches the blatant version of this at startup and throws
`UnreachableFilterChainException`:
> A filter chain that matches any request [`…`] has already been configured, which means that
> this filter chain [`…`] will never get invoked. Please use `HttpSecurity#securityMatcher` to
> ensure that there is only one filter chain configured for 'any request' and that the 'any
> request' filter chain is published last.
(Message read out of the class's constant pool. The validator and the exception are present in
both 6.5.1 and 7.1.1, so this is not a 7.x change.) It runs three checks —
`checkForAnyRequestRequestMatcher`, `checkForDuplicateMatchers`, `checkAuthorizationFilters`
and none of them catch the common case: two *specific* matchers that overlap partially, where
the broader one is declared first. Nothing warns about that.
Order chains explicitly with `@Order`, narrowest first. A bean with no `@Order` gets
`Ordered.LOWEST_PRECEDENCE`, and two of those have no defined relative order at all.
## `ignoring()` where you meant `permitAll()`
Profile `ignoring`, transcript [`demo11-ignoring-vs-permitall.txt`](output/demo11-ignoring-vs-permitall.txt).
```
=== chain 1/2 matcher = PathPattern [/static/**] (0 filters)
<no filters> - this chain does NOTHING.
```
A `SecurityFilterChain` with zero filters is a real, matched chain that runs nothing. The
response proves it:
```
$ curl -sD- -o /dev/null localhost:8080/static/asset.txt
HTTP/1.1 200
<- no security headers at all
$ curl -sD- -o /dev/null localhost:8080/public/hello
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
```
Spring Security itself warns about this at startup:
```
WARN o.s.s.c.a.web.builders.WebSecurity : You are asking Spring Security to ignore
PathPattern [/static/**]. This is not recommended -- please use permitAll via
HttpSecurity#authorizeHttpRequests instead.
```
`ignoring()` is worth it only when the path genuinely serves static bytes and the header cost
matters. Anything behind it is outside Spring Security, including a path traversal that resolves
somewhere interesting.
---
[← 04 · Where custom filters land](04-where-custom-filters-land.md) · **05 · Failure modes** · [06 · Reading the TRACE output →](06-reading-the-trace.md)

View File

@@ -0,0 +1,163 @@
[← 05 · Failure modes](05-failure-modes.md) · **06 · Reading the TRACE output** · [07 · Multiple chains →](07-multiple-chains.md)
# 06 · Reading the TRACE output
```properties
logging.level.org.springframework.security=TRACE
```
That one line is worth more than every diagram of the filter chain, including the ones in this
repository. It is also actively misleading in three specific ways, and this chapter is mostly
about those.
The full transcripts are [`demo3-trace-authenticated.txt`](output/demo3-trace-authenticated.txt)
(a request that succeeds) and [`demo4-trace-csrf-403.txt`](output/demo4-trace-csrf-403.txt)
(one that does not).
## The five lines that matter
**Chain selection.** Before anything runs:
```
TRACE FilterChainProxy : Trying to match request against DefaultSecurityFilterChain defined as
'baseline' in [class path resource [com/ankurm/chain/config/BaselineSecurityConfig.class]]
matching [any request] and having filters [DisableEncodeUrl, WebAsyncManagerIntegration, …] (1/1)
```
This is the single most useful line in the file and almost nobody knows it exists. It names the
**bean** and the **class that declared it**. If you have four `SecurityFilterChain` beans and a
request is behaving as though it hit the wrong one, this line settles it in one grep. The
trailing `(1/1)` is the chain's position in `FilterChainProxy`'s list.
**Entry.** `DEBUG FilterChainProxy : Securing GET /whoami`
**Each filter.** `TRACE FilterChainProxy : Invoking CsrfFilter (5/16)`
The `(n/m)` counters are positions in the same list `/diag/chains` prints, so the two line up
exactly. A filter that never appears here is not in the chain.
**Exit.** `DEBUG FilterChainProxy : Secured GET /whoami` — meaning the chain completed and
handed off to the servlet. It does **not** mean the request succeeded.
**Startup.** Once per chain, at DEBUG, on `DefaultSecurityFilterChain`:
```
DEBUG o.s.s.web.DefaultSecurityFilterChain : Will secure any request with filters:
DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, …
```
Note the wording: `with filters: …`, not `with [ … ]`. The bracketed form appears in the
reference documentation and in most blog posts; it is not what 7.1.1 prints, and it is not what
6.5.1 printed either.
## Misleading thing 1 · the log is not a timeline
Look at the end of the successful request:
```
TRACE FilterChainProxy : Invoking AnonymousAuthenticationFilter (14/16)
TRACE FilterChainProxy : Invoking ExceptionTranslationFilter (15/16)
TRACE FilterChainProxy : Invoking AuthorizationFilter (16/16)
TRACE RequestMatcherDelegatingAuthorizationManager : Authorizing GET /whoami
TRACE RequestMatcherDelegatingAuthorizationManager : Checking authorization on GET /whoami …
TRACE AnonymousAuthenticationFilter : Did not set SecurityContextHolder since already authenticated …
```
`AnonymousAuthenticationFilter` logs its decision **two filters after it was invoked**. It is not
a threading artefact. The filter installs a lazily-resolved `Supplier<SecurityContext>` and the
supplier only runs when something asks for the authentication — which, here, is
`AuthorizationFilter` at 16/16. The log line fires at resolution time.
The same effect appears earlier and matters more:
```
TRACE FilterChainProxy : Invoking SecurityContextHolderFilter (3/16)
TRACE FilterChainProxy : Invoking BasicAuthenticationFilter (11/16)
TRACE BasicAuthenticationFilter : Found username 'alice' in Basic Authorization header
TRACE HttpSessionSecurityContextRepository : No HttpSession currently exists
TRACE SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
```
The session lookup is logged under filter **11**, not filter 3. `SecurityContextHolderFilter`
installed the supplier; `BasicAuthenticationFilter` was the first code to dereference it. If you
are reading the log to work out *when* the session was touched, filter 3 is the wrong answer.
## Misleading thing 2 · `SupplierDeferredSecurityContext : Created …` appears twice
Every request logs that line two or three times. It is not two contexts and it is not a bug —
`SupplierDeferredSecurityContext` logs on each `getContext()` that finds nothing stored. Do not
read it as evidence that something is creating contexts repeatedly.
## Misleading thing 3 · the rejected request runs the chain twice
The 403 transcript is the important one. After `CsrfFilter` rejects:
```
DEBUG CsrfFilter : Invalid CSRF token found for http://localhost:8080/hello
DEBUG AccessDeniedHandlerImpl : Responding with 403 status code
DEBUG o.a.c.c.C.[Tomcat].[localhost] : Processing ErrorPage[errorCode=0, location=/error]
TRACE FilterChainProxy : Trying to match request against DefaultSecurityFilterChain defined as 'baseline' …
DEBUG FilterChainProxy : Securing GET /error
TRACE FilterChainProxy : Invoking DisableEncodeUrlFilter (1/16)
TRACE FilterChainProxy : Invoking AuthorizationFilter (16/16)
TRACE RequestMatcherDelegatingAuthorizationManager : Authorizing GET /error
```
The container dispatches to `/error`, and the **entire sixteen-filter chain runs again** for that
dispatch. Two things about the second pass:
- **Half the filters invoke and then do nothing.** `OncePerRequestFilter.doFilter` starts with
`skipDispatch(request)`, which returns `true` when the request carries the
`jakarta.servlet.error.request_uri` attribute and `shouldNotFilterErrorDispatch()` says so —
and that method returns `true` by default. Of the sixteen default filters, six extend
`OncePerRequestFilter` (`DisableEncodeUrlFilter`, `WebAsyncManagerIntegrationFilter`,
`HeaderWriterFilter`, `CsrfFilter`, `DefaultLogoutPageGeneratingFilter`,
`BasicAuthenticationFilter`) and pass straight through. The other ten extend
`GenericFilterBean` and run in full. You can see it in the transcript: on the second pass
`LogoutFilter` still logs `Did not match request to PathPattern [POST /logout]`, while
`BasicAuthenticationFilter (11/16)` logs nothing at all.
- **So the error dispatch is authorized, but not authenticated.** `AuthorizationFilter` extends
`GenericFilterBean` and runs; `BasicAuthenticationFilter` extends `OncePerRequestFilter` and
does not. `GET /error` is therefore matched against `authorizeHttpRequests` on its own merits
as an **anonymous** request, and under `.anyRequest().authenticated()` it is denied. The trace
ends with `ExceptionTranslationFilter : Sending AnonymousAuthenticationToken … to
authentication entry point since access is denied` and an `AuthorizationDeniedException`
stack trace.
- The `(1/16) … (16/16)` counters restart, so a casual read of the log looks like the original
request ran twice.
This is the source of a whole family of confusing reports: a 403 that logs a 401 underneath it,
an `/error` page that itself 403s, a custom filter that "runs twice" for failed requests only.
Permitting `/error` explicitly is the usual fix, and it is worth doing deliberately rather than
discovering it.
## Stack traces are twice as tall as you expect
```
at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:99)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(…)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(…)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(…)
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(…)
```
Three decorator frames between every pair of real filters. `FilterChainProxy` decorates the chain
with `ObservationFilterChainDecorator` whenever a usable `ObservationRegistry` bean exists — which
in Boot means whenever the actuator starter is present. Without it you get the plain
`VirtualFilterChain` and shorter traces. Nothing is wrong; the frames are just noise, and knowing
they are noise makes the trace readable.
## What to grep
```bash
grep 'Securing\|Secured' app.log # request boundaries
grep 'Trying to match request against' app.log # which chain, and which bean declared it
grep 'Invoking' app.log | tail -30 # the chain as it actually ran
grep -A2 'Invalid CSRF\|Access is denied\|Failed to' app.log
```
---
[← 05 · Failure modes](05-failure-modes.md) · **06 · Reading the TRACE output** · [07 · Multiple chains →](07-multiple-chains.md)

View File

@@ -0,0 +1,71 @@
[← 06 · Reading the TRACE output](06-reading-the-trace.md) · **07 · Multiple chains** · [08 · Debugging recipes →](08-debugging-recipes.md)
# 07 · Multiple chains
`FilterChainProxy` holds a list. It walks it in order, asks each chain `matches(request)`, and
invokes **the first one that says yes**. There is no fall-through and no combining.
The `multichain` profile declares three, and
[`demo10-multichain.txt`](output/demo10-multichain.txt) shows what they cost:
| Chain | `@Order` | Matcher | Filters |
|---|---|---|---|
| API | 1 | `/api/**` | 12 |
| Diagnostics / public | 2 | `/diag/**`, `/public/**` | 10 |
| Browser | 3 | any request | 15 |
## Shorter is the point
The API chain drops `CsrfFilter`, `UsernamePasswordAuthenticationFilter`,
`DefaultResourcesFilter`, `DefaultLoginPageGeneratingFilter` and
`DefaultLogoutPageGeneratingFilter` — five filters that have no meaning for a token client and
five filters that cannot surprise you at three in the morning. A stateless API served by the
browser chain is the origin of most "why is my POST getting a 403" questions.
One counter-intuitive detail: asking for `SessionCreationPolicy.STATELESS` **adds** a filter.
`SessionManagementFilter` (3900) is absent from the default chain and appears only when you
configure `sessionManagement(…)` at all. Statelessness is enforced by a filter, not by an absence
of one.
## Ordering
`@Order` on the bean fixes evaluation order. Narrowest matcher first; the `any request` chain
last. A bean with no `@Order` gets `Ordered.LOWEST_PRECEDENCE`, and two such beans have no
defined order relative to each other — which makes the behaviour depend on bean-definition
ordering, which depends on classpath scanning order.
`WebSecurityFilterChainValidator` catches the blatant failure at startup and throws
`UnreachableFilterChainException` when an `any request` chain is declared before another chain.
It does not catch two specific matchers that partially overlap. See
[05 · Failure modes](05-failure-modes.md#two-chains-where-the-broad-one-shadows-the-narrow-one).
## `securityMatcher` is not `requestMatchers`
They read similarly and do different jobs, and mixing them up produces a chain that is either
inert or catches everything:
- **`http.securityMatcher(…)`** — decides whether **this chain** handles the request at all. It
is the chain's `RequestMatcher`, evaluated by `FilterChainProxy` before any filter runs.
- **`authorizeHttpRequests(a -> a.requestMatchers(…)…)`** — decides what
`AuthorizationFilter` does **once the chain is already running**.
A chain with no `securityMatcher` matches any request. Two of those and the second is dead.
## `ignoring()` versus `permitAll()`
[`demo11-ignoring-vs-permitall.txt`](output/demo11-ignoring-vs-permitall.txt) puts them side by
side. `WebSecurity.ignoring()` produces a genuine `SecurityFilterChain` with **zero filters**:
```
=== chain 1/2 matcher = PathPattern [/static/**] (0 filters)
<no filters> - this chain does NOTHING.
```
and the response carries no `X-Content-Type-Options`, no cache headers, nothing. `permitAll()`
runs all sixteen filters and then authorizes. Spring Security warns about `ignoring()` at
startup and the warning is worth heeding: the saving is a few microseconds, and the cost is that
the path is outside Spring Security entirely — including the `HttpFirewall`.
---
[← 06 · Reading the TRACE output](06-reading-the-trace.md) · **07 · Multiple chains** · [08 · Debugging recipes →](08-debugging-recipes.md)

View File

@@ -0,0 +1,101 @@
[← 07 · Multiple chains](07-multiple-chains.md) · **08 · Debugging recipes** · [09 · Testing the chain →](09-testing-the-chain.md)
# 08 · Debugging recipes
Ordered by how often they settle the question.
## 1 · Print the live chain
[`DiagnosticsController`](../src/main/java/com/ankurm/chain/web/DiagnosticsController.java) is
about forty lines and answers most filter questions outright:
```java
@GetMapping(value = "/diag/chains", produces = "text/plain")
public String chains() {
for (SecurityFilterChain chain : this.filterChainProxy.getFilterChains()) {
// chain.getFilters() is the real, ordered list
}
}
```
It reads the live `FilterChainProxy` bean, so it cannot disagree with reality the way a diagram
can. Copy it into any application you are debugging.
**Delete it before shipping.** It publishes your entire security topology, including which paths
have which chains and how many filters each one has.
## 2 · Turn on TRACE and read the chain-selection line
```properties
logging.level.org.springframework.security=TRACE
```
```bash
grep 'Trying to match request against' app.log
```
That line names the bean and the class that declared the chain that served the request. When a
request is behaving as though a different chain handled it, this is the fastest way to find out
that it did. [06 · Reading the TRACE output](06-reading-the-trace.md) covers the rest, including
three ways the log misleads.
## 3 · Read the startup line before you read anything else
```bash
grep 'Will secure' app.log
```
One line per chain, printed at DEBUG on `DefaultSecurityFilterChain`, listing every filter in
order. If a filter you added is not in that line, nothing else you investigate matters.
## 4 · Ask what order a filter would get
```java
FilterOrderTable.orderOf(CsrfFilter.class); // 1100
FilterOrderTable.orderIfAddedBefore(CsrfFilter.class); // 1099
```
[`FilterOrderTable`](../src/main/java/com/ankurm/chain/support/FilterOrderTable.java) reflects
the real `FilterOrderRegistration`. `GET /diag/order` prints the whole table, marking slots whose
class is not on the classpath. Useful when you are choosing an anchor and want to know what sits
between it and the next real filter.
## 5 · Check for double registration
```bash
curl -s localhost:8080/diag/servlet-filters
```
Any custom filter that appears in both this list and `/diag/chains` runs twice.
[05 · Failure modes](05-failure-modes.md#a-custom-filter-that-runs-twice) has the fix.
## 6 · When a filter is in the chain and does nothing
Two candidates, in order of likelihood:
1. It extends `OncePerRequestFilter` and something else with the same
`getAlreadyFilteredAttributeName()` already ran — usually another instance of the same class.
2. It extends `OncePerRequestFilter` and the request is an `ERROR` or `ASYNC` dispatch, where
`shouldNotFilterErrorDispatch()` / `shouldNotFilterAsyncDispatch()` default to skipping.
Neither logs anything at any level. Put a `System.out.println` at the top of `doFilterInternal`
before concluding the filter is not in the chain — because it is.
## 7 · The symptom table
| Symptom | Look at |
|---|---|
| 401/403 with correct credentials | Is your auth filter after `AuthorizationFilter`? [05](05-failure-modes.md#an-authentication-filter-after-authorizationfilter) |
| 500 where you expected 403 | Are you throwing below `ExceptionTranslationFilter` (4000)? [05](05-failure-modes.md#a-denial-that-comes-back-as-500) |
| Filter runs twice | Is it a `@Bean`? [05](05-failure-modes.md#a-custom-filter-that-runs-twice) |
| Filter never runs, but is in the chain | `OncePerRequestFilter` key collision. [05](05-failure-modes.md#the-second-instance-of-one-onceperrequestfilter-subclass-never-runs) |
| Log seems to show the request twice | The `/error` dispatch. [06](06-reading-the-trace.md#misleading-thing-3--the-rejected-request-runs-the-chain-twice) |
| A path gets no security headers | `ignoring()`. [07](07-multiple-chains.md#ignoring-versus-permitall) |
| The wrong chain handled it | `grep 'Trying to match request against'` |
| `RequestRejectedException` from nowhere | `HttpFirewall`, before any chain. [01](01-the-two-proxies.md#filterchainproxy) |
| Two custom filters in the wrong order | Same anchor, same order number. [04](04-where-custom-filters-land.md#4--two-filters-on-the-same-anchor-get-the-same-number) |
| `FilterSecurityInterceptor` will not compile | Removed in 7.0; use `AuthorizationFilter`. [03](03-the-order-table.md#two-slots-name-classes-that-no-longer-exist) |
---
[← 07 · Multiple chains](07-multiple-chains.md) · **08 · Debugging recipes** · [09 · Testing the chain →](09-testing-the-chain.md)

View File

@@ -0,0 +1,83 @@
[← 08 · Debugging recipes](08-debugging-recipes.md) · **09 · Testing the chain** · [docs index →](README.md)
# 09 · Testing the chain
Twenty-one assertions, in
[`FilterOrderTableTest`](../src/test/java/com/ankurm/chain/FilterOrderTableTest.java) and
[`FilterChainContractTest`](../src/test/java/com/ankurm/chain/FilterChainContractTest.java).
Output in [`docs/output/tests.txt`](output/tests.txt).
## Assert the chain, not the happy path
```java
@Autowired FilterChainProxy proxy;
assertThat(proxy.getFilterChains().get(0).getFilters().stream()
.map((filter) -> filter.getClass().getSimpleName()).toList())
.containsExactly("DisableEncodeUrlFilter", , "AuthorizationFilter");
```
`containsExactly` on the whole list is deliberately brittle. A Spring Security upgrade that adds
or removes a filter should fail this test loudly rather than change behaviour quietly — that is
what the test is for. `containsSubsequence` is the right tool for a claim about *relative*
placement:
```java
assertThat(names).containsSubsequence("LogoutFilter", "ApiKeyAuthenticationFilter",
"UsernamePasswordAuthenticationFilter");
```
## One nested class per profile
Profiles are fixed at context startup, so a scenario is a `@Nested` class with its own
`@ActiveProfiles`. Spring's test context caching means the seven contexts in this suite are each
built once and the whole run takes about eight seconds.
```java
@Nested @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("misordered")
class Misordered { }
```
Boot 4 moved the test slices: `@AutoConfigureMockMvc` is
`org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc` and lives in
`spring-boot-starter-webmvc-test`; the security post-processors need
`spring-boot-starter-security-test`. `spring-boot-starter-test` alone is not enough.
## Assert the number, not the position
```java
assertThat(FilterOrderTable.orderIfAddedBefore(CsrfFilter.class)).isEqualTo(1099);
```
Position in the chain moves whenever anything else is added. The order *number* is a property of
the framework and only moves on an upgrade — which is exactly when you want to be told.
## Assert what is absent
```java
assertThat(names).doesNotContain("SessionManagementFilter", "SecurityContextPersistenceFilter");
assertThat(FilterOrderTable.isOnClasspath(
"org.springframework.security.web.access.intercept.FilterSecurityInterceptor")).isFalse();
```
Half the useful facts about the default chain are negative ones.
## MockMvc shows you the throw, not the status
The 500 in [`demo6`](output/demo6-exception-translation.txt) has no MockMvc equivalent, because
MockMvc has no container to render an error page. It rethrows instead, which is the same fact
from the other side:
```java
assertThat(catchThrowable(() -> mvc.perform(get("/tenant/doc").with(user()))))
.isInstanceOf(AccessDeniedException.class)
.hasMessageContaining("doc-placement");
```
Note `isInstanceOf`, not `rootCause().isInstanceOf` — the exception arrives with no cause, having
been thrown by a filter and never wrapped. Getting that wrong is what surfaced the finding in
[05](05-failure-modes.md#the-second-instance-of-one-onceperrequestfilter-subclass-never-runs).
---
[← 08 · Debugging recipes](08-debugging-recipes.md) · **09 · Testing the chain** · [docs index →](README.md)

View File

@@ -0,0 +1,39 @@
# Documentation index — `filter-chain`
Nine chapters, in order. Each links to the next; each links to the source files and the captured
output it draws on. Nothing here is asserted without a file behind it.
| # | Chapter | What it answers |
|---|---|---|
| 01 | [The two proxies](01-the-two-proxies.md) | Why Spring Security is one servlet filter, and what `DelegatingFilterProxy`, `FilterChainProxy` and `SecurityFilterChain` each do |
| 02 | [The default chain](02-the-default-chain.md) | The sixteen filters of the reference configuration, one by one, and what breaks without each |
| 03 | [The order table](03-the-order-table.md) | `FilterOrderRegistration`: every slot from 100 to 4300, the two reserved gaps, and the two slots naming classes that no longer exist |
| 04 | [Where custom filters land](04-where-custom-filters-land.md) | `addFilterBefore` / `After` / `At` / `addFilter`, the ±1 offset, superclass resolution, and ties |
| 05 | [Failure modes](05-failure-modes.md) | Six ways a chain goes wrong, each reproducible here |
| 06 | [Reading the TRACE output](06-reading-the-trace.md) | The five lines that matter and the three ways the log misleads |
| 07 | [Multiple chains](07-multiple-chains.md) | Matching, ordering, `securityMatcher` vs `requestMatchers`, `ignoring()` vs `permitAll()` |
| 08 | [Debugging recipes](08-debugging-recipes.md) | Seven recipes and a symptom → chapter table |
| 09 | [Testing the chain](09-testing-the-chain.md) | How the twenty-one assertions are written and why |
## Captured output
Every file below is regenerated by [`../scripts/run-all.sh`](../scripts/run-all.sh) from a real
run. None of it is typed by hand.
| File | Scenario |
|---|---|
| [`demo1-order-table.txt`](output/demo1-order-table.txt) | `FilterOrderRegistration` reflected out of the jar |
| [`demo2-default-chain.txt`](output/demo2-default-chain.txt) | The live 16-filter chain, plus the startup DEBUG line |
| [`demo3-trace-authenticated.txt`](output/demo3-trace-authenticated.txt) | TRACE for one authenticated `GET` |
| [`demo4-trace-csrf-403.txt`](output/demo4-trace-csrf-403.txt) | TRACE for a CSRF rejection, including the `/error` re-dispatch |
| [`demo5-custom-placement.txt`](output/demo5-custom-placement.txt) | Four custom filters at four anchors |
| [`demo6-exception-translation.txt`](output/demo6-exception-translation.txt) | 500 vs 403 across the `ExceptionTranslationFilter` boundary |
| [`demo7-misordered.txt`](output/demo7-misordered.txt) | An authentication filter one slot too late |
| [`demo8-tie.txt`](output/demo8-tie.txt) | Two filters, one anchor, one order number |
| [`demo9-double-registration.txt`](output/demo9-double-registration.txt) | A filter bean in two chains at once |
| [`demo10-multichain.txt`](output/demo10-multichain.txt) | Three chains, three lengths |
| [`demo11-ignoring-vs-permitall.txt`](output/demo11-ignoring-vs-permitall.txt) | A chain with zero filters |
| [`demo12-servlet-filters.txt`](output/demo12-servlet-filters.txt) | What the servlet container sees |
| [`tests.txt`](output/tests.txt) | The 21 assertions |
[← back to the module README](../README.md)

View File

@@ -0,0 +1,52 @@
==============================================================================
docs/output/demo1-order-table.txt
FilterOrderRegistration, read out of spring-security-config 7.1.1 by reflection.
GET /diag/order
==============================================================================
FilterOrderRegistration, reflected out of spring-security-config.
Slots are 100 apart, starting at 100. addFilterBefore = slot-1, addFilterAfter = slot+1.
100 org.springframework.security.web.session.DisableEncodeUrlFilter
200 org.springframework.security.web.session.ForceEagerSessionCreationFilter
300 org.springframework.security.web.access.channel.ChannelProcessingFilter <-- CLASS NOT ON CLASSPATH
400 org.springframework.security.web.transport.HttpsRedirectFilter
500 -- reserved, nothing registered --
600 org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
700 org.springframework.security.web.context.SecurityContextHolderFilter
800 org.springframework.security.web.context.SecurityContextPersistenceFilter
900 org.springframework.security.web.header.HeaderWriterFilter
1000 org.springframework.web.filter.CorsFilter
1100 org.springframework.security.web.csrf.CsrfFilter
1200 org.springframework.security.web.authentication.logout.LogoutFilter
1300 org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter <-- CLASS NOT ON CLASSPATH
1400 org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter <-- CLASS NOT ON CLASSPATH
1500 org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter
1600 org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter
1700 org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter
1800 org.springframework.security.cas.web.CasAuthenticationFilter <-- CLASS NOT ON CLASSPATH
1900 org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter <-- CLASS NOT ON CLASSPATH
2000 org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter <-- CLASS NOT ON CLASSPATH
2100 org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
2200 org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationFilter
2300 -- reserved, nothing registered --
2400 org.springframework.security.web.authentication.ui.DefaultResourcesFilter
2500 org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
2600 org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
2700 org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter
2800 org.springframework.security.web.session.ConcurrentSessionFilter
2900 org.springframework.security.web.authentication.www.DigestAuthenticationFilter
3000 org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter <-- CLASS NOT ON CLASSPATH
3100 org.springframework.security.web.authentication.www.BasicAuthenticationFilter
3200 org.springframework.security.web.authentication.AuthenticationFilter
3300 org.springframework.security.web.savedrequest.RequestCacheAwareFilter
3400 org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
3500 org.springframework.security.web.jaasapi.JaasApiIntegrationFilter
3600 org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter
3700 org.springframework.security.web.authentication.AnonymousAuthenticationFilter
3800 org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter <-- CLASS NOT ON CLASSPATH
3900 org.springframework.security.web.session.SessionManagementFilter
4000 org.springframework.security.web.access.ExceptionTranslationFilter
4100 org.springframework.security.web.access.intercept.FilterSecurityInterceptor <-- CLASS NOT ON CLASSPATH
4200 org.springframework.security.web.access.intercept.AuthorizationFilter
4300 org.springframework.security.web.authentication.switchuser.SwitchUserFilter

View File

@@ -0,0 +1,57 @@
==============================================================================
docs/output/demo10-multichain.txt
Three SecurityFilterChain beans. Evaluation stops at the first match.
GET /diag/chains (profile: multichain)
==============================================================================
FilterChainProxy holds 3 SecurityFilterChain(s).
The FIRST chain whose matcher accepts the request wins; the rest are never consulted.
=== chain 1/3 matcher = Or [PathPattern [/api/**]] (12 filters)
1/12 DisableEncodeUrlFilter order=100
2/12 WebAsyncManagerIntegrationFilter order=600
3/12 SecurityContextHolderFilter order=700
4/12 HeaderWriterFilter order=900
5/12 LogoutFilter order=1200
6/12 BasicAuthenticationFilter order=3100
7/12 RequestCacheAwareFilter order=3300
8/12 SecurityContextHolderAwareRequestFilter order=3400
9/12 AnonymousAuthenticationFilter order=3700
10/12 SessionManagementFilter order=3900
11/12 ExceptionTranslationFilter order=4000
12/12 AuthorizationFilter order=4200
=== chain 2/3 matcher = Or [PathPattern [/diag/**], PathPattern [/public/**]] (10 filters)
1/10 DisableEncodeUrlFilter order=100
2/10 WebAsyncManagerIntegrationFilter order=600
3/10 SecurityContextHolderFilter order=700
4/10 HeaderWriterFilter order=900
5/10 LogoutFilter order=1200
6/10 RequestCacheAwareFilter order=3300
7/10 SecurityContextHolderAwareRequestFilter order=3400
8/10 AnonymousAuthenticationFilter order=3700
9/10 ExceptionTranslationFilter order=4000
10/10 AuthorizationFilter order=4200
=== chain 3/3 matcher = any request (15 filters)
1/15 DisableEncodeUrlFilter order=100
2/15 WebAsyncManagerIntegrationFilter order=600
3/15 SecurityContextHolderFilter order=700
4/15 HeaderWriterFilter order=900
5/15 CsrfFilter order=1100
6/15 LogoutFilter order=1200
7/15 UsernamePasswordAuthenticationFilter order=2100
8/15 DefaultResourcesFilter order=2400
9/15 DefaultLoginPageGeneratingFilter order=2500
10/15 DefaultLogoutPageGeneratingFilter order=2600
11/15 RequestCacheAwareFilter order=3300
12/15 SecurityContextHolderAwareRequestFilter order=3400
13/15 AnonymousAuthenticationFilter order=3700
14/15 ExceptionTranslationFilter order=4000
15/15 AuthorizationFilter order=4200
--- which chain served what ---
$ curl -i -u alice:password localhost:8080/api/data
HTTP/1.1 200
$ curl -i localhost:8080/whoami # browser chain: redirect to the login page
HTTP/1.1 302

View File

@@ -0,0 +1,38 @@
==============================================================================
docs/output/demo11-ignoring-vs-permitall.txt
WebSecurity.ignoring() produces a real chain with ZERO filters.
GET /diag/chains (profile: ignoring)
==============================================================================
FilterChainProxy holds 2 SecurityFilterChain(s).
The FIRST chain whose matcher accepts the request wins; the rest are never consulted.
=== chain 1/2 matcher = PathPattern [/static/**] (0 filters)
<no filters> - this chain does NOTHING. Requests it matches are
unauthenticated, unauthorised, and get no security headers.
=== chain 2/2 matcher = any request (12 filters)
1/12 DisableEncodeUrlFilter order=100
2/12 WebAsyncManagerIntegrationFilter order=600
3/12 SecurityContextHolderFilter order=700
4/12 HeaderWriterFilter order=900
5/12 CsrfFilter order=1100
6/12 LogoutFilter order=1200
7/12 BasicAuthenticationFilter order=3100
8/12 RequestCacheAwareFilter order=3300
9/12 SecurityContextHolderAwareRequestFilter order=3400
10/12 AnonymousAuthenticationFilter order=3700
11/12 ExceptionTranslationFilter order=4000
12/12 AuthorizationFilter order=4200
--- response headers, ignored path vs permitAll path ---
$ curl -sD- -o /dev/null localhost:8080/static/asset.txt
HTTP/1.1 200
$ curl -sD- -o /dev/null localhost:8080/public/hello
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0

View File

@@ -0,0 +1,17 @@
==============================================================================
docs/output/demo12-servlet-filters.txt
What the servlet container has registered. FilterChainProxy is ONE entry here.
GET /diag/servlet-filters (profile: baseline)
==============================================================================
Filters registered with the servlet container:
Tomcat WebSocket (JSR356) Filter org.apache.tomcat.websocket.server.WsFilter urls=[/*]
characterEncodingFilter org.springframework.boot.servlet.filter.OrderedCharacterEncodingFilter urls=[/*]
formContentFilter org.springframework.boot.servlet.filter.OrderedFormContentFilter urls=[/*]
requestContextFilter org.springframework.boot.servlet.filter.OrderedRequestContextFilter urls=[/*]
springSecurityFilterChain org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1 urls=[/*]
webMvcObservationFilter org.springframework.web.filter.ServerHttpObservationFilter urls=[/*]
Anything listed here runs OUTSIDE FilterChainProxy. A custom security filter that
appears both here and in /diag/chains will run twice for every request.

View File

@@ -0,0 +1,29 @@
==============================================================================
docs/output/demo2-default-chain.txt
The chain FilterChainProxy actually holds for the reference configuration.
GET /diag/chains (profile: baseline)
==============================================================================
FilterChainProxy holds 1 SecurityFilterChain(s).
The FIRST chain whose matcher accepts the request wins; the rest are never consulted.
=== chain 1/1 matcher = any request (16 filters)
1/16 DisableEncodeUrlFilter order=100
2/16 WebAsyncManagerIntegrationFilter order=600
3/16 SecurityContextHolderFilter order=700
4/16 HeaderWriterFilter order=900
5/16 CsrfFilter order=1100
6/16 LogoutFilter order=1200
7/16 UsernamePasswordAuthenticationFilter order=2100
8/16 DefaultResourcesFilter order=2400
9/16 DefaultLoginPageGeneratingFilter order=2500
10/16 DefaultLogoutPageGeneratingFilter order=2600
11/16 BasicAuthenticationFilter order=3100
12/16 RequestCacheAwareFilter order=3300
13/16 SecurityContextHolderAwareRequestFilter order=3400
14/16 AnonymousAuthenticationFilter order=3700
15/16 ExceptionTranslationFilter order=4000
16/16 AuthorizationFilter order=4200
--- and the same list as Spring Security prints it at startup (DEBUG) ---

View File

@@ -0,0 +1,53 @@
==============================================================================
docs/output/demo3-trace-authenticated.txt
One authenticated GET, org.springframework.security at TRACE.
curl -u alice:password localhost:8080/whoami
Container and bean-factory lines removed; nothing else edited.
==============================================================================
DEBUG o.a.c.authenticator.AuthenticatorBase : Security checking request GET /whoami
DEBUG o.a.c.authenticator.AuthenticatorBase : Not subject to any constraint
TRACE o.s.security.web.FilterChainProxy : Trying to match request against DefaultSecurityFilterChain defined as 'baseline' in [class path resource [com/ankurm/chain/config/BaselineSecurityConfig.class]] matching [any request] and having filters [DisableEncodeUrl, WebAsyncManagerIntegration, SecurityContextHolder, HeaderWriter, Csrf, Logout, UsernamePasswordAuthentication, DefaultResources, DefaultLoginPageGenerating, DefaultLogoutPageGenerating, BasicAuthentication, RequestCacheAware, SecurityContextHolderAwareRequest, AnonymousAuthentication, ExceptionTranslation, Authorization] (1/1)
DEBUG o.s.security.web.FilterChainProxy : Securing GET /whoami
TRACE o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/16)
TRACE o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/16)
TRACE o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/16)
TRACE o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/16)
TRACE o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/16)
TRACE o.s.s.w.c.CsrfTokenRequestAttributeHandler : Wrote a CSRF token to the following request attributes: [_csrf, org.springframework.security.web.csrf.CsrfToken]
TRACE o.s.security.web.csrf.CsrfFilter : Did not protect against CSRF since request did not match IsNotHttpMethod [TRACE, HEAD, GET, OPTIONS]
TRACE o.s.security.web.FilterChainProxy : Invoking LogoutFilter (6/16)
TRACE o.s.s.w.a.logout.LogoutFilter : Did not match request to PathPattern [POST /logout]
TRACE o.s.security.web.FilterChainProxy : Invoking UsernamePasswordAuthenticationFilter (7/16)
TRACE o.s.s.w.a.UsernamePasswordAuthenticationFilter : Did not match request to PathPattern [POST /login]
TRACE o.s.security.web.FilterChainProxy : Invoking DefaultResourcesFilter (8/16)
TRACE o.s.security.web.FilterChainProxy : Invoking DefaultLoginPageGeneratingFilter (9/16)
TRACE o.s.security.web.FilterChainProxy : Invoking DefaultLogoutPageGeneratingFilter (10/16)
TRACE o.s.s.w.a.u.DefaultLogoutPageGeneratingFilter : Did not render default logout page since request did not match [PathPattern [GET /logout]]
TRACE o.s.security.web.FilterChainProxy : Invoking BasicAuthenticationFilter (11/16)
TRACE o.s.s.w.a.www.BasicAuthenticationFilter : Found username 'alice' in Basic Authorization header
TRACE o.s.s.w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
TRACE o.s.s.w.c.SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
TRACE o.s.s.w.c.SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
TRACE o.s.s.authentication.ProviderManager : Authenticating request with DaoAuthenticationProvider (1/1)
DEBUG o.s.s.a.dao.DaoAuthenticationProvider : Authenticated user
DEBUG o.s.s.w.a.www.BasicAuthenticationFilter : Set SecurityContextHolder to UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=alice, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[ROLE_USER, FactorGrantedAuthority [authority=FACTOR_PASSWORD, issuedAt=<timestamp>]]]
TRACE o.s.security.web.FilterChainProxy : Invoking RequestCacheAwareFilter (12/16)
TRACE o.s.s.w.s.HttpSessionRequestCache : matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided
TRACE o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderAwareRequestFilter (13/16)
TRACE o.s.security.web.FilterChainProxy : Invoking AnonymousAuthenticationFilter (14/16)
TRACE o.s.security.web.FilterChainProxy : Invoking ExceptionTranslationFilter (15/16)
TRACE o.s.security.web.FilterChainProxy : Invoking AuthorizationFilter (16/16)
TRACE o.s.s.w.a.i.RequestMatcherDelegatingAuthorizationManager : Authorizing GET /whoami
TRACE o.s.s.w.a.i.RequestMatcherDelegatingAuthorizationManager : Checking authorization on GET /whoami using org.springframework.security.authorization.AuthenticatedAuthorizationManager@<hash>
TRACE o.s.s.w.a.AnonymousAuthenticationFilter : Did not set SecurityContextHolder since already authenticated UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=alice, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[ROLE_USER, FactorGrantedAuthority [authority=FACTOR_PASSWORD, issuedAt=<timestamp>]]]
DEBUG o.s.security.web.FilterChainProxy : Secured GET /whoami
TRACE o.s.web.servlet.DispatcherServlet : GET "/whoami", parameters={}, headers={masked} in DispatcherServlet 'dispatcherServlet'
TRACE o.s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.ankurm.chain.web.DemoController#whoami(HttpServletRequest)
TRACE o.s.web.method.HandlerMethod : Arguments: [SecurityContextHolderAwareRequestWrapper[ org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@<hash>]]
DEBUG o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor : Using 'text/plain', given [*/*] and supported [text/plain]
TRACE o.s.w.s.m.m.a.RequestResponseBodyMethodProcessor : Writing ["principal=alice authenticated=true authorities=[ROLE_USER, FactorGrantedAuthority [authority=FACTOR_PASSWORD, issuedAt=<timestamp>]] type=UsernamePasswordAuthenticationToken counting-filter-invocations=null
"]
TRACE o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
TRACE o.s.web.servlet.DispatcherServlet : No view rendering, null ModelAndView returned.
DEBUG o.s.web.servlet.DispatcherServlet : Completed 200 OK, headers={masked}

View File

@@ -0,0 +1,144 @@
==============================================================================
docs/output/demo4-trace-csrf-403.txt
One POST with no CSRF token. Note WHICH filter rejects it and how far the request got.
curl -X POST -u alice:password localhost:8080/hello
==============================================================================
DEBUG o.a.c.authenticator.AuthenticatorBase : Security checking request POST /hello
DEBUG o.a.c.authenticator.AuthenticatorBase : Not subject to any constraint
TRACE o.s.security.web.FilterChainProxy : Trying to match request against DefaultSecurityFilterChain defined as 'baseline' in [class path resource [com/ankurm/chain/config/BaselineSecurityConfig.class]] matching [any request] and having filters [DisableEncodeUrl, WebAsyncManagerIntegration, SecurityContextHolder, HeaderWriter, Csrf, Logout, UsernamePasswordAuthentication, DefaultResources, DefaultLoginPageGenerating, DefaultLogoutPageGenerating, BasicAuthentication, RequestCacheAware, SecurityContextHolderAwareRequest, AnonymousAuthentication, ExceptionTranslation, Authorization] (1/1)
DEBUG o.s.security.web.FilterChainProxy : Securing POST /hello
TRACE o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/16)
TRACE o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/16)
TRACE o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/16)
TRACE o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/16)
TRACE o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/16)
TRACE o.s.s.w.c.CsrfTokenRequestAttributeHandler : Wrote a CSRF token to the following request attributes: [_csrf, org.springframework.security.web.csrf.CsrfToken]
TRACE o.s.s.web.csrf.CsrfTokenRequestHandler : Did not find a CSRF token in the [X-CSRF-TOKEN] request header
TRACE o.s.s.web.csrf.CsrfTokenRequestHandler : Did not find a CSRF token in the [_csrf] request parameter
DEBUG o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/hello
DEBUG o.s.s.w.access.AccessDeniedHandlerImpl : Responding with 403 status code
TRACE o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
DEBUG o.a.c.c.C.[Tomcat].[localhost] : Processing ErrorPage[errorCode=0, location=/error]
TRACE o.s.security.web.FilterChainProxy : Trying to match request against DefaultSecurityFilterChain defined as 'baseline' in [class path resource [com/ankurm/chain/config/BaselineSecurityConfig.class]] matching [any request] and having filters [DisableEncodeUrl, WebAsyncManagerIntegration, SecurityContextHolder, HeaderWriter, Csrf, Logout, UsernamePasswordAuthentication, DefaultResources, DefaultLoginPageGenerating, DefaultLogoutPageGenerating, BasicAuthentication, RequestCacheAware, SecurityContextHolderAwareRequest, AnonymousAuthentication, ExceptionTranslation, Authorization] (1/1)
DEBUG o.s.security.web.FilterChainProxy : Securing GET /error
TRACE o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/16)
TRACE o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/16)
TRACE o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/16)
TRACE o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/16)
TRACE o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/16)
TRACE o.s.security.web.FilterChainProxy : Invoking LogoutFilter (6/16)
TRACE o.s.s.w.a.logout.LogoutFilter : Did not match request to PathPattern [POST /logout]
TRACE o.s.security.web.FilterChainProxy : Invoking UsernamePasswordAuthenticationFilter (7/16)
TRACE o.s.s.w.a.UsernamePasswordAuthenticationFilter : Did not match request to PathPattern [POST /login]
TRACE o.s.security.web.FilterChainProxy : Invoking DefaultResourcesFilter (8/16)
TRACE o.s.security.web.FilterChainProxy : Invoking DefaultLoginPageGeneratingFilter (9/16)
TRACE o.s.security.web.FilterChainProxy : Invoking DefaultLogoutPageGeneratingFilter (10/16)
TRACE o.s.security.web.FilterChainProxy : Invoking BasicAuthenticationFilter (11/16)
TRACE o.s.security.web.FilterChainProxy : Invoking RequestCacheAwareFilter (12/16)
TRACE o.s.s.w.s.HttpSessionRequestCache : matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided
TRACE o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderAwareRequestFilter (13/16)
TRACE o.s.security.web.FilterChainProxy : Invoking AnonymousAuthenticationFilter (14/16)
TRACE o.s.security.web.FilterChainProxy : Invoking ExceptionTranslationFilter (15/16)
TRACE o.s.security.web.FilterChainProxy : Invoking AuthorizationFilter (16/16)
TRACE o.s.s.w.a.i.RequestMatcherDelegatingAuthorizationManager : Authorizing GET /error
TRACE o.s.s.w.a.i.RequestMatcherDelegatingAuthorizationManager : Checking authorization on GET /error using org.springframework.security.authorization.AuthenticatedAuthorizationManager@<hash>
TRACE o.s.s.w.c.HttpSessionSecurityContextRepository : Did not find SecurityContext in HttpSession 6376951C7CF74BC73C543E50270726EA using the SPRING_SECURITY_CONTEXT session attribute
TRACE o.s.s.w.c.SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
TRACE o.s.s.w.c.SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
TRACE o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=6376951C7CF74BC73C543E50270726EA], Granted Authorities=[ROLE_ANONYMOUS]]
TRACE o.s.s.w.a.ExceptionTranslationFilter : Sending AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=6376951C7CF74BC73C543E50270726EA], Granted Authorities=[ROLE_ANONYMOUS]] to authentication entry point since access is denied
org.springframework.security.authorization.AuthorizationDeniedException: Access Denied
at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:99)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126)
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:181)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter.doFilter(DefaultLoginPageGeneratingFilter.java:246)
at org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter.doFilter(DefaultLoginPageGeneratingFilter.java:232)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.security.web.authentication.ui.DefaultResourcesFilter.doFilter(DefaultResourcesFilter.java:73)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:245)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:239)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:96)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82)
at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:230)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:243)
at org.springframework.security.web.ObservationFilterChainDecorator$AroundFilterObservation$SimpleAroundFilterObservation.lambda$wrap$0(ObservationFilterChainDecorator.java:336)
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227)
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:140)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:237)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:195)
at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
at org.springframework.web.filter.ServletRequestPathFilter.doFilter(ServletRequestPathFilter.java:52)
at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74)
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebSecurityConfiguration.java:317)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:355)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:272)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:406)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:71)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:1307)
at java.base/java.lang.Thread.run(Thread.java:1474)
DEBUG o.s.s.w.s.HttpSessionRequestCache : Saved request http://localhost:8080/error?continue to session
DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using Or [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest], And [Not [MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@<hash>, matchingMediaTypes=[text/html], useEquals=false, ignoredMediaTypes=[]]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@<hash>, matchingMediaTypes=[application/atom+xml, application/x-www-form-urlencoded, application/json, application/octet-stream, application/xml, multipart/form-data, text/xml], useEquals=false, ignoredMediaTypes=[*/*]]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@<hash>, matchingMediaTypes=[*/*], useEquals=true, ignoredMediaTypes=[]]]
DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint : Match found! Executing org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint@<hash>
DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]
DEBUG o.s.s.w.a.DelegatingAuthenticationEntryPoint : No match found. Using default entry point org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint@<hash>
DEBUG o.a.c.c.C.[.[.[/].[dispatcherServlet] : Disabling the response for further output

View File

@@ -0,0 +1,30 @@
==============================================================================
docs/output/demo5-custom-placement.txt
Four custom filters at four anchors.
GET /diag/chains (profile: custom)
==============================================================================
FilterChainProxy holds 1 SecurityFilterChain(s).
The FIRST chain whose matcher accepts the request wins; the rest are never consulted.
=== chain 1/1 matcher = any request (20 filters)
1/20 DisableEncodeUrlFilter order=100
2/20 WebAsyncManagerIntegrationFilter order=600
3/20 SecurityContextHolderFilter order=700
4/20 RequestIdFilter order=? (not in the registration table)
5/20 HeaderWriterFilter order=900
6/20 CsrfFilter order=1100
7/20 LogoutFilter order=1200
8/20 ApiKeyAuthenticationFilter order=? (not in the registration table)
9/20 UsernamePasswordAuthenticationFilter order=2100
10/20 DefaultResourcesFilter order=2400
11/20 DefaultLoginPageGeneratingFilter order=2500
12/20 DefaultLogoutPageGeneratingFilter order=2600
13/20 BasicAuthenticationFilter order=3100
14/20 RequestCacheAwareFilter order=3300
15/20 SecurityContextHolderAwareRequestFilter order=3400
16/20 AnonymousAuthenticationFilter order=3700
17/20 TenantFilter order=? (not in the registration table)
18/20 ExceptionTranslationFilter order=4000
19/20 TenantFilter order=? (not in the registration table)
20/20 AuthorizationFilter order=4200

View File

@@ -0,0 +1,34 @@
==============================================================================
docs/output/demo6-exception-translation.txt
Two identical TenantFilters, 300 apart, straddling ExceptionTranslationFilter (4000).
Both throw AccessDeniedException. Neither of them produces a 403 - for two different reasons.
==============================================================================
$ curl -i -u alice:password localhost:8080/tenant/doc # filter at order 3701
HTTP/1.1 500
$ curl -i -u alice:password localhost:8080/tenant/translated # filter at order 4001
HTTP/1.1 200
$ curl -i -u alice:password -H "X-Tenant-Id: acme" localhost:8080/tenant/doc
HTTP/1.1 200
$ curl -s -H "X-Api-Key: let-me-in" localhost:8080/whoami # api key filter at 1201
principal=api-client authenticated=true authorities=[ROLE_API] type=UsernamePasswordAuthenticationToken counting-filter-invocations=null
$ curl -s -D- -o /dev/null localhost:8080/public/hello | grep X-Request-Id # filter at 701
X-Request-Id: 74ee45fe-9eda-3820-8110-63a6116aa155
--- the same four filters, run with -DUNIQUE_ONCE_KEY=true ---
Each TenantFilter now has its own OncePerRequestFilter key, so both actually execute.
$ curl -i -u alice:password localhost:8080/tenant/doc # filter at order 3701
HTTP/1.1 500
$ curl -i -u alice:password localhost:8080/tenant/translated # filter at order 4001
HTTP/1.1 403
Same code, same exception, 300 order slots apart:
order 3701 - below ExceptionTranslationFilter (4000) - the throw escapes untranslated
order 4001 - above it - the throw becomes a 403

View File

@@ -0,0 +1,27 @@
==============================================================================
docs/output/demo7-misordered.txt
The SAME ApiKeyAuthenticationFilter, moved from order 1201 to 4201.
The key is valid. The filter runs. The request is still rejected.
==============================================================================
--- chain ---
FilterChainProxy holds 1 SecurityFilterChain(s).
The FIRST chain whose matcher accepts the request wins; the rest are never consulted.
=== chain 1/1 matcher = any request (12 filters)
1/12 DisableEncodeUrlFilter order=100
2/12 WebAsyncManagerIntegrationFilter order=600
3/12 SecurityContextHolderFilter order=700
4/12 HeaderWriterFilter order=900
5/12 LogoutFilter order=1200
6/12 BasicAuthenticationFilter order=3100
7/12 RequestCacheAwareFilter order=3300
8/12 SecurityContextHolderAwareRequestFilter order=3400
9/12 AnonymousAuthenticationFilter order=3700
10/12 ExceptionTranslationFilter order=4000
11/12 AuthorizationFilter order=4200
12/12 ApiKeyAuthenticationFilter order=? (not in the registration table)
$ curl -i -H "X-Api-Key: let-me-in" localhost:8080/whoami
HTTP/1.1 401

View File

@@ -0,0 +1,23 @@
==============================================================================
docs/output/demo8-tie.txt
addFilterBefore(x, CsrfFilter.class) twice. Both filters get order 1099.
==============================================================================
--- A registered first ---
executed order: [A, B]
--- chain positions ---
5/12 MarkerFilterA order=? (not in the registration table)
6/12 MarkerFilterB order=? (not in the registration table)
--- B registered first (-DTIE_REVERSED=true), nothing else changed ---
executed order: [B, A]
--- chain positions ---
5/12 MarkerFilterB order=? (not in the registration table)
6/12 MarkerFilterA order=? (not in the registration table)
The order number is identical in both runs. The executed order follows the order of
the addFilterBefore calls, because List.sort is stable - not because Spring Security
promises anything about ties.

View File

@@ -0,0 +1,20 @@
==============================================================================
docs/output/demo9-double-registration.txt
A CountingFilter @Bean added to the security chain. Boot ALSO registers every Filter bean
with the servlet container, so it is in two chains at once.
==============================================================================
--- profile: doublereg ---
$ curl -sD- -o /dev/null localhost:8080/whoami | grep X-Counting
X-Counting-Filter-Invocations: 2
$ curl -s localhost:8080/diag/servlet-filters | grep -i counting
countingFilter com.ankurm.chain.filter.CountingFilter urls=[/*]
--- profile: doublereg,fixed (FilterRegistrationBean.setEnabled(false)) ---
$ curl -sD- -o /dev/null localhost:8080/whoami | grep X-Counting
X-Counting-Filter-Invocations: 1
$ curl -s localhost:8080/diag/servlet-filters | grep -i counting
(not registered with the container)

View File

@@ -0,0 +1,114 @@
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.ankurm.chain.FilterChainContractTest
[INFO] Running two filters added before the same anchor
01:50:14.438 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest$Tie]: Tie does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
01:50:14.614 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.ankurm.chain.FilterChainDemoApplication for test class com.ankurm.chain.FilterChainContractTest$Tie
01:50:14.679 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest$Tie]: Tie does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
01:50:14.680 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest]: FilterChainContractTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
01:50:14.684 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.ankurm.chain.FilterChainDemoApplication for test class com.ankurm.chain.FilterChainContractTest$Tie
INFO c.a.chain.FilterChainContractTest$Tie : Starting FilterChainContractTest.Tie using Java 25.0.4.1 with PID 8588 (started by root in /tmp/ssd/filter-chain)
INFO c.a.chain.FilterChainContractTest$Tie : The following 1 profile is active: "tie"
INFO o.s.s.c.a.a.c.InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer : Global AuthenticationManager configured with UserDetailsService bean with name users
INFO o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
INFO o.s.t.web.servlet.TestDispatcherServlet : Initializing Servlet ''
INFO o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint beneath base path '/actuator'
INFO o.s.t.web.servlet.TestDispatcherServlet : Completed initialization in 33 ms
INFO c.a.chain.FilterChainContractTest$Tie : Started FilterChainContractTest.Tie in 2.615 seconds (process running for 3.902)
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build as described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
WARNING: A Java agent has been loaded dynamically (/root/.m2/repository/net/bytebuddy/byte-buddy-agent/1.18.11/byte-buddy-agent-1.18.11.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.951 s -- in two filters added before the same anchor
[INFO] Running the reference configuration
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest$Baseline]: Baseline does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.chain.FilterChainDemoApplication for test class com.ankurm.chain.FilterChainContractTest$Baseline
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest$Baseline]: Baseline does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest]: FilterChainContractTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.chain.FilterChainDemoApplication for test class com.ankurm.chain.FilterChainContractTest$Baseline
INFO c.a.c.FilterChainContractTest$Baseline : Starting FilterChainContractTest.Baseline using Java 25.0.4.1 with PID 8588 (started by root in /tmp/ssd/filter-chain)
INFO c.a.c.FilterChainContractTest$Baseline : The following 1 profile is active: "baseline"
INFO o.s.s.c.a.a.c.InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer : Global AuthenticationManager configured with UserDetailsService bean with name users
INFO o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
INFO o.s.t.web.servlet.TestDispatcherServlet : Initializing Servlet ''
INFO o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint beneath base path '/actuator'
INFO o.s.t.web.servlet.TestDispatcherServlet : Completed initialization in 6 ms
INFO c.a.c.FilterChainContractTest$Baseline : Started FilterChainContractTest.Baseline in 0.698 seconds (process running for 5.482)
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.761 s -- in the reference configuration
[INFO] Running WebSecurity.ignoring()
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest$Ignoring]: Ignoring does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.chain.FilterChainDemoApplication for test class com.ankurm.chain.FilterChainContractTest$Ignoring
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest$Ignoring]: Ignoring does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest]: FilterChainContractTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.chain.FilterChainDemoApplication for test class com.ankurm.chain.FilterChainContractTest$Ignoring
INFO c.a.c.FilterChainContractTest$Ignoring : Starting FilterChainContractTest.Ignoring using Java 25.0.4.1 with PID 8588 (started by root in /tmp/ssd/filter-chain)
INFO c.a.c.FilterChainContractTest$Ignoring : The following 1 profile is active: "ignoring"
INFO o.s.s.c.a.a.c.InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer : Global AuthenticationManager configured with UserDetailsService bean with name users
WARN o.s.s.c.a.web.builders.WebSecurity : You are asking Spring Security to ignore PathPattern [/static/**]. This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead.
INFO o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
INFO o.s.t.web.servlet.TestDispatcherServlet : Initializing Servlet ''
INFO o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint beneath base path '/actuator'
INFO o.s.t.web.servlet.TestDispatcherServlet : Completed initialization in 8 ms
INFO c.a.c.FilterChainContractTest$Ignoring : Started FilterChainContractTest.Ignoring in 0.616 seconds (process running for 6.187)
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.771 s -- in WebSecurity.ignoring()
[INFO] Running custom filters
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest$Custom]: Custom does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.chain.FilterChainDemoApplication for test class com.ankurm.chain.FilterChainContractTest$Custom
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest$Custom]: Custom does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest]: FilterChainContractTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.chain.FilterChainDemoApplication for test class com.ankurm.chain.FilterChainContractTest$Custom
INFO c.a.c.FilterChainContractTest$Custom : Starting FilterChainContractTest.Custom using Java 25.0.4.1 with PID 8588 (started by root in /tmp/ssd/filter-chain)
INFO c.a.c.FilterChainContractTest$Custom : The following 1 profile is active: "custom"
INFO o.s.s.c.a.a.c.InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer : Global AuthenticationManager configured with UserDetailsService bean with name users
INFO o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
INFO o.s.t.web.servlet.TestDispatcherServlet : Initializing Servlet ''
INFO o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint beneath base path '/actuator'
INFO o.s.t.web.servlet.TestDispatcherServlet : Completed initialization in 4 ms
INFO c.a.c.FilterChainContractTest$Custom : Started FilterChainContractTest.Custom in 0.477 seconds (process running for 6.82)
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.578 s -- in custom filters
[INFO] Running an authentication filter after AuthorizationFilter
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest$Misordered]: Misordered does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.chain.FilterChainDemoApplication for test class com.ankurm.chain.FilterChainContractTest$Misordered
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest$Misordered]: Misordered does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest]: FilterChainContractTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.chain.FilterChainDemoApplication for test class com.ankurm.chain.FilterChainContractTest$Misordered
INFO c.a.c.FilterChainContractTest$Misordered : Starting FilterChainContractTest.Misordered using Java 25.0.4.1 with PID 8588 (started by root in /tmp/ssd/filter-chain)
INFO c.a.c.FilterChainContractTest$Misordered : The following 1 profile is active: "misordered"
INFO o.s.s.c.a.a.c.InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer : Global AuthenticationManager configured with UserDetailsService bean with name users
INFO o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
INFO o.s.t.web.servlet.TestDispatcherServlet : Initializing Servlet ''
INFO o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint beneath base path '/actuator'
INFO o.s.t.web.servlet.TestDispatcherServlet : Completed initialization in 4 ms
INFO c.a.c.FilterChainContractTest$Misordered : Started FilterChainContractTest.Misordered in 0.352 seconds (process running for 7.247)
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.389 s -- in an authentication filter after AuthorizationFilter
[INFO] Running three SecurityFilterChain beans
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest$MultiChain]: MultiChain does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.chain.FilterChainDemoApplication for test class com.ankurm.chain.FilterChainContractTest$MultiChain
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest$MultiChain]: MultiChain does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [com.ankurm.chain.FilterChainContractTest]: FilterChainContractTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
INFO o.s.b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration com.ankurm.chain.FilterChainDemoApplication for test class com.ankurm.chain.FilterChainContractTest$MultiChain
INFO c.a.c.FilterChainContractTest$MultiChain : Starting FilterChainContractTest.MultiChain using Java 25.0.4.1 with PID 8588 (started by root in /tmp/ssd/filter-chain)
INFO c.a.c.FilterChainContractTest$MultiChain : The following 1 profile is active: "multichain"
INFO o.s.s.c.a.a.c.InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer : Global AuthenticationManager configured with UserDetailsService bean with name users
INFO o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
INFO o.s.t.web.servlet.TestDispatcherServlet : Initializing Servlet ''
INFO o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint beneath base path '/actuator'
INFO o.s.t.web.servlet.TestDispatcherServlet : Completed initialization in 18 ms
INFO c.a.c.FilterChainContractTest$MultiChain : Started FilterChainContractTest.MultiChain in 0.398 seconds (process running for 7.681)
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.426 s -- in three SecurityFilterChain beans
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.924 s -- in com.ankurm.chain.FilterChainContractTest
[INFO] Running com.ankurm.chain.FilterOrderTableTest
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.052 s -- in com.ankurm.chain.FilterOrderTableTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 21, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 9.196 s
[INFO] Finished at: <timestamp>
[INFO] ------------------------------------------------------------------------