Blog
navigate_next
Java
Securing Spring Boot 3 Applications with Spring Security 6.1 and Beyond
Gaurav Sharma
April 14, 2024

The most recent update to Spring Security brings several significant changes, enhancing how security can be integrated into Spring Boot applications. This guide aims to demystify the process of applying these updates in your Spring Boot project with the Spring Security module.

Key updates in Spring Security 6.1 include:

  1. The transition from AntMatchers to RequestMatchers: Moving towards a more versatile way to match requests.
  2. Deprecation of WebSecurityConfigurerAdapter: Shifting away from this class to encourage newer configuration methods.
  3. Adoption of DSL Lambdas: Replacing older configurations with more concise and flexible DSL lambda expressions.
  4. Introduction of the SecurityFilterChain: A new approach for configuring security filters, enhancing customization and clarity.

We will examine each change in detail in the latter portion of the article.

Implementing security in a web application can often seem daunting. Still, by the end of this article, you'll gain a solid grasp on effectively integrating Spring Security into any Spring Boot 3 web application.

Remember, irrespective of your web application's size, one has to configure security in their application just once and can do modifications on top of it whenever required.

Chapter 0: Introduction

In this article, we will explore how to secure a web application developed with the latest version of Spring Boot, utilizing the most recent updates in Spring Security. Our journey will take us through creating a Spring Boot web project, its integration with a PostgreSQL database via Spring Data JPA, and the application of security measures provided by the updated Spring Security framework.

This article is split mainly into two parts.

Part One: Developing an Employee Management System with CRUD Operations

In this initial phase, we will focus on crafting an employee management system featuring fundamental CRUD (Create, Read, Update, Delete) operations. We'll establish the groundwork for our system, laying the foundation for subsequent enhancements.

Part Two: Enhancing Security with Spring Security for Endpoint Protection

The crux of our article lies in fortifying our endpoints with the robust security measures provided by Spring Security.

We'll delve into securing our application, ensuring only authorized users can access sensitive endpoints. This step elevates the integrity and confidentiality of our employee management system, enhancing its overall reliability and trustworthiness.

Chapter 1: Developing a simple employee-based management system

We won't go into the details of building a CRUD application since the focus of this article is on securing the application. However, a basic overview is provided for better understanding.

We have included the following dependencies in our project:

  1. Spring Web: Enables building web applications with Spring, including RESTful services.
  2. PostgreSQL Driver: Connects your application to a PostgreSQL database for data storage.
  3. Spring Data JPA: Simplifies data access and manipulation through JPA repositories.
  4. Lombok: Reduces boilerplate code by automatically generating getters, setters, and other common methods.
  5. Spring Boot DevTools: Provides fast application restarts, live reload, and configuration options for a smoother development process.

Employee.java

This Java class defines an <span class="pink">Employee</span> entity with attributes such as <span class="pink">employeeId</span>, <span class="pink">name</span>, <span class="pink">email</span>, <span class="pink">department</span>, and <span class="pink">company</span>, utilizing JPA annotations for ORM and Lombok annotations for boilerplate code like getters, setters, and constructors.


package com.unlogged.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long employeeId;
    private String name;
    private String email;
    private String department;
    private String company;

}

EmployeeRepository.java

This interface defines a repository for <span class="pink">Employee</span> entities, extending Spring Data JPA to facilitate database operations.


package com.unlogged.repo;

import com.unlogged.model.Employee;
import org.springframework.data.jpa.repository.JpaRepository;

public interface EmployeeRepository extends JpaRepository<Employee, Integer>{
}


EmployeeController.java

This controller manages <span class="pink">Employee</span> data, offering endpoints to create, retrieve, update, and delete employees using an <span class="pink">EmployeeService</span>.


package com.unlogged.controller;

import com.unlogged.model.Employee;
import com.unlogged.service.EmployeeService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/employees")
public class EmployeeController {

    private final EmployeeService service;

    public EmployeeController(EmployeeService service) {
        this.service = service;
    }

    @GetMapping
    public List<Employee> getAllEmployees() {
        return service.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Employee> getEmployeeById(@PathVariable Integer id) {
        return service.findById(id)
                .map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PostMapping
    public Employee createEmployee(@RequestBody Employee employee) {
        return service.save(employee);
    }


    @PutMapping("/{id}")
    public ResponseEntity<Employee> updateEmployee(@PathVariable Integer id, @RequestBody Employee employeeDetails) {
        return service.updateEmployee(id, employeeDetails)
                .map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<?> deleteEmployee(@PathVariable Integer id) {
        return service.findById(id)
                .map(employee -> {
                    service.deleteById(id);
                    return ResponseEntity.ok().build();
                })
                .orElseGet(() -> ResponseEntity.notFound().build());
    }
}


EmployeeService.java

The <span class="pink">EmployeeService</span> class interact with the <span class="pink">EmployeeRepository</span> to perform CRUD operations on <span class="pink">Employee</span> entities, allowing for the creation, retrieval, updating, and deletion of employee records in the database.


package com.unlogged.service;

import com.unlogged.model.Employee;
import com.unlogged.repo.EmployeeRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class EmployeeService {

    private final EmployeeRepository repository;

    public EmployeeService(EmployeeRepository repository) {
        this.repository = repository;
    }

    public List<Employee> findAll() {
        return repository.findAll();
    }

    public Optional<Employee> findById(Integer id) {
        return repository.findById(id);
    }

    public Employee save(Employee employee) {
        return repository.save(employee);
    }

    public void deleteById(Integer id) {
        repository.deleteById(id);
    }

    public Optional<Employee> updateEmployee(Integer id, Employee employeeDetails) {
        return repository.findById(id).map(employee -> {
            employee.setName(employeeDetails.getName());
            employee.setEmail(employeeDetails.getEmail());
            employee.setDepartment(employeeDetails.getDepartment());
            employee.setCompany(employeeDetails.getCompany());
            return Optional.of(repository.save(employee));
        }).orElse(Optional.empty());
    }

}


💡 Our application is configured to automatically populate the database with initial data at startup, utilizing SQL files named <span class="pink">schema.sql</span> and <span class="pink">data.sql</span>.

schema.sql


CREATE TABLE employee (
                          employee_id SERIAL PRIMARY KEY,
                          name VARCHAR(255),
                          email VARCHAR(255),
                          department VARCHAR(255),
                          company VARCHAR(255)
);

data.sql


INSERT INTO Employee (name, email, department, company) VALUES ('Aarav Kumar', 'aarav.kumar@example.com', 'HR', 'Tech Innovations Pvt Ltd');
INSERT INTO Employee (name, email, department, company) VALUES ('Diya Sharma', 'diya.sharma@example.com', 'Marketing', 'Creative Minds Ltd');
INSERT INTO Employee (name, email, department, company) VALUES ('Rohan Gupta', 'rohan.gupta@example.com', 'Finance', 'Financial Solutions Inc');
INSERT INTO Employee (name, email, department, company) VALUES ('Isha Patel', 'isha.patel@example.com', 'IT', 'Tech Solutions Pvt Ltd');
INSERT INTO Employee (name, email, department, company) VALUES ('Aditya Singh', 'aditya.singh@example.com', 'Operations', 'Manufacturing Corp');

application.properties


spring.application.name=EmployeeManagementSystem
spring.datasource.url=jdbc:postgresql://localhost:5432/employeeDb
spring.datasource.username=postgres
spring.datasource.password=12345
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
server.port=8080
spring.sql.init.mode=always
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

Chapter 2.0: Protecting our Web Application using Default Spring Security Configuration

💡 Note: Adding Spring Security to your Spring Boot project automatically makes it safer. This is because the creators of Spring decided they wanted every application to be secure right from the start.

How It Works

Once you include Spring Security in your project, it instantly sets up some security features for you. This means your application will have a basic level of security without you needing to do anything extra.

For securing our web application, we need another dependency i.e, Spring Security:

Spring Security: Adds authentication and authorization features to secure your application.

Given that our project is built with Maven and Spring Boot, the dependency for Spring Security would appear in the <span class="pink">pom.xml</span> file as follows:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Our final <span class="pink">pom.xml</span> file would be structured as follows to incorporate the specified dependencies:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.unlogged</groupId>
    <artifactId>EmployeeManagementSystem</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>EmployeeManagementSystem</name>
    <description>EmployeeManagementSystem</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <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-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Note: After incorporating the security dependency and starting the spring boot project, a default login form is automatically provided, complete with fields for a username and password.

When attempting to access any API via the browser, the default login form will be presented like the one given below:

💡 The default username provided by spring security is "user," while the password is auto-generated and can be found in the console.

But this username password should not be used in real-time production scenarios

Here, we are trying to access an API i.e. to get the list of all employees. Attached is a small video to get a clearer picture.

Several limitations come with relying on the default Spring Security setup:

  1. Secures Everything: By default, it locks down all your endpoints, even the ones you might want to keep open.
  2. Not Flexible Enough: The preset security settings are quite general. If your app needs specific security tweaks, you might find these settings a bit limiting.
  3. Easy to Misconfigure: If you're not careful, sticking with the default settings could lead to security gaps or tricky bugs.
  4. One-Size-Fits-All: It treats all apps the same, security-wise, which might not work for apps with unique security needs.

💡 To achieve more precise control over our application's security mechanisms, like our own username, password, and password encryption for better authentication, and authorization of accessing certain APIs, we need to create a custom security configuration file that manages all these. Spring Security excels in offering flexibility for such customizations.

💡 Customizing is Hard Work: Want to change the default security setup? Brace yourself for some complex coding.

Chapter 2.1: Securing web application with our own custom security configuration

To set up our security system, we need to create a user class that includes fields such as username and password. This allows us to store user information in the database and authenticate users based on these credentials.

However, there’s an important aspect to note: Spring Security does not automatically recognize this custom user class. Instead, it works with its predefined <span class="pink">UserDetails</span> interface.

In simple terms, <span class="pink">UserDetails</span> is a special interface in Spring Security designed to handle user information in a way that Spring Security can understand. This means that for Spring Security to work with our custom user class, we need to adapt our class to fit this interface. Essentially, we need to convert our user class into one that implements the <span class="pink">UserDetails</span> interface.


package com.unlogged.model;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String username;
    private String password;

}

Explanation for the above code:

This code sets up a simple <span class="pink">User</span> class to store user information in a database, specifically their ID, username, and password.

implementing the UserDetails interface provided by spring security:

UserPrincipal.java


package com.unlogged.model;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Collections;

public class UserPrincipal implements UserDetails {

    private User user;

    public UserPrincipal(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singleton(new SimpleGrantedAuthority("USER"));
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}


Explanation for the above code:

The <span class="pink">UserPrincipal</span> class is a custom implementation of Spring Security's <span class="pink">UserDetails</span> interface, designed to integrate our own user model with Spring Security's authentication mechanisms.

This class acts as an adapter between our <span class="pink">User</span> class and what Spring Security expects in terms of user details.

Here’s a breakdown of its functionality:

  • Constructor: It takes an instance of our <span class="pink">User</span> class. This allows the <span class="pink">UserPrincipal</span> to access user-specific details like username and password.
  • getAuthorities(): This method specifies the roles or authorities granted to the user. In this case, every user is given a single authority of "USER".
  • getPassword() and getUsername(): These methods simply retrieve the password and username from the <span class="pink">User</span> instance, respectively.
  • Account Status Methods: The methods <span class="pink">isAccountNonExpired()</span>, <span class="pink">isAccountNonLocked()</span>, <span class="pink">isCredentialsNonExpired()</span>, and <span class="pink">isEnabled()</span> are all overridden to return <span class="pink">true</span>. These methods are used by Spring Security to determine if the account is still active, locked, has expired credentials, or is enabled. Returning <span class="pink">true</span> from all these methods suggests that in this simple implementation, these checks are not being used to restrict user access.

UserRepo.java


package com.unlogged.repo;

import com.unlogged.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepo extends JpaRepository<User, Integer> {

    public User findByUsername(String username);
}

Explanation for the above code:

The <span class="pink">findByUsername(String username)</span> method in the <span class="pink">UserRepo</span> interface is a specialized function that lets you find and retrieve a <span class="pink">User</span> based on their username. This method is set up so that Spring Data JPA can automatically handle the database search, meaning you don't have to write any additional SQL code. It returns the <span class="pink">User</span> object if it finds a match, or <span class="pink">null</span> if there is no user with that username.

UserService.java


package com.unlogged.service;

import com.unlogged.model.User;
import com.unlogged.model.UserPrincipal;
import com.unlogged.repo.UserRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserService implements UserDetailsService {
    @Autowired
    private UserRepo userRepo;
    private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);

    public User saveUser(User user) {
        user.setPassword(encoder.encode(user.getPassword()));
        return userRepo.save(user);
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepo.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("Error 404");
        } else {
            return new UserPrincipal(user);
        }
    }
}

Explanation for the above code:

The <span class="pink">UserService</span> class in our application has two main jobs: managing user information and helping with login security.

<span class="pink">UserDetailsService</span> is an interface provided by Spring Security that is used to retrieve user-related data. It has a single method, <span class="pink">loadUserByUsername(String username)</span>, which must be implemented to fetch a <span class="pink">UserDetails</span> object based on the username. The <span class="pink">UserDetails</span> interface itself is a core part of Spring Security, providing essential information (such as username, password, and granted authorities) necessary for security checks.

Here's a quick look at how it works:

  • User Repository and Password Encoder: This class connects to our database to access user information and uses a tool called <span class="pink">BCryptPasswordEncoder</span> to make passwords safe. This tool scrambles the passwords so they aren't easy to guess or steal.
  • saveUser Method: The <span class="pink">saveUser</span> method: Whenever we need to save a new user's information, this method first hashes the user's password and then saves their details to our database. This way, even if someone gets into our database, they won't easily decrypt the passwords.
  • loadUserByUsername Method: This method is all about finding the right user when someone tries to log in. It searches for a user by their username. If it finds the user, it prepares their information in a special format needed for checking who they are during login. If it can't find the user, it lets us know by throwing an error, which helps prevent strangers from getting in.

Overall, the <span class="pink">UserService</span> is key to keeping user details safe and making sure the right person logs in with the correct password.

UserController.java


package com.unlogged.controller;

import com.unlogged.model.User;
import com.unlogged.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@CrossOrigin
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/register")
    public ResponseEntity<String> userRegister(@RequestBody User user) {
        if (userService.saveUser(user) != null) {
            return new ResponseEntity<>("User Registered Successfully", HttpStatus.OK);
        } else {
            return new ResponseEntity<>("Oops! User not registered", HttpStatus.OK);
        }
    }
}

Explanation for the above code:

