Skip to content

Instantly share code, notes, and snippets.

@sandipchitale
Last active July 19, 2026 02:12
Show Gist options
  • Select an option

  • Save sandipchitale/a66531379e51e45e3a243bb21839505f to your computer and use it in GitHub Desktop.

Select an option

Save sandipchitale/a66531379e51e45e3a243bb21839505f to your computer and use it in GitHub Desktop.
Springboot dump filters and spring security filter chains #spring-security-filter-chain
package ?;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.springframework.beans.BeansException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.debug.DebugFilter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Registers a debugging filter that dumps the servlet filter chain and every configured Spring
* Security {@link SecurityFilterChain} — each chain's {@link RequestMatcher} and filters — marking
* the first chain that matches the current request.
*
* <p>Disabled by default. Enable with {@code app.dump-filters.enabled=true}. Output goes to
* {@code System.out} rather than the logger — it is meant to be watched in the console next to the
* request that produced it, not collected into the log file.
*
* <p>Spring provides built-ins that cover most of this, and they are preferable when they suffice:
* <ul>
* <li>{@code logging.level.org.springframework.security.web.DefaultSecurityFilterChain=DEBUG} —
* logs {@code Will secure <matcher> with filters: ...} once per chain at startup.</li>
* <li>{@code logging.level.org.springframework.security.web.FilterChainProxy=TRACE} — logs which
* chain a request matched and each filter as it is invoked.</li>
* <li>{@code @EnableWebSecurity(debug = true)} — installs {@code DebugFilter}, which logs the
* request and its matched chain at INFO under the logger named {@code Spring Security Debugger}.</li>
* <li>{@code /actuator/mappings} — lists servlet filter registrations without any reflection
* (though as an unordered snapshot, not the real chain order).</li>
* </ul>
* What none of them do is expand a composite {@link RequestMatcher}: they print
* {@code matcher.toString()}, so an {@code Or}/{@code And} tree — and in particular the authorization
* server's {@code endpointsMatcher}, held inside a lambda — shows up as an opaque object. Expanding
* that tree is what this class adds, and it is the reason it exists.
*
* @author schitale
*/
@Configuration
@ConditionalOnProperty(name = "app.dump-filters.enabled", havingValue = "true")
public class DumpFiltersConfig {
/** URI suffixes that would otherwise flood the log with dumps for page assets. */
private static final List<String> SKIPPED_SUFFIXES = List.of(".html", ".css", ".js", ".ico", ".png", ".svg", ".map");
public static class DumpFilters extends OncePerRequestFilter {
private final ApplicationContext applicationContext;
/** Resolved lazily on first use: eager injection would tie this bean to the security filter's startup. */
private volatile FilterChainProxy filterChainProxy;
/**
* The rendered chains. Matchers and filters are fixed once the context is up, so the
* reflective matcher walk runs once rather than on every request.
*/
private volatile List<ChainDump> chainDumps;
public DumpFilters(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/** One security chain, pre-rendered, retained so {@link SecurityFilterChain#matches} can be re-evaluated per request. */
private record ChainDump(SecurityFilterChain chain, String matcherText, String filtersText) {
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String uri = request.getRequestURI();
return SKIPPED_SUFFIXES.stream().anyMatch(uri::endsWith);
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
StringBuilder out = new StringBuilder(4096);
out.append("\nBegin Filters ============================\n");
out.append("URL: ").append(request.getMethod()).append(' ').append(request.getRequestURI()).append('\n');
appendHeaders(out, request);
appendCookies(out, request);
for (ServletFilter servletFilter : servletFilters(filterChain)) {
out.append("Filter Name: ").append(servletFilter.name())
.append(" FilterClass: ").append(servletFilter.className()).append('\n');
if ("springSecurityFilterChain".equals(servletFilter.name())) {
appendSecurityFilterChains(out, request);
}
}
out.append("End Filters ==============================\n");
// Deliberately STDOUT, not the logger: this is an interactive debugging aid meant to be
// read in the console as requests happen, not captured into the application log file.
System.out.print(out);
filterChain.doFilter(request, response);
}
private void appendSecurityFilterChains(StringBuilder out, HttpServletRequest request) {
List<ChainDump> dumps = securityChainDumps(out);
if (dumps == null) {
return;
}
out.append("Begin Filter Chains ============================\n");
boolean firstMatched = false;
for (ChainDump dump : dumps) {
out.append(dump.matcherText());
if (!firstMatched && dump.chain().matches(request)) {
firstMatched = true;
out.append("\t\t✅ ").append(request.getMethod()).append(' ')
.append(request.getRequestURI()).append(" Matched ✅\n");
}
out.append(dump.filtersText());
}
out.append("End Filter Chains ==============================\n");
}
/**
* Renders every security chain once and caches the result. The {@code springSecurityFilterChain}
* bean is either the {@link FilterChainProxy} itself or a {@link DebugFilter} wrapping one —
* both reachable through public API, so no reflection is needed to get at the chains.
*/
private List<ChainDump> securityChainDumps(StringBuilder out) {
List<ChainDump> dumps = this.chainDumps;
if (dumps != null) {
return dumps;
}
FilterChainProxy proxy = securityFilterChainProxy(out);
if (proxy == null) {
return null;
}
dumps = new ArrayList<>();
for (SecurityFilterChain chain : proxy.getFilterChains()) {
StringBuilder matcherText = new StringBuilder();
if (chain instanceof DefaultSecurityFilterChain defaultChain) {
appendRequestMatcher(defaultChain.getRequestMatcher(), "\t", matcherText);
} else {
matcherText.append('\t').append(chain).append('\n');
}
StringBuilder filtersText = new StringBuilder();
for (Filter securityFilter : chain.getFilters()) {
filtersText.append("\t\t").append(securityFilter).append('\n');
}
dumps.add(new ChainDump(chain, matcherText.toString(), filtersText.toString()));
}
this.chainDumps = dumps;
return dumps;
}
private FilterChainProxy securityFilterChainProxy(StringBuilder out) {
FilterChainProxy proxy = this.filterChainProxy;
if (proxy != null) {
return proxy;
}
try {
Filter securityFilter = this.applicationContext.getBean("springSecurityFilterChain", Filter.class);
if (securityFilter instanceof DebugFilter debugFilter) {
out.append("\torg.springframework.security.web.debug.DebugFilter\n");
proxy = debugFilter.getFilterChainProxy();
} else if (securityFilter instanceof FilterChainProxy chainProxy) {
proxy = chainProxy;
}
} catch (BeansException ex) {
out.append('\t').append(ex.getMessage()).append('\n');
}
this.filterChainProxy = proxy;
return proxy;
}
}
private record ServletFilter(String name, String className) {
}
/**
* Request headers, sorted and name-aligned, one line per value so a repeated header shows each of
* its values rather than a joined blob. Values are printed whole — the {@code Authorization}
* bearer JWT is usually the reason you turned this dump on, so truncating it would defeat it.
*/
private static void appendHeaders(StringBuilder out, HttpServletRequest request) {
List<String> names = Collections.list(request.getHeaderNames());
if (names.isEmpty()) {
return;
}
names.sort(String.CASE_INSENSITIVE_ORDER);
String format = "\t%-" + width(names) + "s : %s\n";
out.append("Headers:\n");
for (String name : names) {
for (String value : Collections.list(request.getHeaders(name))) {
out.append(format.formatted(name, value));
}
}
}
/**
* The cookies the browser sent, broken out of the raw {@code Cookie} header above — for this
* project that is mainly {@code JSESSIONID}, which tells you whether the authorization-server
* session survived, and whether the stateless SPA accidentally acquired one.
*/
private static void appendCookies(StringBuilder out, HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null || cookies.length == 0) {
return;
}
List<String> names = Arrays.stream(cookies).map(Cookie::getName).sorted(String.CASE_INSENSITIVE_ORDER).toList();
String format = "\t%-" + width(names) + "s = %s\n";
out.append("Cookies:\n");
Arrays.stream(cookies)
.sorted(Comparator.comparing(Cookie::getName, String.CASE_INSENSITIVE_ORDER))
.forEach(cookie -> out.append(format.formatted(cookie.getName(), cookie.getValue())));
}
/** Longest name, so the {@code :}/{@code =} separators line up in a column. */
private static int width(List<String> names) {
return names.stream().mapToInt(String::length).max().orElse(0);
}
/**
* The servlet filters in real invocation order. {@code ServletContext.getFilterRegistrations()}
* is the public alternative but returns an unordered map, losing the ordering that makes this
* dump useful — so read Tomcat's chain directly, and return nothing on any other container
* rather than failing.
*/
private static List<ServletFilter> servletFilters(FilterChain filterChain) {
if (!(filterChain instanceof ApplicationFilterChain applicationFilterChain)) {
return List.of();
}
Field filtersField = ReflectionUtils.findField(ApplicationFilterChain.class, "filters");
if (filtersField == null) {
return List.of();
}
ReflectionUtils.makeAccessible(filtersField);
Object value = ReflectionUtils.getField(filtersField, applicationFilterChain);
if (!(value instanceof ApplicationFilterConfig[] filterConfigs)) {
return List.of();
}
List<ServletFilter> servletFilters = new ArrayList<>(filterConfigs.length);
for (ApplicationFilterConfig filterConfig : filterConfigs) {
if (filterConfig != null) {
servletFilters.add(new ServletFilter(filterConfig.getFilterName(), filterConfig.getFilterClass()));
}
}
return servletFilters;
}
/**
* Recursively expands a {@link RequestMatcher}. The composite matchers keep their children in
* private fields with no accessor (verified against spring-security-web 7.1), so reflection is
* unavoidable here — this is the part no built-in debug facility provides.
*/
private static void appendRequestMatcher(RequestMatcher requestMatcher, String indent, StringBuilder out) {
if (requestMatcher instanceof OrRequestMatcher) {
out.append(indent).append("Or\n");
appendChildren(OrRequestMatcher.class, "requestMatchers", requestMatcher, indent, out);
} else if (requestMatcher instanceof AndRequestMatcher) {
out.append(indent).append("And\n");
appendChildren(AndRequestMatcher.class, "requestMatchers", requestMatcher, indent, out);
} else if (requestMatcher instanceof NegatedRequestMatcher) {
out.append(indent).append("Not\n");
Object child = readField(NegatedRequestMatcher.class, "requestMatcher", requestMatcher);
if (child instanceof RequestMatcher negated) {
appendRequestMatcher(negated, indent + "\t", out);
}
} else {
out.append(indent).append(requestMatcher).append('\n');
appendLambdaEndpointsMatcher(requestMatcher, indent, out);
}
}
@SuppressWarnings("unchecked")
private static void appendChildren(Class<?> type, String fieldName, RequestMatcher requestMatcher,
String indent, StringBuilder out) {
Object children = readField(type, fieldName, requestMatcher);
if (children instanceof List<?> list) {
((List<RequestMatcher>) list).forEach(child -> appendRequestMatcher(child, indent + "\t", out));
}
}
/**
* Special case for {@code OAuth2AuthorizationServerConfigurer}: its matcher is a lambda that
* captures the configurer, whose {@code endpointsMatcher} holds the actual endpoint list.
* Without this the authorization server chain prints as an unreadable lambda reference.
*/
private static void appendLambdaEndpointsMatcher(RequestMatcher requestMatcher, String indent, StringBuilder out) {
Object captured = readField(requestMatcher.getClass(), "arg$1", requestMatcher);
if (captured == null) {
return;
}
Object endpointsMatcher = readField(captured.getClass(), "endpointsMatcher", captured);
if (endpointsMatcher instanceof RequestMatcher matcher) {
appendRequestMatcher(matcher, indent + "\t", out);
}
}
private static Object readField(Class<?> type, String fieldName, Object target) {
Field field = ReflectionUtils.findField(type, fieldName);
if (field == null) {
return null;
}
ReflectionUtils.makeAccessible(field);
return ReflectionUtils.getField(field, target);
}
@Bean
FilterRegistrationBean<DumpFilters> dumpFiltersRegistration(ApplicationContext applicationContext) {
FilterRegistrationBean<DumpFilters> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new DumpFilters(applicationContext));
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registrationBean;
}
}
public class DumpServlets extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
System.out.println("---Servlets---");
Map<String, ? extends ServletRegistration> servletRegistrations = request.getServletContext()
.getServletRegistrations();
for (Entry<String, ? extends ServletRegistration> servletNameServletRegistrationEntry : servletRegistrations
.entrySet()) {
System.out.println(
"Servlet Name: " + servletNameServletRegistrationEntry.getKey()
+ " Servlet Class: " + servletNameServletRegistrationEntry.getValue().getClassName()
+ " Servlet Mappings: " + servletNameServletRegistrationEntry.getValue().getMappings()
);
}
filterChain.doFilter(request, response);
}
}
@Bean
public FilterRegistrationBean<DumpServlets> servlets() {
FilterRegistrationBean<DumpServlets> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new DumpServlets());
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registrationBean;
}
any request
GET /favicon.ico Matched
org.springframework.security.web.session.DisableEncodeUrlFilter@7fad214a
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@164642a4
org.springframework.security.web.context.SecurityContextHolderFilter@51bddd98
org.springframework.security.web.header.HeaderWriterFilter@4faf104
org.springframework.security.web.csrf.CsrfFilter@671ea6ff
org.springframework.security.web.authentication.logout.LogoutFilter@2e43c38d
org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter@104dc1a2
org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter@314a31b0
org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter@67d32a54
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@235b4cb8
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@75cf0de5
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@77d4ac52
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@252744a1
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@50b0afd7
org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter@4735d6e5
org.springframework.security.web.access.ExceptionTranslationFilter@49fb0bbd
org.springframework.security.web.access.intercept.AuthorizationFilter@24c8d8be
This is the request matcher used by Spring Security OAuth2 Athorization Server (Note: some of the paths repeat):
org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer$$Lambda$780/0x0000000800fc7c10@468e8565
Or
Or
Ant [pattern='/oauth2/token', POST]
Ant [pattern='/oauth2/introspect', POST]
Ant [pattern='/oauth2/revoke', POST]
Ant [pattern='/oauth2/device_authorization', POST]
Ant [pattern='/.well-known/oauth-authorization-server', GET]
Or
Ant [pattern='/oauth2/authorize', GET]
Ant [pattern='/oauth2/authorize', POST]
Ant [pattern='/oauth2/token', POST]
Ant [pattern='/oauth2/introspect', POST]
Ant [pattern='/oauth2/revoke', POST]
Ant [pattern='/oauth2/device_authorization', POST]
Or
Ant [pattern='/oauth2/device_verification', GET]
Ant [pattern='/oauth2/device_verification', POST]
Or
Ant [pattern='/.well-known/openid-configuration', GET]
Or
Ant [pattern='/connect/logout', GET]
Ant [pattern='/connect/logout', POST]
Or
Ant [pattern='/userinfo', GET]
Ant [pattern='/userinfo', POST]
Ant [pattern='/oauth2/jwks', GET]
And the full filter chain:
Begin Filters ============================
URL: GET /oauth2/jwks
Filter Name: filters FilterClass: com.example.authserver.DumpFiltersConfig$DumpFilters
Filter Name: characterEncodingFilter FilterClass: org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter
Filter Name: formContentFilter FilterClass: org.springframework.boot.web.servlet.filter.OrderedFormContentFilter
Filter Name: requestContextFilter FilterClass: org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter
Filter Name: springSecurityFilterChain FilterClass: org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1
Begin Filter Chains ============================
org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer$$Lambda$780/0x0000000800fc7c10@468e8565
Or
Or
Ant [pattern='/oauth2/token', POST]
Ant [pattern='/oauth2/introspect', POST]
Ant [pattern='/oauth2/revoke', POST]
Ant [pattern='/oauth2/device_authorization', POST]
Ant [pattern='/.well-known/oauth-authorization-server', GET]
Or
Ant [pattern='/oauth2/authorize', GET]
Ant [pattern='/oauth2/authorize', POST]
Ant [pattern='/oauth2/token', POST]
Ant [pattern='/oauth2/introspect', POST]
Ant [pattern='/oauth2/revoke', POST]
Ant [pattern='/oauth2/device_authorization', POST]
Or
Ant [pattern='/oauth2/device_verification', GET]
Ant [pattern='/oauth2/device_verification', POST]
Or
Ant [pattern='/.well-known/openid-configuration', GET]
Or
Ant [pattern='/connect/logout', GET]
Ant [pattern='/connect/logout', POST]
Or
Ant [pattern='/userinfo', GET]
Ant [pattern='/userinfo', POST]
Ant [pattern='/oauth2/jwks', GET]
GET /oauth2/jwks Matched
org.springframework.security.web.session.DisableEncodeUrlFilter@91da29b
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7066363
org.springframework.security.web.context.SecurityContextHolderFilter@41ccb3b9
-> org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.AuthorizationServerContextFilter@3443e476
org.springframework.security.web.header.HeaderWriterFilter@3b353651
org.springframework.security.web.csrf.CsrfFilter@63f855b
-> org.springframework.security.oauth2.server.authorization.oidc.web.OidcLogoutEndpointFilter@2091833
org.springframework.security.web.authentication.logout.LogoutFilter@626c19cf
-> org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter@9f86dc3
-> org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter@2f80cb79
-> org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceVerificationEndpointFilter@fe09383
-> org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter@1de0641b
-> org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter@7f287b98
-> org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter@2e1b374c
org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter@67bd351e
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@1e84f3c8
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5d5a51b1
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@75b38c36
org.springframework.security.web.access.ExceptionTranslationFilter@3913f206
org.springframework.security.web.access.intercept.AuthorizationFilter@7161457
-> org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter@35c4697c
-> org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter@70d77826
-> org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter@4e3e11b9
-> org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceAuthorizationEndpointFilter@49e2b3c5
-> org.springframework.security.oauth2.server.authorization.oidc.web.OidcUserInfoEndpointFilter@7ba93755
any request
org.springframework.security.web.session.DisableEncodeUrlFilter@6f8e9d06
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@77d381e6
org.springframework.security.web.context.SecurityContextHolderFilter@7f7c420c
org.springframework.security.web.header.HeaderWriterFilter@66383c29
org.springframework.security.web.csrf.CsrfFilter@3f63a513
org.springframework.security.web.authentication.logout.LogoutFilter@d7bbf12
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@3a36cd5
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@6435fa1c
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@2272cbb0
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5d152bcd
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@43cb5f38
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3f6f3cc
org.springframework.security.web.access.ExceptionTranslationFilter@413bef78
org.springframework.security.web.access.intercept.AuthorizationFilter@5f7eee96
End Filter Chains ==============================
package ?;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.debug.DebugFilter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* This registers a filter to dump all the configured filter and security filter chains.
*
* Used for debugging security filter chains configured using HttpSecurity.
*
* @author schitale
* @since 23.4
*/
@Configuration
public class DumpFiltersConfig {
public static class DumpFilters extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (filterChain instanceof ApplicationFilterChain) {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
PrintStream out = new PrintStream(byteArrayOutputStream);) {
out.println();
out.println("Begin Filters ============================");
out.println("URL: " + request.getMethod() + " " + request.getRequestURI());
ApplicationFilterChain applicationFilterChain = (ApplicationFilterChain) filterChain;
try {
Field filters = applicationFilterChain.getClass().getDeclaredField("filters");
filters.setAccessible(true);
ApplicationFilterConfig[] filterConfigs = (ApplicationFilterConfig[]) filters
.get(applicationFilterChain);
boolean firstMatched = false;
for (ApplicationFilterConfig applicationFilterConfig : filterConfigs) {
if (applicationFilterConfig != null) {
out.println("Filter Name: " + applicationFilterConfig.getFilterName()
+ " FilterClass: " + applicationFilterConfig.getFilterClass());
if (applicationFilterConfig.getFilterName().equals("springSecurityFilterChain")) {
try {
Method getFilter = applicationFilterConfig.getClass()
.getDeclaredMethod("getFilter");
getFilter.setAccessible(true);
DelegatingFilterProxy delegatingFilterProxy = (DelegatingFilterProxy) getFilter
.invoke(applicationFilterConfig);
Field delegateField = DelegatingFilterProxy.class.getDeclaredField("delegate");
delegateField.setAccessible(true);
FilterChainProxy filterChainProxy = null;
if (delegateField.get(delegatingFilterProxy) instanceof FilterChainProxy) {
filterChainProxy = (FilterChainProxy) delegateField.get(delegatingFilterProxy);
}
if (delegateField.get(delegatingFilterProxy) instanceof DebugFilter debugFilter) {
// DebugFilter debugFilter = (DebugFilter) delegateField.get(delegatingFilterProxy);
out.println("\torg.springframework.security.web.debug.DebugFilter");
filterChainProxy = debugFilter.getFilterChainProxy();
}
if (filterChainProxy != null) {
List<SecurityFilterChain> filterChains = filterChainProxy.getFilterChains();
out.println("Begin Filter Chains ============================");
for (SecurityFilterChain securityFilterChain : filterChains) {
DefaultSecurityFilterChain defaultSecurityFilterChain = (DefaultSecurityFilterChain) securityFilterChain;
RequestMatcher requestMatcher = defaultSecurityFilterChain.getRequestMatcher();
printRequestMatcher(requestMatcher, "\t", out);
if (!firstMatched && defaultSecurityFilterChain.getRequestMatcher().matches(request)) {
firstMatched = true;
out.println("\t\t✅ " + request.getMethod() + " " + request.getRequestURI() + " Matched ✅");
}
List<Filter> securityFilters = securityFilterChain.getFilters();
for (Filter securityFilter : securityFilters) {
out.println("\t\t" + securityFilter);
}
}
out.println("End Filter Chains ==============================");
}
} catch (NoSuchMethodException | InvocationTargetException e) {
out.println(e.getMessage());
}
}
}
}
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
System.err.println(e.getMessage());
}
out.println("End Filters ==============================");
System.out.print(byteArrayOutputStream.toString(StandardCharsets.UTF_8));
}
}
filterChain.doFilter(request, response);
}
}
// Recursive method to print RequestMatcher and its sub RequestMatchers
private static void printRequestMatcher(RequestMatcher requestMatcher, String indent, PrintStream out) {
if (requestMatcher instanceof OrRequestMatcher orRequestMatcher) {
out.println(indent + "Or");
// OrRequestMatcher orRequestMatcher = (OrRequestMatcher) requestMatcher;
Field requestMatchersField = ReflectionUtils.findField(OrRequestMatcher.class, "requestMatchers");
ReflectionUtils.makeAccessible(requestMatchersField);
List<RequestMatcher> requestMatchers =
(List<RequestMatcher>) ReflectionUtils.getField(requestMatchersField, requestMatcher);
requestMatchers.forEach((RequestMatcher rm) -> {
printRequestMatcher(rm, indent + "\t", out);
});
} else if (requestMatcher instanceof AndRequestMatcher andRequestMatcher) {
out.println(indent + "And");
// AndRequestMatcher andRequestMatcher = (AndRequestMatcher) requestMatcher;
Field requestMatchersField = ReflectionUtils.findField(AndRequestMatcher.class, "requestMatchers");
ReflectionUtils.makeAccessible(requestMatchersField);
List<RequestMatcher> requestMatchers =
(List<RequestMatcher>) ReflectionUtils.getField(requestMatchersField, requestMatcher);
requestMatchers.forEach((RequestMatcher rm) -> {
printRequestMatcher(rm, indent + "\t", out);
});
} else if (requestMatcher instanceof NegatedRequestMatcher negatedRequestMatcher) {
out.println(indent + "Not");
// NegatedRequestMatcher negatedRequestMatcher = (NegatedRequestMatcher) requestMatcher;
Field requestMatcherField = ReflectionUtils.findField(NegatedRequestMatcher.class, "requestMatcher");
ReflectionUtils.makeAccessible(requestMatcherField);
RequestMatcher rm = (RequestMatcher) ReflectionUtils.getField(requestMatcherField, requestMatcher);
printRequestMatcher(rm, indent + "\t", out);
} else {
out.println(indent + requestMatcher);
// Check if lambda - get the arg$1
Field requestMatcherField = ReflectionUtils.findField(requestMatcher.getClass(), "arg$1");
if (requestMatcherField != null) {
ReflectionUtils.makeAccessible(requestMatcherField);
Object o = ReflectionUtils.getField(requestMatcherField, requestMatcher);
if (o != null) {
// Special case of OAuth2AuthorizationServerConfigurer.endpointsMatcher
Field endpointsMatcherField = ReflectionUtils.findField(o.getClass(), "endpointsMatcher");
if (endpointsMatcherField != null) {
ReflectionUtils.makeAccessible(endpointsMatcherField);
RequestMatcher rm = (RequestMatcher) ReflectionUtils.getField(endpointsMatcherField, o);
printRequestMatcher(rm, indent + "\t", out);
}
}
}
}
}
@Bean
FilterRegistrationBean<DumpFilters> filters() {
FilterRegistrationBean<DumpFilters> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new DumpFilters());
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registrationBean;
}
}
@sandipchitale

