<link rel="stylesheet" href="styles-3GBANQL6.css">
← Back to blog

Spring Boot

Modernizing Spring Security: Replacing WebSecurityConfigurerAdapter

Modernizing Spring Security: Replacing WebSecurityConfigurerAdapter

Introduction

With the recent updates in Spring Security, the WebSecurityConfigurerAdapter has been deprecated. This change encourages developers to adopt a more functional style of security configuration. In this article, we will explore how to upgrade your Spring Security setup by configuring authentication, HTTP security, and web security without relying on the deprecated adapter. We’ll also provide examples of how to test various endpoints based on user roles such as anonymous, user, and admin.

Setting Up Your Spring Boot Application

Before we dive into the configuration, let’s ensure that we have a Spring Boot application set up with the necessary dependencies. You can start with the following dependencies in your pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

Configuring Authentication

In Spring Security 5 and later, you can define your authentication mechanism using SecurityFilterChain. Here’s how to configure in-memory authentication with user roles.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
                .requestMatchers("/api/anonymous/**").permitAll()
                .anyRequest().authenticated()
            )
            .httpBasic()
            .and()
            .csrf().disable();

        return http.build();
    }

    @Bean
    public AuthenticationManager authManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder = 
            http.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder
            .inMemoryAuthentication()
            .withUser("user").password(passwordEncoder().encode("password")).roles("USER")
            .and()
            .withUser("admin").password(passwordEncoder().encode("admin")).roles("ADMIN");
        
        return authenticationManagerBuilder.build();
    }
}

Explanation

- We create a PasswordEncoder bean to handle password encoding. - The SecurityFilterChain configures authorization rules for different endpoints based on user roles. - The AuthenticationManager is set up for in-memory authentication with two users: one with the role of USER and another with ADMIN.

Configuring HTTP Security

In addition to authentication, configuring HTTP security is crucial for protecting your application's endpoints. The previous example already contains HTTP security configurations, but let’s elaborate a bit more:

http
    .authorizeHttpRequests(authorize -> authorize
        .requestMatchers("/api/admin/**").hasRole("ADMIN")
        .requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
        .requestMatchers("/api/anonymous/**").permitAll()
        .anyRequest().authenticated()
    )
    .httpBasic()
    .and()
    .csrf().disable();

- The requestMatchers method specifies which roles are allowed to access specific endpoints. - permitAll() allows anonymous access to certain paths, while authenticated() requires users to be logged in. - The configuration disables CSRF protection, which you may want to reconsider depending on your application requirements.

Configuring Web Security

Web security configurations can also include additional settings such as CORS or session management. Here’s a simple configuration:

http
    .cors()
    .and()
    .sessionManagement(session -> session
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    );

- This setup allows CORS requests and configures the application to be stateless, meaning it does not store any session information on the server side.

Endpoints Controller

Let’s create a simple REST controller to demonstrate how our security settings apply to different endpoints based on user roles.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    @GetMapping("/api/anonymous/info")
    public String anonymousInfo() {
        return "This is public information accessible by anyone.";
    }

    @GetMapping("/api/user/info")
    public String userInfo() {
        return "This is information accessible by authenticated users.";
    }

    @GetMapping("/api/admin/info")
    public String adminInfo() {
        return "This is sensitive information accessible only by admins.";
    }
}

Testing Endpoints

You can test the endpoints using tools like Postman or cURL. Here’s how to test the different roles:

1. Role: Anonymous User

To test the anonymous endpoint, make a GET request to /api/anonymous/info. This should return a public message without authentication.

2. Role: User

To test the user role, authenticate using the username user and password password. Make a GET request to /api/user/info. You should receive a message indicating that the information is accessible to authenticated users.

3. Role: Admin

For the admin role, authenticate using the username admin and password admin. Make a GET request to /api/admin/info. You should see a message stating that the information is only accessible by admins.

Comprehensive Testing

To ensure that our security configurations are functioning as expected, we can implement comprehensive tests for all roles. Here's how to set up unit tests using JUnit and MockMvc:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(UserController.class)
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testAnonymousUser() throws Exception {
        mockMvc.perform(get("/api/anonymous/info"))
            .andExpect(status().isOk())
            .andExpect(content().string("This is public information accessible by anyone."));
    }

    @WithMockUser(username = "user", roles = {"USER"})
    @Test
    public void testUserRole() throws Exception {
        mockMvc.perform(get("/api/user/info"))
            .andExpect(status().isOk())
            .andExpect(content().string("This is information accessible by authenticated users."));
    }

    @WithMockUser(username = "admin", roles = {"ADMIN"})
    @Test
    public void testAdminRole() throws Exception {
        mockMvc.perform(get("/api/admin/info"))
            .andExpect(status().isOk())
            .andExpect(content().string("This is sensitive information accessible only by admins."));
    }

    @WithMockUser(username = "user", roles = {"USER"})
    @Test
    public void testAdminRoleAccessDenied() throws Exception {
        mockMvc.perform(get("/api/admin/info"))
            .andExpect(status().isForbidden());
    }
}

Explanation of Tests

- The testAnonymousUser method tests the anonymous endpoint and expects a 200 OK status and the correct response message. - The testUserRole method tests the user role access to the user endpoint, expecting a successful response. - The testAdminRole method checks if the admin role can access the admin endpoint. - The testAdminRoleAccessDenied method ensures that a user without admin privileges cannot access the admin endpoint, expecting a 403 Forbidden response.

Conclusion

Upgrading from the deprecated WebSecurityConfigurerAdapter to the new functional style of Spring Security configuration enhances the clarity and maintainability of your security setup. By defining security rules, authentication methods, and endpoint access levels in a structured way, you ensure a more robust security posture for your application. As you adopt these new practices, remember to keep your dependencies updated and explore the extensive features that Spring Security provides.

LET'S BUILD WHAT'S NEXT

Ready to turn your idea into a real solution?

Let's discuss how we can help you create value with technology.

Let's Talk