The WebSecurityConfigurerAdapter class was deprecated in Spring Security 5.7 and removed in Spring Security 6.0. A security configuration is no longer a class that we extend. It is a set of beans, and a SecurityFilterChain bean takes the place of the configure(HttpSecurity) method. Spring Security 7, the version inside Spring Boot 4, also removed the old chained style with and(), so every option is set with a lambda.
We meet the deprecated WebSecurityConfigurerAdapter when we upgrade an app from Spring Boot 2 to Spring Boot 3 or 4. The compiler stops at extends WebSecurityConfigurerAdapter, at antMatchers(), at authorizeRequests() and at User.withDefaultPasswordEncoder(). Each of them has a new form, and we show every one of them with a before and after snippet.
The following example is the minimal replacement. The configure(HttpSecurity) method becomes a @Bean method that returns the built filter chain.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/recipes").permitAll() // open to everyone
.anyRequest().authenticated() // everything else needs a login
)
.httpBasic(withDefaults()) // Customizer.withDefaults()
.formLogin(withDefaults());
return http.build(); // the SecurityFilterChain
}
}
Notice that there is no base class, no @Override and no and(). The rest of the post maps each method of the old adapter to its bean, from the URL rules and the in-memory users to method security and CSRF, and we finish with tests for the new configuration.
1. What Replaced WebSecurityConfigurerAdapter
Before Spring Security 5.7, one class did all the security setup. We extended WebSecurityConfigurerAdapter and overrode its configure() methods, one for the HTTP rules, one for the ignored paths and one for the users, plus authenticationManagerBean() when our own code needed the manager. Since 5.7, Spring Security wants each of these pieces as its own bean. A bean can be replaced or tested on its own, whereas the adapter forced everything into one subclass.
The change came in three steps. Spring Security 5.7.0-M2 (February 2022) deprecated the adapter. Spring Security 6.0 (November 2022, inside Spring Boot 3.0) removed it together with antMatchers(). Spring Security 7.0 (inside Spring Boot 4.0) removed authorizeRequests() and the chained style with and(). The table shows where each old API went.
| Old API (Spring Security 5) | Status | New API (Spring Security 7) |
|---|---|---|
| extends WebSecurityConfigurerAdapter | Removed in 6.0 | A plain @Configuration class with @Bean methods |
| configure(HttpSecurity http) | Removed in 6.0 | @Bean SecurityFilterChain |
| configure(WebSecurity web) | Removed in 6.0 | @Bean WebSecurityCustomizer |
| configure(AuthenticationManagerBuilder auth) | Removed in 6.0 | @Bean UserDetailsService plus @Bean PasswordEncoder |
| authenticationManagerBean() | Removed in 6.0 | @Bean AuthenticationManager |
| http.authorizeRequests() | Removed in 7.0 (deprecated in 6.0) | http.authorizeHttpRequests(authorize -> …) |
| antMatchers(), mvcMatchers(), regexMatchers() | Removed in 6.0 | requestMatchers() |
| http.csrf().disable().and() | Removed in 7.0 (deprecated in 6.1) | http.csrf(csrf -> csrf.disable()) |
| User.withDefaultPasswordEncoder() | Deprecated, stays | User.withUsername() plus a PasswordEncoder bean |
| @EnableGlobalMethodSecurity(prePostEnabled = true) | Deprecated, stays | @EnableMethodSecurity |

