Spring Security Upgrade Guide: Migrating from Deprecated WebSecurityConfigurerAdapter to Modern Configuration
If you’ve worked with Spring Boot and Spring Security for any length of time, the `WebSecurityConfigurerAdapter` class was likely the cornerstone of your security setup. It was the familiar, reliable way to configure everything from authentication mechanisms to authorization rules. However, with the release of Spring Security 5.7.0 and the subsequent arrival of Spring Boot 3, this trusty adapter has been deprecated and is now fully removed. This change isn’t just a cosmetic update; it represents a fundamental shift towards a more modular, flexible, and testable security configuration paradigm.
This comprehensive guide will walk you through why this change was made, explain the modern component-based approach, and provide detailed, step-by-step instructions with code examples to help you seamlessly migrate your existing applications. By the end, you’ll not only understand how to update your code but also appreciate the significant benefits the new configuration style brings to your projects.
Why Was WebSecurityConfigurerAdapter Deprecated?
The decision to deprecate `WebSecurityConfigurerAdapter` was driven by a desire to better align Spring Security with the broader evolution of the Spring Framework. The classic approach, while functional, had several architectural drawbacks that the new component-based model elegantly solves. The core philosophy is to move away from a single, monolithic configuration class and embrace a collection of discrete, purpose-built Spring beans.
The primary reasons for its deprecation include:
- Tight Coupling: Extending `WebSecurityConfigurerAdapter` tightly coupled your security configuration to the framework’s implementation details. Your class was forced into an inheritance hierarchy, making it less of a simple configuration component and more of a framework extension.
- Hidden Global State: The adapter often worked by modifying a global `HttpSecurity` object behind the scenes. This could lead to unpredictable behavior, especially in complex applications where multiple configurations might interact in non-obvious ways. The new model makes the configuration explicit and self-contained.
- Difficulty with Conditional Configuration: Applying security rules conditionally (e.g., enabling certain features only in a specific profile) was cumbersome. The new bean-based approach allows you to leverage all of Spring’s powerful conditional annotations (like `@Profile` or `@ConditionalOnProperty`) directly on your security beans.
- Reduced Testability: Testing a large, monolithic configuration class is more difficult than testing small, focused beans. The new approach promotes unit testing of individual security components in isolation.
The Modern Approach: Component-Based Configuration
The future of Spring Security configuration is built entirely around Spring’s dependency injection (DI) and Inversion of Control (IoC) principles. Instead of extending a class and overriding its methods, you now define one or more `@Bean` methods that return security-specific components. The most important of these is the `SecurityFilterChain`.
A `SecurityFilterChain` is an ordered chain of security filters that Spring Security applies to incoming HTTP requests. By defining a `SecurityFilterChain` as a bean, you are providing a complete, self-contained set of security rules for a specific set of URL patterns. You can have multiple such beans, each handling a different part of your application, for example, one for your REST API and another for your web UI.
Step-by-Step Migration: A Practical Example
Let’s dive into a concrete example. We’ll take a typical security configuration using `WebSecurityConfigurerAdapter` and convert it to the modern `SecurityFilterChain` bean style.
The “Before”: Classic WebSecurityConfigurerAdapter
Here is a common security configuration you might find in an older Spring Boot application. It configures form-based login, HTTP Basic authentication, and secures all endpoints, requiring users to be authenticated.
// This class is now DEPRECATED and should NOT be used in new applications.
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class LegacySecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatchers("/css/**", "/js/**", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(formLogin -> formLogin
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.permitAll()
);
}
}
The “After”: Using a SecurityFilterChain Bean
Now, let’s migrate the above configuration to the modern, component-based approach. The logic remains identical, but the structure is completely different.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class ModernSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/css/**", "/js/**", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(formLogin -> formLogin
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.permitAll()
);
return http.build();
}
}
Code Breakdown: What Changed?
Let’s analyze the key differences between the “before” and “after” examples to understand the migration process clearly.
- No More Inheritance: The new `ModernSecurityConfig` class is a plain POJO. It no longer extends `WebSecurityConfigurerAdapter`. This immediately decouples our configuration from the framework’s internal structure.
- The `@Bean` Annotation is Key: The configuration logic is now encapsulated within a method annotated with `@Bean`. This method, `securityFilterChain`, tells the Spring container to create and manage a bean of type `SecurityFilterChain`.
- `HttpSecurity` as a Method Parameter: Instead of getting a `HttpSecurity` object from an overridden `configure` method, it is now injected directly by Spring as a parameter into our bean definition method.
- Return a `SecurityFilterChain`: The bean method must return a `SecurityFilterChain` object. This is accomplished by calling `http.build()` at the end of the configuration chain. This finalizes the configuration and produces the immutable filter chain object.
- Method Name Change: Note the change from `authorizeRequests()` to `authorizeHttpRequests()`. This is a subtle but important change in newer Spring Security versions for clarity and consistency. Similarly, `antMatchers()` has been superseded by `requestMatchers()`. While `antMatchers()` still works, `requestMatchers()` is the recommended, more flexible replacement.
Migrating Common Configurations
Beyond basic authorization, you often need to configure other aspects of web security, such as ignoring static resources or defining an `AuthenticationManager`.
Configuring a WebSecurityCustomizer for Static Resources
Previously, you might have used `WebSecurity.ignoring()` to completely bypass the security filter chain for static assets like CSS or JavaScript files. The modern equivalent is to define a `WebSecurityCustomizer` bean.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
@Configuration
public class WebSecurityCustomizerConfig {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
// Using a lambda expression for conciseness
return (web) -> web.ignoring().requestMatchers("/css/**", "/js/**", "/images/**");
}
}
Note: It’s generally preferred to handle static resources using `.permitAll()` inside your `SecurityFilterChain` as shown in our main example. Using `web.ignoring()` completely removes the requests from the security context, which can be a security risk if not handled carefully. Use `permitAll()` unless you have a specific performance reason to bypass the filter chain entirely.
Configuring AuthenticationManager
In the old model, the `AuthenticationManager` was often implicitly available. To expose it as a bean in the new model, which is necessary for manual authentication or integration with other parts of your application, you can get it from the `AuthenticationConfiguration`.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
@Configuration
public class AppConfig {
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
This simple bean definition makes the `AuthenticationManager` available for dependency injection anywhere in your application.
Handling Multiple SecurityFilterChain Beans
One of the most powerful features of the new model is the ability to define multiple, independent security filter chains. This is perfect for applications that have distinct security requirements for different parts of the system, such as a stateful web UI and a stateless REST API.
You can define multiple `SecurityFilterChain` beans. Spring Security will use the `@Order` annotation to determine which chain to apply first. The first chain that matches a request’s URL pattern will be used.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class MultipleSecurityConfigs {
@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // Apply this chain only to paths starting with /api/
.authorizeHttpRequests(authorize -> authorize
.anyRequest().hasRole("API_USER")
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.httpBasic(); // Use HTTP Basic for the API
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(); // Use Form Login for the web UI
return http.build();
}
}
In this example, requests to `/api/**` will be handled by the first, stateless filter chain, while all other requests will fall through to the second, stateful chain with form login.
What About Method Security (@PreAuthorize, etc.)?
The migration also simplifies how you enable method-level security. The old `@EnableGlobalMethodSecurity` annotation has also been deprecated. The new, recommended approach is to use `@EnableMethodSecurity`.
This single annotation is more powerful and flexible. It enables Spring’s pre-post annotations (`@PreAuthorize`, `@PostAuthorize`, etc.) by default and has attributes for enabling JSR-250 and `@Secured` annotations if needed.
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableMethodSecurity(
// jsr250Enabled = true, // Uncomment to enable @RolesAllowed
// securedEnabled = true // Uncomment to enable @Secured
)
public class MethodSecurityConfig {
// No other beans or methods are needed here, just the annotation.
}
Conclusion and Best Practices
Migrating away from `WebSecurityConfigurerAdapter` is a necessary step to keep your Spring applications modern, secure, and maintainable. While it requires a shift in mindset, the component-based approach offers clear advantages in modularity, testability, and flexibility. It fully embraces the dependency injection principles that make the Spring Framework so powerful.
As you perform your migration, keep these best practices in mind:
- One Responsibility per Bean: Create separate `SecurityFilterChain` beans for distinct areas of your application (e.g., API vs. UI). This keeps your configuration clean and easy to understand.
- Embrace the Lambda DSL: The Lambda DSL, as shown in the examples, provides a more readable and concise way to configure `HttpSecurity`. It’s the standard for modern Spring Security.
- Be Specific with Matchers: Use specific `securityMatcher()` or `requestMatchers()` to clearly define the scope of each `SecurityFilterChain`. Avoid overly broad patterns that could have unintended side effects.
- Centralize User Details: Keep your `UserDetailsService` bean or `AuthenticationProvider` beans separate from your `SecurityFilterChain` configuration to maintain a clean separation of concerns.
By following this guide, you can confidently upgrade your Spring Security configuration, paving the way for more robust and manageable application security for years to come.
“`