Spring Boot Security REST API Tutorial

This tutorial will guide you through building and securing a REST API using Spring Boot 3, Spring Security 6, and Java 21. We will use Java Records to transfer data between the client and server. This tutorial is designed for beginners and covers the following topics:
  1. Introduction to Spring Boot for Beginners
  2. Introduction to REST API
  3. Introduction to Spring Security
  4. Creating a Spring Boot Project
  5. Creating CRUD REST APIs with MySQL Database
  6. Securing the REST API using Basic Authentication
  7. Securing the REST API using In-Memory Authentication
  8. Securing the REST API using Database Authentication
Spring Boot Security REST API Tutorial

Introduction to Spring Boot

Spring Boot is an open-source Java-based framework for creating stand-alone, production-grade Spring applications. It simplifies the development process by providing defaults for code and annotation configuration, enabling you to start coding quickly without worrying about setup details.

Key Features of Spring Boot

  • Auto-Configuration: Automatically configures your Spring application based on the dependencies you have added to the project.
  • Standalone: Creates stand-alone Spring applications with embedded servers.
  • Production-ready Features: Includes production-ready features such as metrics, health checks, and externalized configuration.
  • Convention over Configuration: Reduces the need for explicit configuration by following conventions.
  • Spring Boot Starters: Provides a set of pre-configured dependencies for various functionalities, making it easy to get started.

Introduction to REST API

A REST API (Representational State Transfer Application Programming Interface) is an architectural style for building web services that interact over HTTP. REST APIs allow different software systems to communicate and exchange data efficiently. The key principles of REST include statelessness, resource-based interactions, and standardized HTTP methods like GET, POST, PUT, and DELETE.

Key Principles of REST

  • Stateless: Each request from the client to the server must contain all the information needed to understand and process the request.
  • Resource-Based: REST treats any content as a resource, such as users, posts, or items.
  • HTTP Methods: REST uses standard HTTP methods for CRUD operations (Create, Read, Update, Delete).

Introduction to Spring Security

Spring Security is a powerful and highly customizable authentication and access-control framework for Java applications. It is the de facto standard for securing Spring-based applications and provides comprehensive security services for Java EE-based enterprise software applications.

Key Features of Spring Security

  • Authentication: Verifying the identity of a user.
  • Authorization: Determining whether an authenticated user has access to a specific resource.
  • Protection Against Attacks: Such as session fixation, clickjacking, cross-site request forgery, etc.

Step 1: Creating a Spring Boot Project

Using Spring Initializr

  1. Go to Spring Initializr.

  2. Configure the project:

    • Project: Maven Project
    • Language: Java
    • Spring Boot: 3.3.0
    • Packaging: Jar
    • Java: 21
    • Dependencies: Spring Web, Spring Data JPA, MySQL Driver, Lombok, Spring Security
  3. Click on "Generate" to download the project.

  4. Unzip the downloaded project and open it in your favorite IDE.

Example Project Structure

spring-boot-security-rest-api/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/demo/
│   │   │       └── DemoApplication.java
│   │   │       └── controller/
│   │   │           └── ProductController.java
│   │   │           └── OrderController.java
│   │   │       └── model/
│   │   │           └── Product.java
│   │   │           └── Order.java
│   │   │           └── User.java
│   │   │           └── Role.java
│   │   │       └── repository/
│   │   │           └── ProductRepository.java
│   │   │           └── OrderRepository.java
│   │   │           └── UserRepository.java
│   │   │       └── service/
│   │   │           └── ProductService.java
│   │   │           └── OrderService.java
│   │   │           └── UserService.java
│   │   │       └── config/
│   │   │           └── SecurityConfig.java
│   │   └── resources/
│   │       ├── application.properties
│   └── test/
│       └── java/
│           └── com/example/demo/
│               └── DemoApplicationTests.java
├── mvnw
├── mvnw.cmd
├── pom.xml
└── .mvn/
    └── wrapper/
        └── maven-wrapper.properties

Step 2: Creating CRUD REST APIs with MySQL Database

Setting Up the Database

  1. Create a MySQL database named spring_boot_db.

Configure Database Connection

  1. Update the application.properties file to configure the MySQL database connection.

    spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot_db
    spring.datasource.username=root
    spring.datasource.password=root
    spring.jpa.hibernate.ddl-auto=update
    spring.jpa.show-sql=true
    

