How to debug Spring Security filter chains step by step
How to debug Spring Security filter chains step by step
When Spring Security blocks a request, it usually happens long before your controller is reached.
At that point, many developers are stuck asking:
“Which filter rejected my request — and why?”
This post shows a practical, step-by-step approach to debug Spring Security filter chains, without reading the entire framework source code.
Why filter chains are so hard to debug
Spring Security uses a chain of filters, each responsible for one concern:
- Request matching
- Authentication
- Authorization
- Exception handling
Once a filter rejects a request, the chain stops.
If you don’t know which filter made the decision, debugging becomes guesswork.
Step 1: Confirm which SecurityFilterChain is used
The first thing to verify is whether the request is handled by the expected security chain.
Enable this log:
logging.level.org.springframework.security.web.FilterChainProxy=DEBUG
You will see output like:
Securing GET /api/admin/users
Using SecurityFilterChain [RequestMatcher=any request]
If the wrong chain is selected, everything that follows is irrelevant.
Many 401 / 403 issues start here.
Step 2: Identify the filter where the chain stops
Next, enable:
logging.level.org.springframework.security=DEBUG
Now each filter will log when it processes the request.
Look for the last filter that logs before the response is returned.
That filter is where the request was rejected.
Step 3: Know the most common “stop filters”
In real-world systems, most failures happen in just a few filters:
- BearerTokenAuthenticationFilter — JWT not parsed or rejected
- AnonymousAuthenticationFilter — request treated as anonymous
- ExceptionTranslationFilter — converts exceptions to 401 / 403
- AuthorizationFilter — access denied
Once you know which filter stopped the chain, the cause is usually obvious.
Step 4: Distinguish authentication vs authorization failures
This distinction is critical:
- If the chain stops before authentication → 401
- If it stops after authentication → 403
Ask this question:
Did Spring ever create an Authentication object?
If not, focus on token parsing and filters.
If yes, focus on roles, authorities, and matchers.
Step 5: Log SecurityContext at key points
Strategically logging the security context saves hours of debugging.
Right after authentication:
Authentication auth =
SecurityContextHolder.getContext().getAuthentication();
log.info("Authenticated user={}, authorities={}",
auth != null ? auth.getName() : "none",
auth != null ? auth.getAuthorities() : "none");
If this log never appears, authentication never succeeded.
Step 6: Watch for matcher-related surprises
Security rules are evaluated in order.
A common mistake:
.requestMatchers("/api/**").authenticated()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
The first rule matches everything — the second rule is never reached.
Debug logs will reveal this immediately.
Step 7: Reactive (WebFlux) filter chains behave differently
In WebFlux:
- There is no thread-local context
- Logging outside the reactive chain often shows nothing
Filters may appear to run, but authentication context is lost downstream.
Always log inside the reactive pipeline.
A repeatable debugging checklist
When a request fails, walk through this checklist:
- Which SecurityFilterChain handled the request?
- Which filter was last executed?
- Was authentication created?
- Which matcher and rule were applied?
If you can answer all four, the bug is usually trivial.
Final thoughts
Spring Security is complex — but it is not random.
Every 401 or 403 comes from a specific filter, in a specific chain, for a specific reason.
Once you learn how to observe the filter chain, debugging becomes systematic instead of frustrating.
Series complete:
This post concludes the Spring Security 401 / 403 debugging series.
Part of the Spring Boot SSO Series
❤️ Support This Blog
If this post helped you, you can support my writing with a small donation. Thank you for reading.
Comments
Post a Comment