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 style | extends WebSecurityConfigurerAdapter | SecurityFilterChain @Bean | SecurityFilterChain @Bean (lambda DSL only) |
| Non-lambda chained DSL (.and()) | Standard | Deprecated | Removed |
| authorizeRequests() | Standard | Deprecated → authorizeHttpRequests() | Removed |
| Namespace | javax.servlet | jakarta.servlet | jakarta.servlet |
| Default for unmatched requests | Whatever 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
- Spring Boot 3 to 4 Migration Guide
- JUnit 6 with Spring Boot: Unit, Slice, and Integration Testing
- RestTemplate to RestClient Migration Guide
- Hibernate 5 to 6 to 7 Migration Guide
- Secure Your Jersey REST APIs