Spring Boot Security REST API Tutorial
author : Sai K
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
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
- Go to start.spring.io
- Select
- Project: Maven Project
- Language: Java
- Spring Boot: 3.0.0 (or latest)
- 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
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
{ } package com.example.demo.repository; import com.example.demo.model.Order; import org.springframework.data.jpa.repository.JpaRepository; public interface OrderRepository extends JpaRepository
{ } Explanation
- extends JpaRepository
: Indicates that the ProductRepository interface extends JpaRepository,
providing CRUD operations for the Product entity. - extends JpaRepository
: 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
getAllProducts() { return productRepository.findAll(); } public Optional getProductById(Long id) { return productRepository.findById(id); } public Product createProduct(Product product) { return productRepository.save(product); } public Optional 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
getAllOrders() { return orderRepository.findAll(); } public Optional getOrderById(Long id) { return orderRepository.findById(id); } public Order createOrder(Order order) { return orderRepository.save(order); } public Optional 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 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
getAllProducts() { return productService.getAllProducts(); } @GetMapping("/{id}") public Optional getProductById(@PathVariable Long id) { return productService.getProductById(id); } @PostMapping public Product createProduct(@RequestBody Product product) { return productService.createProduct(product); } @PutMapping("/{id}") public Optional 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
getAllOrders() { return orderService.getAllOrders(); } @GetMapping("/{id}") public Optional getOrderById(@PathVariable Long id) { return orderService.getOrderById(id); } @PostMapping public Order createOrder(@RequestBody Order order) { return orderService.createOrder(order); } @PutMapping("/{id}") public Optional 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
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 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
{ } 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
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
{
"name": "New Product",
"price": 150.0
}
{
"id": 3,
"name": "New Product",
"price": 150.0
}
4.Update Product
Request: PUT /api/products/{id}
{
"name": "Updated Product",
"price": 180.0
}
{
"id": 1,
"name": "Updated Product", "price": 180.0
}
5.Delete Product
6.Get All Orders
[
{
"id": 1,
"productId": 1,
"quantity": 2
},
{
"id": 2,
"productId": 2,
"quantity": 1
}
]
7.Get Order by ID
- Request:GET /api/orders/{id}
- Response:
{
"id": 1,
"productId": 1,
"quantity": 2
}
8.Create Order
- Request: POST /api/orders
{
"productId": 1,
"quantity": 3
}
- Response:
{
"id": 3,
"productId": 1,
"quantity": 3
}
9.Update Order
- Request:PUT /api/orders/{id}
{
"productId": 1,
"quantity": 5
}
- Response:
{
"id": 1,
"productId": 1,
"quantity": 5
}
10.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.