sandipchitale commented Oct 17, 2020

Copy link
Copy Markdown
Author
  • Sample output:
Filter Name: dumpFilters FilterClass: com.example.oauth2arc.Oauth2arcApplication$DumpFilters
Filter Name: characterEncodingFilter FilterClass: org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter
Filter Name: webMvcMetricsFilter FilterClass: org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter
Filter Name: sessionRepositoryFilter FilterClass: org.springframework.session.web.http.SessionRepositoryFilter
Filter Name: formContentFilter FilterClass: org.springframework.boot.web.servlet.filter.OrderedFormContentFilter
Filter Name: requestContextFilter FilterClass: org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter
Filter Name: springSecurityFilterChain FilterClass: org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1
	Ant [pattern='/user']
		org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@6e93b0e7
		org.springframework.security.web.context.SecurityContextPersistenceFilter@14be750c
		org.springframework.security.web.header.HeaderWriterFilter@5112b7
		org.springframework.security.web.csrf.CsrfFilter@48c5698
		org.springframework.security.web.authentication.logout.LogoutFilter@49190ed6
		org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@3d033453
		org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@598657cd
		org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@5eeee124
		org.springframework.security.web.savedrequest.RequestCacheAwareFilter@15e08615
		org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@7f93f4c
		org.springframework.security.web.authentication.AnonymousAuthenticationFilter@62cd562d
		org.springframework.security.web.session.SessionManagementFilter@40f1aa95
		org.springframework.security.web.access.ExceptionTranslationFilter@4efb13f1
		org.springframework.security.web.access.intercept.FilterSecurityInterceptor@49631cfb
	Ant [pattern='/headers']
		org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@e972ee1
		org.springframework.security.web.context.SecurityContextPersistenceFilter@b94703b
		org.springframework.security.web.header.HeaderWriterFilter@a9e8da1
		org.springframework.security.web.csrf.CsrfFilter@2037f12b
		org.springframework.security.web.authentication.logout.LogoutFilter@4abbe41c
		org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@3ac0a14b
		org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@720a1fd0
		org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@5e24592e
		org.springframework.security.web.savedrequest.RequestCacheAwareFilter@33a47707
		org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4d290757
		org.springframework.security.web.authentication.AnonymousAuthenticationFilter@6f0a4e30
		org.springframework.security.web.session.SessionManagementFilter@14b5752f
		org.springframework.security.web.access.ExceptionTranslationFilter@687a0e40
		org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1f45db49
	any request
		org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@1b791dca
		org.springframework.security.web.context.SecurityContextPersistenceFilter@7cca31fc
		org.springframework.security.web.header.HeaderWriterFilter@273dec10
		org.springframework.security.web.csrf.CsrfFilter@2fc7fa6e
		org.springframework.security.web.authentication.logout.LogoutFilter@1d944fc0
		org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c3c453b
		org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@30cafd13
		org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@15ac02d5
		org.springframework.security.web.savedrequest.RequestCacheAwareFilter@619c3546
		org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@29090809
		org.springframework.security.web.authentication.AnonymousAuthenticationFilter@2af9a5ef
		org.springframework.security.web.session.SessionManagementFilter@6cceb281
		org.springframework.security.web.access.ExceptionTranslationFilter@7f8c48f3
Filter Name: Tomcat WebSocket (JSR356) Filter FilterClass: org.apache.tomcat.websocket.server.WsFilter

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment