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.