Create Entity Classes

  1. Create a new package named model and add the Product and Order entity classes.

    package com.example.demo.model;
    
    import jakarta.persistence.Entity;
    import jakarta.persistence.GeneratedValue;
    import jakarta.persistence.GenerationType;
    import jakarta.persistence.Id;
    
    @Entity
    public class Product {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
        private double price;
    
        public Product() {}
    
        public Product(String name, double price) {
            this.name = name;
            this.price = price;
        }
    
        // Getters and setters
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public double getPrice() {
            return price;
        }
    
        public void setPrice(double price) {
            this.price = price;
        }
    }
    
    package com.example.demo.model;
    
    import jakarta.persistence.Entity;
    import jakarta.persistence.GeneratedValue;
    import jakarta.persistence.GenerationType;
    import jakarta.persistence.Id;
    
    @Entity
    public class Order {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private Long productId;
        private int quantity;
    
        public Order() {}
    
        public Order(Long productId, int quantity) {
            this.productId = productId;
            this.quantity = quantity;
        }
    
        // Getters and setters
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public Long getProductId() {
            return productId;
        }
    
        public void setProductId(Long productId) {
            this.productId = productId;
        }
    
        public int getQuantity() {
            return quantity;
        }
    
        public void setQuantity(int quantity) {
            this.quantity = quantity;
        }
    }
    

Explanation

  • @Entity: Specifies that the class is an entity and is mapped to a database table.
  • @Id: Specifies the primary key of an entity.
  • @GeneratedValue(strategy = GenerationType.IDENTITY): Provides the specification of generation strategies for the primary key values.

Create Repository Interfaces

  1. Create a new package named repository and add the ProductRepository and OrderRepository interfaces.

    package com.example.demo.repository;
    
    import com.example.demo.model.Product;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface ProductRepository extends JpaRepository<Product, Long> {
    }
    
    package com.example.demo.repository;
    
    import com.example.demo.model.Order;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface OrderRepository extends JpaRepository<Order, Long> {
    }
    

Explanation

  • extends JpaRepository<Product, Long>: Indicates that the ProductRepository interface extends JpaRepository, providing CRUD operations for the Product entity.
  • extends JpaRepository<Order, Long>: Indicates that the OrderRepository interface extends JpaRepository, providing CRUD operations for the Order entity.

Create Service Classes

  1. Create a new package named service and add the ProductService and OrderService classes.

    package com.example.demo.service;
    
    import com.example.demo.model.Product;
    import com.example.demo.repository.ProductRepository;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    import java.util.Optional;
    
    @Service
    public class ProductService {
    
        private final ProductRepository productRepository;
    
        @Autowired
        public ProductService(ProductRepository productRepository) {
            this.productRepository = productRepository;
        }
    
        public List<Product> getAllProducts() {
            return productRepository.findAll();
        }
    
        public Optional<Product> getProductById(Long id) {
            return productRepository.findById(id);
        }
    
        public Product createProduct(Product product) {
            return productRepository.save(product);
        }
    
        public Optional<Product> updateProduct(Long id, Product productDetails) {
            return productRepository.findById(id).map(product -> {
                product.setName(productDetails.getName());
                product.setPrice(productDetails.getPrice());
                return productRepository.save(product);
            });
        }
    
        public void deleteProduct(Long id) {
            productRepository.deleteById(id);
        }
    }
    
    package com.example.demo.service;
    
    import com.example.demo.model.Order;
    import com.example.demo.repository.OrderRepository;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    import java.util.Optional;
    
    @Service
    public class OrderService {
    
        private final OrderRepository orderRepository;
    
        @Autowired
        public OrderService(OrderRepository orderRepository) {
            this.orderRepository = orderRepository;
        }
    
        public List<Order> getAllOrders() {
            return orderRepository.findAll();
        }
    
        public Optional<Order> getOrderById(Long id) {
            return orderRepository.findById(id);
        }
    
        public Order createOrder(Order order) {
            return orderRepository.save(order);
        }
    
        public Optional<Order> updateOrder(Long id, Order orderDetails) {
            return orderRepository.findById(id).map(order -> {
                order.setProductId(orderDetails.getProductId());
                order.setQuantity(orderDetails.getQuantity());
                return orderRepository.save(order);
            });
        }
    
        public void deleteOrder(Long id) {
            orderRepository.deleteById(id);
        }
    }
    

Explanation

  • @Service: Indicates that the class is a service component in the Spring context.
  • public ProductService(ProductRepository productRepository): Uses constructor-based dependency injection to inject the ProductRepository bean.
  • public OrderService(OrderRepository orderRepository): Uses constructor-based dependency injection to inject the OrderRepository bean.

