Replace deprecated WebSecurityConfigurerAdapter in Spring Security 5.7.x

Replace deprecated WebSecurityConfigurerAdapter in Spring Security 5.7.x

Spring Security for authentication and authorization

Spring Security is an open source security framework that provides permission-based access control, authentication, security event publishing, and other features. Using Spring Security in a Spring Boot application makes it very easy to implement user authentication and authorization.

Authentication in Spring Security

Spring Security authentication is implemented through the AuthenticationManager interface, which is an authentication manager used to authenticate users. In Spring Security, the default implementation of the AuthenticationManager interface is the ProviderManager.

The ProviderManager is an authentication manager that contains one or more AuthenticationProvider implementations for authenticating users. the AuthenticationProvider interface is an authentication provider for authenticating users. In Spring Security, the default implementation of AuthenticationProvider is DaoAuthenticationProvider.

The DaoAuthenticationProvider is an authentication provider that is used to authenticate users. It requires a UserDetailsService implementation to obtain user information and passwords and then uses PasswordEncoder for password verification; the UserDetailsService interface is a user details service interface to obtain user information and passwords. The PasswordEncoder interface is a password encoder interface used to encode and decode passwords.

The following is an example of a basic Spring Security configuration to implement authentication:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/user/**").hasRole("USER")
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .logout()
            .and()
            .csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder);
    }
  }

In the code above, Spring Security is enabled using the @EnableWebSecurity annotation. configure(HttpSecurity http) method is used to configure access control, specifying which URLs require which roles to access and that any requests require authentication. formLogin() method enables form-based authentication, logout() method enables logout support, and csrf().disable() method disables CSRF protection.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/user/**").hasRole("USER")
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .logout()
            .and()
            .csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder);
    }

The configure(AuthenticationManagerBuilder auth) method is used to configure authentication, specifying which UserDetailsService implementation to use to obtain user information and passwords, and which PasswordEncoder implementation to use for password verification.

Authorization in Spring Security

Authorisation in Spring Security is implemented through the AccessDecisionManager interface, which is an access decision manager that determines whether a user has permission to access a resource. In Spring Security, the default implementation of the AccessDecisionManager interface is AffirmativeBased.

AffirmativeBased is an access decision manager that contains one or more AccessDecisionVoter implementations that determine whether a user has permission to access a resource. the AccessDecisionVoter interface is a voter that determines whether a user has permission to access a resource. In Spring Security, the default implementation of the AccessDecisionVoter is the RoleVoter.

RoleVoter is a voter that determines whether a user has access to a resource based on the user’s role. In Spring Security, we can customise the voter by implementing the AccessDecisionVoter interface to decide if a user has access to a resource based on their needs.

The following is a basic Spring Security configuration example for implementing authorization:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/user/**").hasRole("USER")
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .logout()
            .and()
            .csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder);
    }

In the above code, a custom AccessDecisionVoter instance is created using the @Bean annotation for custom voting logic. In the configure(HttpSecurity http) method, the custom AccessDecisionVoter instance is added to the Access Decision Manager via the accessDecisionManager() method.

Complete sample code

The following is a complete Spring Security configuration example code to implement authentication and authorization.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/user/**").hasRole("USER")
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .logout()
            .and()
            .csrf().disable()
            .exceptionHandling()
            .accessDeniedPage("/403");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder);
    }

    @Bean
    public AccessDecisionVoter<Object> accessDecisionVoter(){
        RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy());
        return roleHierarchyVoter;
    }

    @Bean
    public RoleHierarchyImpl roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
        return roleHierarchy;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

In the code above, Spring Security is enabled using the @EnableWebSecurity annotation. configure(HttpSecurity http) method is used to configure access control, specifying which URLs require which roles to access and that any requests require authentication. formLogin() method enables form-based authentication, the logout() method enables logout support, the csrf().disable() method disables CSRF protection, and the accessDeniedPage() method is used to specify the route to be redirected when access is denied.

The configure(AuthenticationManagerBuilder auth) method is used to configure authentication, specifying which UserDetailsService implementation to use to obtain user information and passwords, and which PasswordEncoder implementation to use for password verification.

The accessDecisionVoter() method creates a custom AccessDecisionVoter instance that is used to customise the voting logic. In this example we have used the RoleHierarchyVoter class to implement a voting logic based on role inheritance relationships. the RoleHierarchyImpl class is used to define role inheritance relationships.

The passwordEncoder() method is used to create a password encoder instance, here we use the BCryptPasswordEncoder class to encode the password.

Finally, we need to implement the UserDetailsService interface, which is used to obtain user information and passwords. The following is a simple example implementation:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));

        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                user.getRoles().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
    }
}

In the above code, we use the UserRepository class to get the user information and password and return it wrapped in a UserDetails instance. In this example, we have used the org.springframework.security.core.userdetails.User class to implement the UserDetails interface.

Spring Security deprecated WebSecurityConfigurerAdapter class in its 5.7.0 version as it encourages us to use component based configuration instead. However, this might confuse many people, since internet is full of examples for WebSecurityConfigurerAdapter. In this blog post, I show how to convert WebSecurityConfigurerAdapter configuration to component based configuration.

Here, you can see a security configuration class extends WebSecurityConfigurerAdapter as most Java developers used to see. It basically has a custom filter, two custom authentication provider and bunch of antMatcher() settings.

Let's convert this class to component based version.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtTokenFilter jwtTokenFilter;

    // Injecting JWT custom authentication provider
    @Autowired
    JwtAuthenticationProvider customAuthenticationProvider;

    // Injecting Google custom authentication provider
    @Autowired
    GoogleCloudAuthenticationProvider googleCloudAuthenticationProvider;

    @Bean
    public AuthenticationManager getAuthenticationManager() throws Exception {
        return super.authenticationManagerBean();
    }

    // adding our custom authentication providers
    // authentication manager will call these custom provider's
    // authenticate methods from now on.
    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(customAuthenticationProvider)
            .authenticationProvider(googleCloudAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // disabling csrf since we won't use form login
                .csrf().disable()
                // giving every permission to every request for /login endpoint
                .authorizeRequests().antMatchers("/login").permitAll()
                // for everything else, the user has to be authenticated
                .anyRequest().authenticated()
                // setting stateless session, because we choose to implement Rest API
                .and().sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // adding the custom filter before UsernamePasswordAuthenticationFilter in the filter chain
        http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

Converting AuthenticationManager bean

Old version (Before Spring Security 5.7.0)

@Bean
public AuthenticationManager getAuthenticationManager() throws Exception {
    return super.authenticationManagerBean();
}

New version (After Spring Security 5.7.0)

@Bean
AuthenticationManager authenticationManager(
        AuthenticationConfiguration authenticationConfiguration) throws Exception {
    return authenticationConfiguration.getAuthenticationManager();
}

2. Converting multiple authentication provider setup

Old version (Before Spring Security 5.7.0)

@Override
protected void configure(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(customAuthenticationProvider)
        .authenticationProvider(googleCloudAuthenticationProvider);
}

New version (After Spring Security 5.7.0)

@Autowired
void registerProvider(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(customAuthenticationProvider)
        .authenticationProvider(googleCloudAuthenticationProvider);
}

3. Converting HttpSecurity setup

Old version (Before Spring Security 5.7.0)

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        // disabling csrf since we won't use form login
        .csrf().disable()
        // giving every permission to every request for /login endpoint
        .authorizeRequests().antMatchers("/login").permitAll()
        // for everything else, the user has to be authenticated
        .anyRequest().authenticated()
        // setting stateless session, because we choose to implement Rest API
        .and().sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

    // adding the custom filter before UsernamePasswordAuthenticationFilter in the filter chain
    http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
}

New version (After Spring Security 5.7.0)

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // disabling csrf since we won't use form login
        .csrf().disable()
        // giving permission to every request for /login endpoint
        .authorizeRequests().antMatchers("/login").permitAll()
        // for everything else, the user has to be authenticated
        .anyRequest().authenticated()
        // setting stateless session, because we choose to implement Rest API
        .and().sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

    // adding the custom filter before UsernamePasswordAuthenticationFilter in the filter chain
    http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

Finally, security configuration should look like below. WebSecurityConfiguration does not extend WebSecurityConfigurerAdapter anymore, so I do not rely on WebSecurityConfigurerAdapter's methods. Instead, I create objects required for the configurations with @Bean annotation.

I also used @Autowired to access AuthenticationManagerBuilder, so I could set up my custom authentication providers.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class WebSecurityConfiguration { 

    @Autowired
    private JwtTokenFilter jwtTokenFilter;

    // Injecting JWT custom authentication provider
    @Autowired
    JwtAuthenticationProvider customAuthenticationProvider;

    // Injecting Google custom authentication provider
    @Autowired
    GoogleCloudAuthenticationProvider googleCloudAuthenticationProvider;

    @Bean
    AuthenticationManager authenticationManager(
        AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    // adding our custom authentication providers
    // authentication manager will call these custom provider's
    // authenticate methods from now on.
    @Autowired
    void registerProvider(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(customAuthenticationProvider)
            .authenticationProvider(googleCloudAuthenticationProvider);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // disabling csrf since we won't use form login
            .csrf().disable()
            // giving permission to every request for /login endpoint
            .authorizeRequests().antMatchers("/login").permitAll()
            // for everything else, the user has to be authenticated
            .anyRequest().authenticated()
            // setting stateless session, because we choose to implement Rest API
            .and().sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // adding the custom filter before UsernamePasswordAuthenticationFilter in the filter chain
        http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

Conclusion

Spring Security making a flow it very easy to protect applications against malicious attacks and data leaks.

https://github.com/redhabayuanggara/backendstory/tree/main/spring-security-upgrade