The migration is a mechanical change, so the behavior of the app stays the same. We cover each row in the next sections with a before and after snippet, taken from a small recipe API built with Spring Boot 4.1.1, Spring Security 7.1.1 and Java 25. The complete project is in the spring-security-filter-chain folder.
2. Migrating the HttpSecurity Configuration
The configure(HttpSecurity) method was where we wrote the URL rules and switched on login methods. It becomes a @Bean method that takes the same HttpSecurity argument, sets the same options and returns http.build(). The @EnableWebSecurity annotation turns the SecurityFilterChain beans into the one servlet filter that checks every request. Spring Boot adds the annotation for us when we leave it out.
The following example is the old adapter code for a recipe API where /recipes is public and every other URL needs a login. The base class and antMatchers() stopped compiling in Spring Security 6.0, and authorizeRequests() and and() stopped compiling in 7.0.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/recipes").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.formLogin();
}
}
In the new form, every option is a method on http that takes a lambda. Spring calls this style the lambda DSL. The lambda gets an object that holds the settings of that one option, for example the URL rules for authorizeHttpRequests() or the login page for formLogin(). When we want the defaults, we pass Customizer.withDefaults().
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/recipes").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(withDefaults())
.formLogin(withDefaults());
return http.build();
}
}
The lambda DSL is not optional on Spring Security 7. The no-argument versions such as http.csrf() or http.formLogin() and the and() method were removed in 7.0. The static import for withDefaults() is org.springframework.security.config.Customizer.withDefaults. The bean method can be package-private, as in the Spring reference, so we do not need public on it.
3. Replacing antMatchers With requestMatchers
The antMatchers(), mvcMatchers() and regexMatchers() methods were removed in Spring Security 6.0, and their replacement requestMatchers() takes the same string patterns. The authorizeRequests() method was deprecated at the same time and removed in 7.0, so we also switch to authorizeHttpRequests(). The rules inside the lambda read the same as before.
http.authorizeRequests()
.antMatchers("/recipes").permitAll()
.antMatchers(HttpMethod.GET, "/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/recipes").permitAll()
.requestMatchers(HttpMethod.GET, "/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
The pattern syntax is the same for the common cases such as “/admin/**” or “/recipes/{id}”. One difference matters on Spring Security 7. Since 7.0, a string pattern is matched with a PathPatternRequestMatcher, the same matcher that Spring MVC uses. A pattern must cover the full path after the context root. When the DispatcherServlet is mapped to /mvc instead of /, the pattern starts with /mvc as well. If the app used regex patterns, we build a RegexRequestMatcher and pass it to requestMatchers(RequestMatcher…).
4. In-Memory Users Without withDefaultPasswordEncoder
With the adapter, we overrode configure(AuthenticationManagerBuilder) and called inMemoryAuthentication() to register test users. Most examples built those users with User.withDefaultPasswordEncoder(), which hashes the password at startup. The method is deprecated because the plain password sits in the compiled class and in memory, and the Javadoc says it stays in the API only for demos. The IDE warning on it is not an error, but a production app should not use it.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
UserDetails ravi = User.withDefaultPasswordEncoder()
.username("ravi")
.password("pass")
.roles("USER")
.build();
auth.inMemoryAuthentication().withUser(ravi);
}
In the new style, we publish a UserDetailsService bean, and InMemoryUserDetailsManager is the implementation that keeps the users in a map. We also publish a PasswordEncoder bean and encode each password with it, so no plain password is stored. Spring Security picks up both beans and uses them to check every login, which the startup log confirms with “Global AuthenticationManager configured with UserDetailsService bean with name userDetailsService”.
@Bean
UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails ravi = User.withUsername("ravi")
.password(passwordEncoder.encode("pass"))
.roles("USER")
.build();
UserDetails admin = User.withUsername("admin")
.password(passwordEncoder.encode("pass"))
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(ravi, admin);
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
If the users come from a database, the same bean returns a JdbcUserDetailsManager built on the DataSource, which replaces auth.jdbcAuthentication(). The database-backed form login article shows that setup. When the passwords are already hashed, for example in a users table, we store the hash with its prefix such as {bcrypt} and skip the encode() call.
5. Exposing the AuthenticationManager Bean
The adapter had an authenticationManagerBean() method that we overrode to get the AuthenticationManager as a bean, which custom login endpoints and JWT filters need. Without the adapter, we build the manager ourselves. A DaoAuthenticationProvider checks a user name and password against the UserDetailsService, and a ProviderManager is the AuthenticationManager that asks one or more providers in turn.
@Bean
AuthenticationManager authenticationManager(UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(provider);
}
The constructor DaoAuthenticationProvider(UserDetailsService) is the Spring Security 7 form, because the old no-argument constructor with setUserDetailsService() was removed. We only need the bean when our own code calls authenticate(). When the app only uses form login or HTTP Basic, Spring Security builds the manager from the UserDetailsService and PasswordEncoder beans, and we skip this method. The custom authentication provider article shows how to add a second provider to the same ProviderManager.
6. Ignoring Static Resources With WebSecurityCustomizer
The configure(WebSecurity) method was used to take paths such as /css/** out of the security filters altogether. The replacement is a WebSecurityCustomizer bean, a functional interface whose one method receives the same WebSecurity object.
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/css/**", "/images/**");
}
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers("/css/**", "/images/**");
}
Spring Security accepts the bean but prints a warning at startup, because an ignored path gets no security headers and no CSRF check at all.
WARN o.s.s.c.a.web.builders.WebSecurity : You are asking Spring Security to ignore PathPattern [/css/**]. This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead.
The recommended form is a permitAll() rule in the filter chain, so the request still passes through the filters but needs no login. There is a second reason to prefer it. When a browser asks for a file that does not exist under /css/, Spring Boot forwards the 404 to /error. That forward does go through the filter chain, so an anonymous caller sees a 401 instead of the 404.
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/css/**", "/images/**").permitAll()
.anyRequest().authenticated()
);
7. From @EnableGlobalMethodSecurity to @EnableMethodSecurity
The @EnableGlobalMethodSecurity annotation switches on method security, that is, the @PreAuthorize, @PostAuthorize and @Secured checks on service methods. The annotation is deprecated but still compiles on 7.1. Its replacement @EnableMethodSecurity, added in Spring Security 5.6, switches on @PreAuthorize and @PostAuthorize by default, so the prePostEnabled = true attribute goes away.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
// ...
}
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
// ...
}
The service code does not change. In the recipe API, the URL /recipes/secret is open to every logged-in user, but the service method behind it allows only the ADMIN role. So the user ravi gets a 403 from the method check and not from the URL rules.
@PreAuthorize("hasRole('ADMIN')")
public String secretRecipe() {
return "grandma's cake";
}
If the app still uses @Secured or the JSR-250 @RolesAllowed, we switch those on with @EnableMethodSecurity(securedEnabled = true) or jsr250Enabled = true, the same attribute names as before.
8. CSRF and Form Login in the Lambda DSL
Every other option of HttpSecurity follows the same pattern as authorizeHttpRequests(). The old chain http.csrf().disable().and().formLogin().loginPage(“/login”) no longer compiles, because the no-argument csrf() and formLogin() methods and and() were removed in Spring Security 7.0. Each option becomes one call with a lambda, and the calls are chained on http itself.
http.csrf().ignoringAntMatchers("/api/**")
.and()
.formLogin().loginPage("/login").permitAll()
.and()
.logout().logoutSuccessUrl("/");
http
.csrf(csrf -> csrf.ignoringRequestMatchers("/api/**"))
.formLogin(form -> form.loginPage("/login").permitAll())
.logout(logout -> logout.logoutSuccessUrl("/"));
The most common options use the same few calls.
| Old chain (Spring Security 5) | Lambda form (Spring Security 7) |
|---|---|
| .csrf().disable() | .csrf(csrf -> csrf.disable()) |
| .csrf().ignoringAntMatchers(“/api/**”) | .csrf(csrf -> csrf.ignoringRequestMatchers(“/api/**”)) |
| .formLogin() | .formLogin(withDefaults()) |
| .formLogin().loginPage(“/login”).permitAll() | .formLogin(form -> form.loginPage(“/login”).permitAll()) |
| .httpBasic() | .httpBasic(withDefaults()) |
| .sessionManagement().sessionCreationPolicy(STATELESS) | .sessionManagement(session -> session.sessionCreationPolicy(STATELESS)) |
| .logout().logoutSuccessUrl(“/”) | .logout(logout -> logout.logoutSuccessUrl(“/”)) |
CSRF protection stays on by default, as before. A stateless REST API that is called with a token in a header can disable it. An app with a browser login form keeps it and lets Thymeleaf or JSP add the hidden token to each form. The form login and HTTP Basic articles show complete apps in the lambda style.
9. Testing the New Configuration
A migration is complete when the same requests get the same answers as before, so we verify it with a few MockMvc tests. Spring Boot 4 provides MockMvcTester, which runs a request through the full filter chain without a server, and the spring-boot-starter-security-test starter adds httpBasic() and @WithMockUser to log a test user in.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security-test</artifactId>
<scope>test</scope>
</dependency>
The following example is one test each for an open URL, a protected URL without a login, the same URL with HTTP Basic, and the admin URL with the ADMIN role. The @AutoConfigureMockMvc annotation moved to the package org.springframework.boot.webmvc.test.autoconfigure in Spring Boot 4.
@SpringBootTest
@AutoConfigureMockMvc
class SecurityConfigTest {
@Autowired
MockMvcTester mvc;
@Test
void publicUrlIsAllowedWithoutLogin() {
mvc.get().uri("/recipes")
.assertThat()
.hasStatus(HttpStatus.OK)
.bodyJson().isEqualTo("[\"pasta\",\"salad\",\"soup\"]");
}
@Test
void protectedUrlIsDeniedWithoutLogin() {
mvc.get().uri("/recipes/mine")
.assertThat()
.hasStatus(HttpStatus.UNAUTHORIZED);
}
@Test
void protectedUrlIsAllowedWithBasicAuth() {
mvc.get().uri("/recipes/mine").with(httpBasic("ravi", "pass"))
.assertThat()
.hasStatus(HttpStatus.OK)
.bodyJson().isEqualTo("[\"omelette\"]");
}
@Test
@WithMockUser(username = "admin", roles = "ADMIN")
void adminUrlIsAllowedForAdminRole() {
mvc.get().uri("/admin/recipes")
.assertThat()
.hasStatus(HttpStatus.OK)
.hasBodyTextEqualTo("all recipes of all users");
}
}
The denied request gets a 401 and not a redirect, because the request does not ask for HTML in its Accept header. With both httpBasic() and formLogin() on, Spring Security sends a browser to the login page and answers an API client with 401. The same app answers curl the same way.
curl localhost:8080/recipes # ["pasta","salad","soup"]
curl localhost:8080/recipes/mine # 401
curl -u ravi:pass localhost:8080/recipes/mine # ["omelette"]
curl -u ravi:pass localhost:8080/admin/recipes # 403
curl -u admin:pass localhost:8080/admin/recipes # all recipes of all users
curl -u ravi:pass localhost:8080/recipes/secret # 403 (method security)
The project has four more tests, for ravi on the admin URL, for ravi and admin on the secret recipe, and for the ignored /css/** path. All eight pass with mvn test.
[INFO] Running com.howtodoinjava.demo.security.SecurityConfigTest
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
10. WebSecurityConfigurerAdapter FAQs
10.1. Why Was WebSecurityConfigurerAdapter Deprecated?
The Spring Security team wanted every part of the configuration to be a bean, so that it can be replaced or tested like any other bean. The adapter forced all of it into one subclass, and apps with two filter chains had to extend the adapter twice with an @Order on each. With SecurityFilterChain beans, a second chain is one more @Bean method with a securityMatcher().
10.2. Can We Keep WebSecurityConfigurerAdapter on Spring Boot 3 or 4?
No. The class exists only up to Spring Security 5.8. Spring Boot 3.0 ships Spring Security 6.0 and Spring Boot 4.0 ships Spring Security 7.0, and neither contains the class. Spring Security 5.8 still runs on Spring Boot 2.7 and already has the new API. So an app can move to 5.8 first, replace the adapter while the old code still compiles, and upgrade Spring Boot after that.
10.3. Why Is User.withDefaultPasswordEncoder() Deprecated?
The method is marked deprecated to warn that it is unsafe for production, and the Javadoc says there are no plans to remove it. It hashes the password at runtime, so the plain text password is in the compiled class and in memory until the hash is built. For a demo it is fine. For a real app, we store hashed passwords and inject a PasswordEncoder bean as in section 4.
10.4. Is There a Tool That Migrates the Code for Us?
Yes, OpenRewrite has recipes for the Spring Boot 3 upgrade that rewrite WebSecurityConfigurerAdapter subclasses into SecurityFilterChain beans and change antMatchers() to requestMatchers(). The recipes handle the common shapes, so we still read the result, run the tests and move the remaining pieces such as the AuthenticationManager bean by hand.
11. Conclusion
A Spring Security configuration without WebSecurityConfigurerAdapter is a @Configuration class with beans. The SecurityFilterChain bean holds the URL rules and login options in the lambda DSL. A UserDetailsService bean with a PasswordEncoder bean replaces inMemoryAuthentication() and withDefaultPasswordEncoder(). A WebSecurityCustomizer bean replaces configure(WebSecurity), and @EnableMethodSecurity replaces @EnableGlobalMethodSecurity.
On Spring Security 7, the lambda form is the only form. The and() method, authorizeRequests() and the no-argument options such as csrf() are gone, and antMatchers() was removed earlier, in 6.0. A handful of MockMvc tests, for an open URL and for a protected URL with and without a login, show that the migrated chain behaves like the old one.
12. References
- Spring Security without the WebSecurityConfigurerAdapter (Spring blog)
- Java Configuration (Spring Security reference)
- Preparing for Spring Security 7, Configuration Migrations
- Preparing for Spring Security 7, Web Migrations
- Authorize HttpServletRequests
- Method Security
- User.withDefaultPasswordEncoder() Javadoc
- Testing with MockMvc
Happy Learning !!