Create Controller Classes

  1. Create a new package named controller and add the ProductController and OrderController classes.

    package com.example.demo.controller;
    
    import com.example.demo.model.Product;
    import com.example.demo.service.ProductService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.List;
    import java.util.Optional;
    
    @RestController
    @RequestMapping("/api/products")
    public class ProductController {
    
        private final ProductService productService;
    
        @Autowired
        public ProductController(ProductService productService) {
            this.productService = productService;
        }
    
        @GetMapping
        public List<Product> getAllProducts() {
            return productService.getAllProducts();
        }
    
        @GetMapping("/{id}")
        public Optional<Product> getProductById(@PathVariable Long id) {
            return productService.getProductById(id);
        }
    
        @PostMapping
        public Product createProduct(@RequestBody Product product) {
            return productService.createProduct(product);
        }
    
        @PutMapping("/{id}")
        public Optional<Product> updateProduct(@PathVariable Long id, @RequestBody Product productDetails) {
            return productService.updateProduct(id, productDetails);
        }
    
        @DeleteMapping("/{id}")
        public void deleteProduct(@PathVariable Long id) {
            productService.deleteProduct(id);
        }
    }
    
    package com.example.demo.controller;
    
    import com.example.demo.model.Order;
    import com.example.demo.service.OrderService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.List;
    import java.util.Optional;
    
    @RestController
    @RequestMapping("/api/orders")
    public class OrderController {
    
        private final OrderService orderService;
    
        @Autowired
        public OrderController(OrderService orderService) {
            this.orderService = orderService;
        }
    
        @GetMapping
        public List<Order> getAllOrders() {
            return orderService.getAllOrders();
        }
    
        @GetMapping("/{id}")
        public Optional<Order> getOrderById(@PathVariable Long id) {
            return orderService.getOrderById(id);
        }
    
       @PostMapping
        public Order createOrder(@RequestBody Order order) {
            return orderService.createOrder(order);
        }
    
        @PutMapping("/{id}")
        public Optional<Order> updateOrder(@PathVariable Long id, @RequestBody Order orderDetails) {
            return orderService.updateOrder(id, orderDetails);
        }
    
        @DeleteMapping("/{id}")
        public void deleteOrder(@PathVariable Long id) {
            orderService.deleteOrder(id);
        }
     }
    

Explanation

  • @RestController: Indicates that the class is a REST controller.
  • @RequestMapping("/api/products"): Maps HTTP requests to the /api/products URL.
  • @RequestMapping("/api/orders"): Maps HTTP requests to the /api/orders URL.
  • CRUD Methods: Implements CRUD operations for the Product and Order entities.

Step 3: Securing the REST API