The <span class="pink">UserController</span> class has a method called <span class="pink">userRegister</span> that manages the process of signing up new users. When a user successfully registers, it sends back a message "User Registered Successfully"; if the registration fails, it responds with "Oops! User not registered."

And the most important class i.e. SecurityConfig.java  where we define all our latest security-related beans.

SecurityConfig.java


package com.unlogged.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(new BCryptPasswordEncoder(12));
        return provider;
    }


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf(AbstractHttpConfigurer::disable)
        .cors(Customizer.withDefaults())
                .authorizeHttpRequests(auth ->
                        auth
                                .requestMatchers("/register")
                                .permitAll().anyRequest()
                                .authenticated())
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .httpBasic(Customizer.withDefaults());


        return httpSecurity.build();
    }


}

Comparison with what we used to do in previous versions of spring security i.e below spring security 6:

  1. The Transition from AntMatchers to RequestMatchers:
    • Previously, <span class="pink">AntMatchers</span> were used to specify URL patterns to control access in Spring Security. Now, we use <span class="pink">RequestMatchers</span>, a more versatile tool that not only matches URLs but can also consider other factors like the type of HTTP request. This allows for more detailed and flexible security configurations.
  2. Deprecation of WebSecurityConfigurerAdapter:
    • The old way of setting up security configurations involved extending the <span class="pink">WebSecurityConfigurerAdapter</span> class. Spring Security has moved away from this to encourage using more modern methods like directly configuring a <span class="pink">SecurityFilterChain</span> bean. This method is less rigid and more modular, making it easier to customize security settings as needed.
  3. Adoption of DSL Lambdas:
    • The configuration code now uses DSL (Domain Specific Language) lambdas, which are more concise and flexible.DSL Lambdas in the context of Spring Security provide a modern and streamlined way to configure security rules. This approach utilizes lambda expressions, which are essentially anonymous functions or blocks of code you can pass around, to configure settings in a more direct and readable way.
  4. Introduction of the SecurityFilterChain:
    • Instead of configuring security settings globally in one big class, we now define a <span class="pink">SecurityFilterChain</span> bean that handles security in a chain-like manner. This approach breaks down the security configurations into smaller, manageable parts, enhancing customization and clarity. For instance, in our setup, we can explicitly state which endpoints are open to all users and which require authentication, and manage how sessions are handled in a stateless manner

Explanation for the above  code:- <span class="pink">SecurityConfig</span> class

1. authenticationProvider() Method

