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:
93
filter-chain/docs/01-the-two-proxies.md
Normal file
93
filter-chain/docs/01-the-two-proxies.md
Normal 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)
|
||||
69
filter-chain/docs/02-the-default-chain.md
Normal file
69
filter-chain/docs/02-the-default-chain.md
Normal 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 4–5)
|
||||
3. The request is authenticated (filters 6–14)
|
||||
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)
|
||||
117
filter-chain/docs/03-the-order-table.md
Normal file
117
filter-chain/docs/03-the-order-table.md
Normal 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)
|
||||
112
filter-chain/docs/04-where-custom-filters-land.md
Normal file
112
filter-chain/docs/04-where-custom-filters-land.md
Normal 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)
|
||||
166
filter-chain/docs/05-failure-modes.md
Normal file
166
filter-chain/docs/05-failure-modes.md
Normal 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)
|
||||
163
filter-chain/docs/06-reading-the-trace.md
Normal file
163
filter-chain/docs/06-reading-the-trace.md
Normal 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)
|
||||
71
filter-chain/docs/07-multiple-chains.md
Normal file
71
filter-chain/docs/07-multiple-chains.md
Normal 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)
|
||||
101
filter-chain/docs/08-debugging-recipes.md
Normal file
101
filter-chain/docs/08-debugging-recipes.md
Normal 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)
|
||||
83
filter-chain/docs/09-testing-the-chain.md
Normal file
83
filter-chain/docs/09-testing-the-chain.md
Normal 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)
|
||||
39
filter-chain/docs/README.md
Normal file
39
filter-chain/docs/README.md
Normal 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)
|
||||
52
filter-chain/docs/output/demo1-order-table.txt
Normal file
52
filter-chain/docs/output/demo1-order-table.txt
Normal 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
|
||||
57
filter-chain/docs/output/demo10-multichain.txt
Normal file
57
filter-chain/docs/output/demo10-multichain.txt
Normal 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
|
||||
38
filter-chain/docs/output/demo11-ignoring-vs-permitall.txt
Normal file
38
filter-chain/docs/output/demo11-ignoring-vs-permitall.txt
Normal 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
|
||||
17
filter-chain/docs/output/demo12-servlet-filters.txt
Normal file
17
filter-chain/docs/output/demo12-servlet-filters.txt
Normal 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.
|
||||
29
filter-chain/docs/output/demo2-default-chain.txt
Normal file
29
filter-chain/docs/output/demo2-default-chain.txt
Normal 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) ---
|
||||
|
||||
53
filter-chain/docs/output/demo3-trace-authenticated.txt
Normal file
53
filter-chain/docs/output/demo3-trace-authenticated.txt
Normal 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}
|
||||
144
filter-chain/docs/output/demo4-trace-csrf-403.txt
Normal file
144
filter-chain/docs/output/demo4-trace-csrf-403.txt
Normal 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
|
||||
30
filter-chain/docs/output/demo5-custom-placement.txt
Normal file
30
filter-chain/docs/output/demo5-custom-placement.txt
Normal 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
|
||||
34
filter-chain/docs/output/demo6-exception-translation.txt
Normal file
34
filter-chain/docs/output/demo6-exception-translation.txt
Normal 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
|
||||
27
filter-chain/docs/output/demo7-misordered.txt
Normal file
27
filter-chain/docs/output/demo7-misordered.txt
Normal 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
|
||||
23
filter-chain/docs/output/demo8-tie.txt
Normal file
23
filter-chain/docs/output/demo8-tie.txt
Normal 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.
|
||||
20
filter-chain/docs/output/demo9-double-registration.txt
Normal file
20
filter-chain/docs/output/demo9-double-registration.txt
Normal 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)
|
||||
114
filter-chain/docs/output/tests.txt
Normal file
114
filter-chain/docs/output/tests.txt
Normal 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] ------------------------------------------------------------------------
|
||||
Reference in New Issue
Block a user