Spring Security 5 to 6 to 7 Migration: SecurityFilterChain, Lambda DSL, and the Silent Authorization Changes

Of every breaking change in the Spring Boot 2 → 3 era, the removal of WebSecurityConfigurerAdapter generated the most confused stack traces I’ve debugged — because security configuration is the one place where “it compiles and runs” tells you almost nothing about whether it still protects anything. This guide migrates Spring Security 5 configurations to the 6.x component model (Spring Boot 3.x), covers what tightens further in Spring Security 7 (Spring Boot 4), and flags the places where a mechanical conversion quietly changes your authorization behaviour.

Tested with: Spring Security 5.8 → 6.4 on Spring Boot 3.4, and 7.0 on Spring Boot 4. For the surrounding upgrade, see the Spring Boot 3 to 4 migration guide.

What Changed, Version by Version

Spring Security 5 (Boot 2)Spring Security 6 (Boot 3)Spring Security 7 (Boot 4)
Configuration styleextends WebSecurityConfigurerAdapterSecurityFilterChain @BeanSecurityFilterChain @Bean (lambda DSL only)
Non-lambda chained DSL (.and())StandardDeprecatedRemoved
authorizeRequests()StandardDeprecated → authorizeHttpRequests()Removed
Namespacejavax.servletjakarta.servletjakarta.servlet
Default for unmatched requestsWhatever you configured (often permissive)authenticated-by-default patterns encouraged; mvcMatchers removed in favour of requestMatchers

The Core Conversion: Adapter → SecurityFilterChain

// BAD: Spring Security 5 — class removed in 6
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            .httpBasic();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("ankur").password("{noop}secret").roles("ADMIN");
    }
}
// GOOD: Spring Security 6/7 — beans, lambda DSL, requestMatchers
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())   // see CSRF note below before copying this
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .httpBasic(Customizer.withDefaults());
        return http.build();
    }
    @Bean
    UserDetailsService users(PasswordEncoder encoder) {
        UserDetails admin = User.withUsername("ankur")
            .password(encoder.encode("secret"))
            .roles("ADMIN")
            .build();
        return new InMemoryUserDetailsManager(admin);
    }
    @Bean
    PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

Three structural shifts: configuration methods become @Bean definitions; the chained .and() style becomes nested lambdas (mandatory by 7); and antMatchers/mvcMatchers collapse into requestMatchers.

The Silent Behaviour Changes

1. authorizeRequests vs authorizeHttpRequests Is Not a Rename

The old authorizeRequests() used FilterSecurityInterceptor and evaluated SpEL-based config attributes; authorizeHttpRequests() uses AuthorizationManager. Two consequences trip migrations. First, by default the new model also authorizes the dispatch types — forwards and error dispatches are now checked, so an unauthenticated error page rendered via /error can suddenly return 401 after migration. If your error handling broke, that’s why:

.authorizeHttpRequests(auth -> auth
    .dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
    .requestMatchers("/public/**").permitAll()
    .anyRequest().authenticated())

Second, complex SpEL expressions (access("hasRole('X') and hasIpAddress(...)")) need rewriting as AuthorizationManager implementations or WebExpressionAuthorizationManager — verify each one with a test rather than trusting the conversion.

2. CSRF Defaults for SPAs

Spring Security 6 changed CSRF token handling (deferred token loading, XorCsrfTokenRequestAttributeHandler for BREACH protection). SPAs that read the XSRF-TOKEN cookie and worked on 5.x can start failing with 403s on 6.x until you configure the SPA-specific handler from the official docs. And before copying csrf.disable() from any tutorial (including the snippet above): that is only acceptable for stateless token-based APIs, never for session-based browser apps.

3. Password Encoding Is Non-Negotiable

If your 5.x config used {noop} or a custom no-op encoder “temporarily” (we have all been there), the migration is the moment it stops being tolerable: use the delegating encoder shown above. The format prefix ({bcrypt} etc.) in stored passwords keeps existing hashes working while new ones upgrade.

Hop 2: Security 6 → 7 (Spring Boot 4)

If you completed the 5→6 conversion with the lambda DSL, Spring Security 7 is mostly a cleanup release for you: the deprecated chained DSL (.and()) and authorizeRequests() are removed outright, so configurations that ignored 6.x deprecation warnings fail to compile. The path is identical to the Hibernate story on this site: burn down deprecations on the middle version and the final hop is a version bump. The one new area to review on 7 is method security — @EnableMethodSecurity (which replaced @EnableGlobalMethodSecurity in 6) is now the only model, with @PreAuthorize as the primary annotation.