This method sets up the rules for checking who is trying to access our application:

  • User Details Service: Think of it as a way to look up information about users. When someone tries to log in, this service helps the system verify who they are by checking the username and password they provided.
  • Password Encoder: This part of the setup uses a special method (BCrypt) to handle passwords safely. When users create their passwords, this method scrambles them into a format that's very hard to decode. This means even if someone unauthorized gets access to the scrambled password, it's tough for them to figure out the actual password.

💡 In essence, the <span class="pink">authenticationProvider()</span> method prepares our application to securely check if users are who they claim to be and to handle their passwords securely.

2. securityFilterChain(HttpSecurity httpSecurity) Method

This method sets the rules for what is allowed in our application and how security is managed:

  • Turn off CSRF protection: CSRF is a type of attack that tricks the user into performing actions they didn’t intend to. For many applications, especially those that don’t maintain a continuous conversation with the user (like APIs), it’s safe to turn this off.
  • Control Access: We specifically say that anyone can access the <span class="pink">/register</span> endpoint without logging in (which is useful for new users registering). Every other request (or action) in the application needs the user to be logged in.
  • Session Management: We configure our application to not keep any record of user sessions. This means each request to the server must include credentials, making it more secure for stateless applications like APIs.
  • Basic Authentication: This is a simple security measure that requires users to provide a username and password with their requests.

💡 By setting up the <span class="pink">SecurityFilterChain</span>, we're essentially telling our application how to handle security checks and user access step by step. This configuration helps keep the application safe and ensures that only authorized users can access certain features.

Demo with our own custom security filter chain:

Chapter 3: Some Additional Stuff:

Enhanced CORS Configuration in Spring Security

When developing a web application using frameworks like React or Angular, a common issue that arises when consuming APIs is related to Cross-Origin Resource Sharing (CORS).

CORS is a security policy implemented by browsers to prevent requests to your server from scripts running on pages hosted on other domains unless explicitly allowed.

Simply using the <span class="pink">@CrossOrigin</span> annotation on REST controller classes in Spring Boot may initially seem like the solution to enable cross-origin requests. This annotation configures the necessary HTTP headers to allow cross-origin interactions for that specific controller.

However, when Spring Security is integrated into a Spring Boot application, configuring CORS becomes slightly more complex. Spring Security applies a more stringent handling of CORS and security headers, which means that merely using the <span class="pink">@CrossOrigin</span> annotation might not be sufficient to handle CORS issues fully.

💡 To effectively configure CORS in an application secured by Spring Security, you need to extend the security configuration to explicitly allow cross-origin requests.

💡 This involves defining an additional bean in the <span class="pink">SecurityConfig.java</span> class or adjusting the security filter chain to include proper CORS configuration.

Here's a more detailed explanation of how to do this:

Extending Spring Security Configuration for CORS

  • Define a CORS Configuration Source: This is a crucial step where you specify which origins, HTTP methods, and headers are allowed. It involves creating a <span class="pink">CorsConfigurationSource</span> bean that outlines these policies.

    For a React application running on port 3000 to successfully consume backend APIs protected by Spring Security, you need to configure CORS appropriately in your Spring Boot application. This setup ensures that the React application can make cross-origin requests to your secured backend.

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    configuration.setAllowCredentials(true);
    configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
    configuration.setExposedHeaders(Arrays.asList("Authorization"));

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

  • Integrate CORS with Spring Security: After defining the CORS configuration source, you must integrate this configuration with Spring Security. This is done by modifying the HttpSecurity object within the SecurityFilterChain method to apply your CORS settings.

Now, our final SecurityFilterChain Bean will look like this:


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf(AbstractHttpConfigurer::disable)
                .cors(c -> c.configurationSource(corsConfigurationSource()))
                .authorizeHttpRequests(auth ->
                        auth
                                .requestMatchers("/register")
                                .permitAll().anyRequest()
                                .authenticated())
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .httpBasic(Customizer.withDefaults());


        return httpSecurity.build();
    }

By following these steps, you ensure that CORS is handled appropriately in your Spring Boot application with Spring Security, enabling secure cross-origin requests from your frontend applications hosted on different domains. This configuration allows for a more flexible and secure setup compared to using the <span class="pink">@CrossOrigin</span> annotation alone.

Note: In upcoming articles, we'll dive into more advanced topics in Spring Security, including resource access based on authorization, JSON Web Tokens (JWT), and Social Login(OAuth2) integration with Spring Boot.

Thanks! Happy coding!

Gaurav Sharma
April 14, 2024
Use Unlogged to
mock instantly
record and replay methods
mock instantly
Install Plugin