Skip to content

SecurityFilterChain and what defining one turns off

Severity: criticalBehaviour changed in Spring Security 6.0Applies to: Spring Security 6.xApplies to: Spring Security 7.xFacts last verified 2026-07-26 against Spring Boot 4.1.0 · Spring Security 7.1.0

WebSecurityConfigurerAdapter was deprecated in Spring Security 5.7.0-M2 and removed in 6.0. The class does not exist, so this is not a style preference — code extending it does not compile. Configuration is a SecurityFilterChain bean.

That much is widely known by now. The part that is not, and the reason this page sits at the top of the checklist, is what defining that bean does to the rest of your application.

Defining a SecurityFilterChain turns off Actuator’s automatic protection

Section titled “Defining a SecurityFilterChain turns off Actuator’s automatic protection”

From Spring Boot’s own Actuator documentation:

If Spring Security is on the classpath and no other SecurityFilterChain bean is present, all actuators other than /health are secured by Spring Boot auto-configuration. If you define a custom SecurityFilterChain bean, Spring Boot auto-configuration backs off and lets you fully control the actuator access rules.

Read those two sentences together with the migration above. Every application moving off WebSecurityConfigurerAdapter must define a SecurityFilterChain, and the moment it does, Boot stops protecting the Actuator endpoints on its behalf.

Nothing warns. The build succeeds, the login page works, the tests pass — and /actuator/** is now governed by whatever your new chain says, which for a chain written from a tutorial about form login is usually nothing at all.

This is the single most valuable thing on this page: a security migration that silently widens a different attack surface.

the fix
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// Say something explicit about actuator. Boot no longer will.
.requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole("ADMIN")
.requestMatchers("/", "/login").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}

EndpointRequest is Boot’s own matcher for this — it resolves the actual Actuator paths rather than hardcoding /actuator/**, which matters because management.endpoints.web.base-path can move them.

verify it workedRun this in: framework cli
Terminal window
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/actuator/env

Anything other than 401, 403 or 404 from an unauthenticated request means the chain does not cover it. Test it against a running application rather than reading the config — this is precisely the case where the config looks complete.

Two method names changed at the same time, and automated migrations handle them:

  • authorizeRequests()authorizeHttpRequests()
  • antMatchers()requestMatchers()

The rename that matters is the first one, because the two methods do not use the same matching engine, and a migration tool will happily produce code that compiles while matching different paths than it used to. Read every security-relevant line a migration tool touches. Compiling is not the bar; the bar is that the rules still say what you meant.

Spring Boot 4.1.0 manages Spring Security 7.1.0. Spring Security 6.5 was the last 6.x release, and its own migration guide recommends going to 6.5 first, using its preparation steps, then moving to 7 — rather than jumping.

So WebSecurityConfigurerAdapter advice is now two majors stale. Anything written against the adapter predates 2022 and should be treated as describing a different framework.

before you ship this

This is the change most likely to lock people out of their own application, in both directions: too permissive and everything is public, too strict and the login page itself is behind authentication.

.anyRequest().authenticated() without a permitAll() for /login and static assets produces a redirect loop. Order matters — the first matching rule wins, so a broad matcher above a narrow one silently swallows it.

Migrate on a branch with an integration test that asserts an anonymous request to a protected path gets 401 and to a public path gets 200. That test is worth more than reading the chain.