A Migration Test Net

Security migrations need tests more than any other migration, because the failure mode is silence. Before converting, write authorization tests for every protected route family — they are cheap with spring-security-test:

@WebMvcTest(AdminController.class)
class AdminSecurityTest {
    @Autowired MockMvc mvc;
    @Test
    void anonymousIsRejected() throws Exception {
        mvc.perform(get("/admin/users")).andExpect(status().isUnauthorized());
    }
    @Test
    @WithMockUser(roles = "USER")
    void wrongRoleIsForbidden() throws Exception {
        mvc.perform(get("/admin/users")).andExpect(status().isForbidden());
    }
    @Test
    @WithMockUser(roles = "ADMIN")
    void adminIsAllowed() throws Exception {
        mvc.perform(get("/admin/users")).andExpect(status().isOk());
    }
}

Run these green on 5.x, migrate, run again. Any flip is a behaviour change you just caught before production did. (More patterns in JUnit 6 with Spring Boot.)

Frequently Asked Questions

Can I keep WebSecurityConfigurerAdapter on Spring Boot 3? No — the class does not exist in Spring Security 6. The conversion is mandatory, not stylistic.

Multiple adapters → multiple what? Multiple SecurityFilterChain beans, each with securityMatcher() scoping and @Order. Order matters: the first matching chain wins, so put the most specific matcher (e.g. /api/**) at the lowest order.

What about OAuth2 configs? The resource-server and client DSLs survived the move largely intact — the lambda conversion applies, but the property-driven configuration (spring.security.oauth2.*) is unchanged in spirit. The dispatch-type and CSRF notes above still apply.

AI Prompts for the Migration

Convert the Adapter

Convert this WebSecurityConfigurerAdapter to Spring Security 6 SecurityFilterChain beans with the lambda DSL: [paste config here]. Replace antMatchers/mvcMatchers with requestMatchers, convert configure(AuthenticationManagerBuilder) to UserDetailsService/AuthenticationManager beans, and list every place the new AuthorizationManager model may behave differently.

What it does: The structural conversion plus an explicit behaviour-difference report instead of a silent rewrite.

When to use it: Per security configuration class.

Audit Authorization Coverage

Here are my controllers and my migrated SecurityFilterChain: [paste both]. Map every endpoint to the rule that matches it, flag endpoints that fall through to anyRequest(), and identify forward/error dispatches that the new dispatch-type authorization will now block.

What it does: Catches the unmatched-endpoint and error-page regressions that compile cleanly.

When to use it: Immediately after conversion, before manual testing.

Rewrite SpEL Access Rules

Rewrite these access() SpEL expressions for the AuthorizationManager model: [paste expressions here]. Use built-in managers where possible, generate custom AuthorizationManager implementations where not, and include a unit test for each.

What it does: Converts the one part of the migration with no mechanical mapping.

When to use it: If your audit found access() expressions beyond hasRole/permitAll.

Generate the Test Net

From this SecurityFilterChain: [paste here], generate MockMvc authorization tests covering every rule — anonymous, wrong role, correct role — including CSRF behaviour for state-changing endpoints.

What it does: Builds the before/after safety net automatically.

When to use it: Before starting the conversion, on the old behaviour.

Fix SPA CSRF Breakage

My SPA sends the XSRF-TOKEN cookie value in the X-XSRF-TOKEN header and gets 403 after the Spring Security 6 upgrade. Here is my CSRF config: [paste here]. Produce the SPA-compatible configuration with the Xor handler and explain what changed in token handling.

What it does: Resolves the single most-reported post-migration bug for frontend+API apps.

When to use it: When the frontend team reports mysterious 403s after your upgrade.

Conclusion

The adapter-to-bean conversion is finite and well-documented; the risk lives in the semantic gaps — dispatch-type authorization, SpEL rewrites, CSRF token handling. Write the authorization test net first, convert with the lambda DSL (it’s mandatory by 7 anyway), audit every endpoint against its matching rule, and clear all deprecation warnings on 6 so the Boot 4 / Security 7 hop is a non-event.

See Also

Further Reading

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.