Securing the REST API using Basic In-memory Authentication

  1. Add the SecurityConfig class in the config package.

    package com.example.demo.config;
    
    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.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    import org.springframework.security.web.SecurityFilterChain;
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/api/orders/**").hasAnyRole("USER", "ADMIN")
                    .requestMatchers("/api/products/**").hasRole("ADMIN")
                    .anyRequest().authenticated()
                )
                .httpBasic(withDefaults());
            return http.build();
        }
    
        @Bean
        public UserDetailsService userDetailsService() {
            UserDetails user = User.withUsername("user")
                .password("{noop}password")
                .roles("USER")
                .build();
            UserDetails admin = User.withUsername("admin")
                .password("{noop}admin")
                .roles("ADMIN")
                .build();
            return new InMemoryUserDetailsManager(user, admin);
        }
    }
    

Explanation

  • @Configuration: Indicates that the class has @Bean definition methods.
  • @EnableWebSecurity: Enables Spring Security's web security support.
  • securityFilterChain(HttpSecurity http): Configures the security filter chain.
  • http.csrf(csrf -> csrf.disable()): Disables CSRF protection.
  • http.authorizeHttpRequests(auth -> auth.requestMatchers("/api/orders/**").hasAnyRole("USER", "ADMIN").requestMatchers("/api/products/**").hasRole("ADMIN").anyRequest().authenticated()): Configures authorization rules.
  • http.httpBasic(withDefaults()): Configures basic HTTP authentication.
  • userDetailsService(): Creates an in-memory user details manager with two users: one with the role USER and the other with the role ADMIN.

Securing the REST API using Database Authentication

  1. Create User and Role entities.

    package com.example.demo.model;
    
    import jakarta.persistence.*;
    import java.util.Set;
    
    @Entity
    public class User {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String username;
        private String password;
    
        @ManyToMany(fetch = FetchType.EAGER)
        @JoinTable(
            name = "users_roles",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id")
        )
        private Set<Role> roles;
    
        // Getters and setters
    }
    
    package com.example.demo.model;
    
    import jakarta.persistence.*;
    
    @Entity
    public class Role {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
    
        // Getters and setters
    }
    
  2. Create UserRepository and RoleRepository.

    package com.example.demo.repository;
    
    import com.example.demo.model.User;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface UserRepository extends JpaRepository<User, Long> {
        User findByUsername(String username);
    }
    
    package com.example.demo.repository;
    
    import com.example.demo.model.Role;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface RoleRepository extends JpaRepository<Role, Long> {
    }
    
  3. Create UserService and UserDetailsServiceImpl.

    package com.example.demo.service;
    
    import com.example.demo.model.Role;
    import com.example.demo.model.User;
    import com.example.demo.repository.UserRepository;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    
    import java.util.Set;
    import java.util.stream.Collectors;
    
    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Autowired
        private UserRepository userRepository;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userRepository.findByUsername(username);
            if (user == null) {
                throw new UsernameNotFoundException("User not found");
            }
    
            Set<GrantedAuthority> authorities = user.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority(role.getName()))
                .collect(Collectors.toSet());
    
            return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
        }
    }
    
  4. Update SecurityConfig to use database authentication.

    package com.example.demo.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    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.core.userdetails.UserDetailsService;
    import org.springframework.security.web.SecurityFilterChain;
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/api/orders/**").hasAnyRole("USER", "ADMIN")
                    .requestMatchers("/api/products/**").hasRole("ADMIN")
                    .anyRequest().authenticated()
                )
                .userDetailsService(userDetailsService)
                .httpBasic(withDefaults());
            return http.build();
        }
    }
    

Explanation

  • @ManyToMany: Indicates a many-to-many relationship between User and Role.
  • UserRepository: Extends JpaRepository, providing CRUD operations for the User entity.
  • RoleRepository: Extends JpaRepository, providing CRUD operations for the Role entity.
  • UserDetailsServiceImpl: Implements the UserDetailsService interface to load user-specific data.
  • securityFilterChain(HttpSecurity http): Configures the security filter chain to use the UserDetailsService for authentication.

By following these steps, you will have a secured REST API where:

  • Viewing and placing orders is accessible to users with the roles USER and ADMIN.
  • Managing products is accessible only to users with the role ADMIN.

Testing the REST APIs Using Postman

  1. Get All Products

    • Request: GET /api/products
    • Response:
      [
        {
          "id": 1,
          "name": "Product 1",
          "price": 100.0
        },
        {
          "id": 2,
          "name": "Product 2",
          "price": 200.0
        }
      ]
      
  2. Get Product by ID

    • Request: GET /api/products/{id}
    • Response:
      {
        "id": 1,
        "name": "Product 1",
        "price": 100.0
      }
      
  3. Create Product

    • Request: POST /api/products
      {
        "name": "New Product",
        "price": 150.0
      }
      
    • Response:
      {
        "id": 3,
        "name": "New Product",
        "price": 150.0
      }
      
  4. Update Product

    • Request: PUT /api/products/{id}
      {
        "name": "Updated Product",
        "price": 180.0
      }
      
    • Response:
      {
        "id": 1,
        "name": "Updated
      
      

Product", "price": 180.0 } ```

  1. Delete Product

    • Request: DELETE /api/products/{id}
    • Response: 204 No Content
  2. Get All Orders

    • Request: GET /api/orders
    • Response:
      [
        {
          "id": 1,
          "productId": 1,
          "quantity": 2
        },
        {
          "id": 2,
          "productId": 2,
          "quantity": 1
        }
      ]
      
  3. Get Order by ID

    • Request: GET /api/orders/{id}
    • Response:
      {
        "id": 1,
        "productId": 1,
        "quantity": 2
      }
      
  4. Create Order

    • Request: POST /api/orders
      {
        "productId": 1,
        "quantity": 3
      }
      
    • Response:
      {
        "id": 3,
        "productId": 1,
        "quantity": 3
      }
      
  5. Update Order

    • Request: PUT /api/orders/{id}
      {
        "productId": 1,
        "quantity": 5
      }
      
    • Response:
      {
        "id": 1,
        "productId": 1,
        "quantity": 5
      }
      
  6. Delete Order

    • Request: DELETE /api/orders/{id}
    • Response: 204 No Content

This completes the setup and testing of securing REST APIs in Spring Boot using basic, in-memory, and database authentication.

Comments