Credits / Notes taken from:
Related Spring Boot tutorial notes from me:
-
Spring Boot Full Stack with Angular Application - EmployeeManager - August 2022
-
Spring Boot with Angular App: Server Ping Status Tracker - July 2023
-
Check out all my study notes here: https://github.com/radualexandrub/Study/tree/master
Table of Contents (ToC):
- Spring Boot API with Spring Security 5.7.x
- Project Setup using Spring Boot 2.7.18
- CRUD Operations - Data Model, Service, Repository, Controller
- Database Configuration using MySQL
- Testing API with Postman
- Spring Security 5.7.x - HTTP Basic Authentication
- Web Security configuration using Spring Security 5.7.11 with STATELESS Sessions
- Account Model, Repository and Service
- User Details Service - Tell Spring Security how to load the users / accounts
- Account Authentication Provider
- Account Resource / Controller
- (Optional) ApplicationStartRunner
- Testing Basic Auth with Postman
- Deployment using Docker
Other useful resources:
- What is REST (API)? https://30secondsofinterviews.org/
REST (REpresentational State Transfer) is a software design pattern for network architecture. A RESTful web application exposes data in the form of information about its resources.
Generally, this concept is used in web applications to manage state. With most applications, there is a common theme of reading, creating, updating, and destroying data. Data is modularized into separate tables like
posts,users,comments, and a RESTful API exposes access to this data with:
- An identifier for the resource. This is known as the endpoint or URL for the resource.
- The operation the server should perform on that resource in the form of an HTTP method or verb. The common HTTP methods are GET, POST, PUT, and DELETE.
Here is an example of the URL and HTTP method with a
postsresource:
- Reading:
/posts/=> GET- Creating:
/posts/new=> POST- Updating:
/posts/:id=> PUT- Destroying:
/posts/:id=> DELETE
(Friday, January 05, 2024, 14:35)
Project configuration:
- Project: Maven Project
- Spring Boot: Version 3.2.1 (Jan 2024)
- Project Metadata:
- Group (domain): "ENTER YOUR DOMAIN HERE" (for me it'll be com.radubulai)
- Artifact (the name of the application): springbootapisecurity
- Name: springbootapisecurity
- Description: Spring Boot REST API Application with authentication using Spring Security
- Package name (you could leave the autogenerated name): com.radubulai.springbootapisecurity
- Packaging: Jar
- Java Version: 17
Dependencies:
- Spring Web - Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.
- Spring Data JPA - Persist data in SQL stores with Java Persistence API using Spring Data and Hibernate (ORM - Object Relational Mappind).
- Spring Security - Highly customizable authentication and access-control framework for Spring applications
- MySQL JDBC driver
- Validation
I/O- Bean Validation with Hibernate validator. - Lombok
DEVELOPER TOOL- Java annotation library which helps to reduce boilerplate code.
🔴🔴🔴 Important Notes:
- For this tutorial, we will change Spring Boot Version to 2.7.18 that uses Spring Security 5.7.11. Reasong being that many parts (including syntax, method calling, classes) from the "Get Arrays" Video Tutorial are deprecated in the new Spring Security 6.x (used in Spring Boot 3.x).
- I will try to adapt this tutorial to Spring Security 6.x (e.g. 6.2.1) and Spring Boot 3.x (e.g. 3.2.1) in the future (in a separate tutorial)
- However, many companies will still use Spring Boot 2.7.x and Spring Security 5.x, so all the information here might still be relevant
The contents of pom.xml file using Spring Boot 2.7.18:
<?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>
<!-- https://github.com/spring-projects/spring-boot/releases/tag/v3.2.1 -->
<!-- https://spring.io/blog/2023/11/23/spring-boot-2-7-18-available-now/ -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.radubulai</groupId>
<artifactId>springbootapisecurity</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springbootapisecurity</name>
<description>Spring Boot REST API Application with authentication using Spring Security</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-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</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: If we change a major version of a package (e.g. Spring Boot from 3.x to 2.x) in pom.xml, IntelliJ will not recognize methods/classes that were changed:
- Go to
File>Project Structure>Libraries> Clear all the libraries - Go to
Maven(right ribbon) > Reload all Maven Projects
Spring Boot API with Spring Security and Docker - Architecture - 4m
Spring Boot API with Spring Security and Docker - Domain Model - 14m
🟠 NOTE: On this tutorial, the main database model used is "Employee" -> In these notes/tutorial, we will use a "Project" (database) model, as in https://github.com/radualexandrub/Study/blob/master/SQL/MySQL_ChatGPT.md#project-schema (Radu-Alexandru Bulai - Friday, January 05, 2024)
- On
\src\main\java\com\radubulai\springbootapisecurity: Createmodelpackage - Inside
modelpackage, createProjectjava class
// ./model/Project.java using Spring Boot 2.7.18:
package com.radubulai.springbootapisecurity.model;
import javax.persistence.*;
import javax.validation.constraints.*;
import lombok.*;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 2024-01-05
*/
@Entity
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Project {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false, unique = true)
@NotEmpty(message = "Project Key (Alphanumeric Project Identifier) cannot be empty or null")
private String keyName;
private String name;
private String description;
private String createdAt;
}🔴 Note: In Spring Boot 3.2.1: import javax.persistence.*; and import javax.validation.constraints.*; (from Spring Boot 2.7.18) are replaced by import jakarta.persistence.*; and import jakarta.validation.constraints.*;
🟠 Note: If certain words reserved by MySQL are used (such as insert, select, key, etc), we would receive generic error ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near when first compiling (mvn spring-boot:run) the project to create the database schema - Reference: https://stackoverflow.com/questions/23446377/syntax-error-due-to-using-a-reserved-word-as-a-table-or-column-name-in-mysql
Spring Boot API with Spring Security and Docker - Service Interface - 21m
- On
\src\main\java\com\radubulai\springbootapisecurity: Createrepositorypackage - Within
repositorypackage, create "ProjectRepository" java interface -
- This interface
ProjectRepositorywill extend theJpaRepository. When extending fromJpaRepositoryinterface, we need to specify the model type (Project) and the ID data type (Long):public interface ProjectRepository extends JpaRepository<Project, Long>.
- This interface
Note: We can CTRL+Click on
JpaRepositoryinterface to see its decompilled .class file (its code), and look over its methods, eg:findAll,saveAll,deleteAllInBatch, etc... (those are useful methods as we don't need to implement them from scratch)
// ProjectRepository.java
package com.radubulai.springbootapisecurity.repository;
import com.radubulai.springbootapisecurity.model.Project;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 2024-01-05
*/
public interface ProjectRepository extends JpaRepository<Project, Long> {
void deleteProjectById(Long id);
Optional<Project> findProjectById(Long id);
}Spring Boot API with Spring Security and Docker - Service Interface - 17m55s
- On
\src\main\java\com\radubulai\springbootapisecurity: Createservicepackage - Within
servicepackage, create "ProjectService" java interface
// ./service/ProjectService.java
package com.radubulai.springbootapisecurity.service;
import com.radubulai.springbootapisecurity.model.Project;
import java.util.List;
public interface ProjectService {
List<Project> findAllProjects();
Project findProjectById(Long id);
Project addProject(Project project);
Project updateProject(Project project);
Boolean deleteProjectById(Long id);
}- Inside
servicepackage we can create aimplementationpackage, and here we'll have theProjectServiceImpljava class- Inside
ProjectServiceImpl.java, we create aProjectRepositoryobject where we will use the defined SQL / Query methods - Now, usually after declaring this
projectRepositoryobject, we needed to initialize it by calling thepublic ProjectServiceImpl(ProjectRepository projectRepository) { this.projectRepository = projectRepository; }constructor - however, since we use the Lombok library, we can simply add the@RequiredArgsConstructorannotation - We also need to annotate the
ProjectServiceclass repo with@Servicedecorator
- Inside
// ProjectServiceImpl.java
package com.radubulai.springbootapisecurity.service.implementation;
import com.radubulai.springbootapisecurity.exception.ProjectNotFoundException;
import com.radubulai.springbootapisecurity.model.Project;
import com.radubulai.springbootapisecurity.repository.ProjectRepository;
import com.radubulai.springbootapisecurity.service.ProjectService;
// import jakarta.transaction.Transactional; // Spring Boot 3.2.1
import javax.transaction.Transactional; // Spring Boot 2.7.18
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 2024-01-05
*/
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class ProjectServiceImpl implements ProjectService {
private final ProjectRepository projectRepository;
@Override
public List<Project> findAllProjects() {
return projectRepository.findAll();
}
@Override
public Project findProjectById(Long id) {
return projectRepository.findProjectById(id).orElseThrow(
() -> new ProjectNotFoundException(String.format("Project by id %s was not found", id))
);
}
@Override
public Project addProject(Project project) {
return projectRepository.save(project);
}
@Override
public Project updateProject(Project project) {
return projectRepository.save(project);
}
@Override
public Boolean deleteProjectById(Long id) {
projectRepository.deleteProjectById(id);
return Boolean.TRUE;
}
}Notes on the annotations used:
@Service: This annotation is from the Spring Framework and is used to mark a class as a service component. It indicates that the class contains the business logic of the application. By annotating the class with@Service, it becomes eligible for auto-detection and can be injected into other Spring components, such as controllers.@RequiredArgsConstructor: This is a Lombok annotation that automatically generates a constructor with required arguments based on the class's final fields. In this case, since theProjectRepositoryfield is marked asfinal, Lombok generates a constructor that accepts an instance ofProjectRepositoryand assigns it to the field. This eliminates the need for explicitly defining a constructor in the class.@Transactional: This annotation is from the Spring Framework and is used to define the transactional behavior of a method or class. By annotating the class with@Transactional, all public methods in the class become transactional. Transactions ensure data consistency and integrity by enforcing ACID (Atomicity, Consistency, Isolation, Durability) properties when performing database operations.@Slf4j: This annotation is from Lombok and is used to generate a logger field in the class. It automatically creates a logger instance with the name "log" that can be used for logging messages within the class. The logging framework used depends on the project's configuration.
Also, note that we have a dedicated ProjectNotFoundException within exception java package:
// ProjectNotFoundException.java
package com.radubulai.springbootapisecurity.exception;
public class ProjectNotFoundException extends RuntimeException{
public ProjectNotFoundException(String message) {
super(message);
}
}(Separate from main tutorial) We will take the same approach from https://github.com/radualexandrub/Study/blob/master/SpringBoot/SpringBootAngularPingStatusApp.md#optional-response-model-for-each-response-from-api by responding with a custom Response whenever this REST API Server is called.
Before implementing the Resource/Controller, we can create a Response class (under model package) that we can send back to the end user (browser) no matter the response to the request is an error or a succesfull retrieve/update/etc of data. The Response will include several properties such as:
timeStampstatusCode(the numerical status code)status(the correspondingHttpStatusenum value from Spring Framework, e.g.OKfor 200,CREATEDfor 201,MOVED_PERMANENTLYfor 301,FOUNDfor 302,BAD_REQUESTfor 400,UNAUTHORIZEDfor 401,NOT_FOUNDfor 404,INTERNAL_SERVER_ERRORfor 500, etc.)reason(a descriptive reason for the response)message(a human-readable message that can be shown to the end user)developerMessage(a more technical message for developers or for debugging purposes)data
Note, by default, if we do not implement such class, every response that our API will send will be the direct JSON data (and other details will be found in the header of the HTTP request).
// Response.java
package com.radubulai.springbootapisecurity.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import lombok.experimental.SuperBuilder;
import org.springframework.http.HttpStatus;
import java.time.LocalDateTime;
import java.util.Map;
@Data
@SuperBuilder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Response {
protected LocalDateTime timeStamp;
protected int statusCode;
protected HttpStatus status;
protected String reason;
protected String message;
protected String developerMessage;
protected Map<?, ?> data;
}Annotations from above:
@Dataannotation from Lombok library generates boilerplate code for common methods such as getters, setters,equals(),hashCode(), andtoString()@SuperBuilderannotation from Lombok library allows for a fluent builder API for constructing instances of theResponseclass (see below its usage in ProjectResource controller).@JsonInclude(JsonInclude.Include.NON_NULL)annotation from the Jackson library ensures that properties with null values are not included in the JSON serialization - it helps in producing a more concise and clean JSON response
Spring Boot API with Spring Security and Docker - Resource - 34m
- On
\src\main\java\com\radubulai\springbootapisecurity: Createresourcepackage - Within
resourcepackage, create "ProjectResource" java class
// ProjectResource.java
package com.radubulai.springbootapisecurity.resource;
import com.radubulai.springbootapisecurity.model.Project;
import com.radubulai.springbootapisecurity.model.Response;
import com.radubulai.springbootapisecurity.service.ProjectService;
// import jakarta.validation.Valid; // Spring Boot 3.2.1
import javax.validation.Valid; // Spring Boot 2.7.18
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import static java.time.LocalDateTime.now;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 2024-01-07
* @apiNote Controller/Resource for Project
*/
@RestController
@RequestMapping("/api/projects")
@RequiredArgsConstructor
public class ProjectResource {
private final ProjectService projectService;
@GetMapping("")
public ResponseEntity<Response> getAllProjects() {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("projects", projectService.findAllProjects()))
.message("Projects retrieved")
.status(HttpStatus.OK)
.statusCode(HttpStatus.OK.value())
.build());
}
@GetMapping("/{id}")
public ResponseEntity<Response> getProjectById(@PathVariable Long id) {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("project", projectService.findProjectById(id)))
.message("Project retrieved")
.status(HttpStatus.OK)
.statusCode(HttpStatus.OK.value())
.build());
}
@PostMapping("")
public ResponseEntity<Response> addProject(@RequestBody @Valid Project project) {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("project", projectService.addProject(project)))
.message("Project created")
.status(HttpStatus.CREATED)
.statusCode(HttpStatus.CREATED.value())
.build());
}
@PostMapping("/save-all")
public ResponseEntity<Response> addProjects(@RequestBody @Valid Project[] projects) {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("projects", projectService.addProjects(projects)))
.message(String.format("%s Projects created", projects.length))
.status(HttpStatus.CREATED)
.statusCode(HttpStatus.CREATED.value())
.build());
}
@PutMapping("")
public ResponseEntity<Response> updateProject(@RequestBody @Valid Project project) {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("project", projectService.updateProject(project)))
.message("Project updated")
.status(HttpStatus.OK)
.statusCode(HttpStatus.OK.value())
.build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Response> deleteProjectById(@PathVariable Long id) {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("deleted", projectService.deleteProjectById(id)))
.message("Project deleted")
.status(HttpStatus.OK)
.statusCode(HttpStatus.OK.value())
.build());
}
}From tutorial Spring Boot API with Spring Security and Docker - Database Configuration - 59m
For more details, see my notes from here https://github.com/radualexandrub/Study/blob/master/SpringBoot/SpringBootAngularPingStatusApp.md#database-configuration
After installing MySQL 8.0 (448MB installer), we can open "MySQL 8.0 Command Line Client" (from Windows Start Menu).
🔵 Note: (On Windows) If we cannot start the MySQL Server (eg. "MySQL Workbench" just crashes when we try to start the server):
- Open Windows Start Menu, search and open "Services", manually find
MySQL80service -> Right click it -> Start. - See more here: Can't startup and connect to MySQL server.
To set up our database in our Java Spring Application, we need to go to src/main/resource/application.yml (Note: application.properties can be easily renamed/refactored to application.yml):
# application.yml using Spring Boot 3.2.1 and its dependecies
spring:
datasource:
# MySQL
url: jdbc:mysql://localhost:3306/projectmanagerapp
username: root
password: microsoft
jpa:
show-sql: false
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
format_sql: true
debug: false# application.yml using Spring Boot 2.7.18 and its dependencies
spring:
datasource:
# MySQL
url: jdbc:mysql://localhost:3306/projectmanagerapp
username: root
password: microsoft
jpa:
show-sql: false
hibernate:
ddl-auto: update
properties:
hibernate:
globally_quoted_identifiers: true
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
format_sql: true
debug: falseFor the MySQL setup:
- The default port for MySQL is 3306, so the DB address will be
localhost:3306 - the name of the database will be
projectmanagerapp=> the address will be localhost:3306/projectmanagerapp
The spring.jpa section contains configuration settings for JPA (Java Persistence API) and Hibernate, the ORM (Object-Relational Mapping) framework.
show-sql: trueenables logging of SQL statements executed by Hibernate, providing visibility into the generated SQL queries (Note that this should always be disabled in production).hibernate.ddl-auto:- value
createspecifies that Hibernate should automatically create the database schema based on the entity mappings. This will create the necessary tables when the application starts. Note that this setting is typically used in development and should be handled differently in production. - value
updatemeans that Hibernate will update the database schema based on the entity classes' definitions if necesary.
- value
properties.hibernate.dialectspecifies the dialect to use for the MySQL database. In this case, theorg.hibernate.dialect.MySQLDialectdialect is selected, which is suitable for MySQL version 5 and InnoDB storage engine.properties.hibernate.format_sql: trueenables the formatting of SQL statements logged by Hibernate, making them more readable for debugging purposes.
🟠 NOTE: Since we use Spring Boot 3 that uses Hibernate 6, Hibernate 6 changed how dialects work, and org.hibernate.dialect.MySQLDialect needs to be used (which configures itself based the actual server version). The version specific dialects containing InnoDB in their name were removed in Hibernate 6 - Reference https://stackoverflow.com/questions/74582403/jpa-hibernate-how-to-find-the-dialect
We can now create the projectmanagerapp MySQL database:
- Open "MySQL 8.0 Command Line Client", write:
create database projectmanagerapp; - We can check with
show databases;command
We can run the Java Application from Terminal, in the main project directory:
mvn spring-boot:runAfter running the app, the Project table from the pingstatustracker database is created automatically in MySQL and can be seen via MySQL Workbench App.
Accessing http://localhost:8080/api/projects will redirect us to http://localhost:8080/login since we have "Spring Security" installed as a dependency in our project. The default username is user and the password can be found within Spring logs when first running the application:
See my other notes from here https://github.com/radualexandrub/Study/blob/master/SpringBoot/SpringBootAngularPingStatusApp.md#testing-with-postman
🔵 Send a GET request to http://localhost:8080/api/projects
- ⬅the server will respond:
{
"timeStamp": "2024-01-07T20:28:05.2301537",
"statusCode": 200,
"status": "OK",
"message": "Projects retrieved",
"data": {
"projects": [
{
"id": 1,
"keyName": "PORTFOLIO",
"name": "HappyCat Portfolio",
"description": "HappyCat Customer Portfolio",
"createdAt": "2024-01-06"
},
{
"id": 2,
"keyName": "CRMMANAGER",
"name": "CRM Manager Backend API in Spring Boot",
"description": "Official Project for the BACKEND of CRM Manager Application",
"createdAt": "2024-01-07T20:26:48.465963800"
}
]
}
}🔵 Send a GET request to http://localhost:8080/api/projects/1 (id)
- ⬅The server will return:
{
"timeStamp": "2024-01-07T20:05:21.9751627",
"statusCode": 200,
"status": "OK",
"message": "Project retrieved",
"data": {
"project": {
"id": 1,
"keyName": "PORTFOLIO",
"name": "HappyCat Portfolio",
"description": "HappyCat Customer Portfolio",
"createdAt": "2024-01-06"
}
}
}🔵 Send a POST request with new Server information
- Open a new tab in Postman with the URL of http://localhost:8080/api/projects
- Set the request type to POST request
- Click on "Body" subtab
- check the "raw" radio button
- select "JSON" format
- write a JSON without specifying the id (the id will be generated by Spring JPA)
- ➡send
{
"keyName": "CRMMANAGER",
"name": "CRM Manager Backend API in Spring Boot",
"description": "Official Project for the BACKEND of CRM Manager Application"
}- ⬅The server will respond:
{
"timeStamp": "2024-01-07T20:26:48.4659638",
"statusCode": 201,
"status": "CREATED",
"message": "Project created",
"data": {
"project": {
"id": 2,
"keyName": "CRMMANAGER",
"name": "CRM Manager Backend API in Spring Boot",
"description": "Official Project for the BACKEND of CRM Manager Application",
"createdAt": "2024-01-07T20:26:48.465963800"
}
}
}🟠 Note: With Spring Security dependency installed in our project, we will receive a 401 Unauthorized response when trying to send a POST request. We can temporarily disable (comment) spring security dependency in the project main pom.xml in order to test our API (Reference https://stackoverflow.com/questions/45232071/springboot-401-unauthorized-even-with-out-security):
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>Reference: https://stackoverflow.com/questions/45232071/springboot-401-unauthorized-even-with-out-security
🔵 Send UPDATE request to update a server
- Open a new tab in Postman with the URL of http://localhost:8080/api/projects
- Set the request type to PUT request
- Click on "Body" subtab, check the "raw" radio button, and select "JSON" format
- Instead of this entry (that we currenly have in our database):
{
"id": 1,
"keyName": "PORTFOLIO",
"name": "HappyCat Portfolio",
"description": "HappyCat Customer Portfolio",
"createdAt": "2024-01-06"
}- ➡We'll send this project:
{
"id": 1,
"keyName": "PORTFOLIO",
"name": "Custom HappyCat Portfolio",
"description": "Customized Portfolio for HappyCat Portfolio via separate contract"
}- ⬅The server will respond:
{
"timeStamp": "2024-01-07T20:42:01.8811525",
"statusCode": 200,
"status": "OK",
"message": "Project updated",
"data": {
"project": {
"id": 1,
"keyName": "PORTFOLIO",
"name": "Custom HappyCat Portfolio",
"description": "Customized Portfolio for HappyCat Portfolio via separate contract",
"createdAt": "2024-01-06"
}
}
}🔵 Send DELETE request to delete a server http://localhost:8080/api/projects/3
- ⬅The server will respond:
{
"timeStamp": "2024-01-07T20:52:51.8763893",
"statusCode": 200,
"status": "OK",
"message": "Project deleted",
"data": {
"deleted": true
}
}Commit message as of 2024-01-07:
Create CRUD RESTful API for Project
1. Create Project Model class
2. Create ProjectRepository
3. Create ProjectService Interface and ProjectServiceImplementation
4. Create a Response model that include additional helpful properties
- timeStamp
- statusCode
- status (e.g. OK 200, CREATED 201, BAD_REQUEST 400, INTERNAL_SERVER_ERROR 500, etc)
- reason
- message (a human-readable message that can be shown to the end user)
- developerMessage (a more technical message for developers or for debugging purposes)
- data
5. Create ProjectResource / Controller to expose API URLs
6. Database configuration in application.yml
- Temporarily comment spring-boot-starter-security in pom.xml for API Testing
(Sunday, January 07, 2024, 20:59)
- Protect API routes (Only unprotected routes can be accessed by non-authenticated users)
- Create new user account functionality
- Log in with existing account
- Authenticated users are authorized
- Roles to manage access control
Spring Boot API with Spring Security and Docker - Security Overview - 1h09m
From Roland Toussaint "Junior": Whenever we add the spring boot security dependency in our spring boot application, we will have a "Spring Security Filter Chain" in front of our application (the REST API) / in front of the "Dispatcher Servlet":
In Spring Security, we have to configure an authentication provider - Spring Security also has a default authentication provider called DaoAuthenticationProvider. We can also provide a UserDetailsService which tells spring security how to load the users (e.g. load users from memory / a file / database).
Spring Documentation:
- https://spring.io/guides/topicals/spring-security-architecture/
- Authentication Provider (does the actual authentication, checks for username password email etc) https://docs.spring.io/spring-security/site/docs/4.0.x/apidocs/org/springframework/security/authentication/AuthenticationProvider.html
AbstractUserDetailsAuthenticationProvider
Spring Boot API with Spring Security and Docker - Security Overview - 1h13m
Other useful resources:
- https://stackoverflow.com/questions/76993442/deprecated-csrf-and-requireschannel-methods-after-spring-boot-v3-migration
- https://codejava.net/frameworks/spring-boot/spring-security-fix-deprecated-methods
- https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter/
Let's begin:
- On
\src\main\java\com\radubulai\springbootapisecurity: Createsecuritypackage - Within
securitypackage, createWebSecurityConfigurationjava class
// WebSecurityConfiguration.java using Spring Security 5.7.11 and Spring Boot 2.7.18
package com.radubulai.springbootapisecurity.security;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 2024-01-08
*/
@RequiredArgsConstructor
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration {
private final AccountAuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.authenticationProvider(authenticationProvider);
http.csrf().disable();
http.authorizeHttpRequests()
.antMatchers(HttpMethod.POST, "/api/accounts/**").permitAll();
http.authorizeHttpRequests()
.anyRequest()
.hasAnyRole("USER", "ADMIN")
.and()
.httpBasic(Customizer.withDefaults())
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return http.build();
}
}Notes:
- In Spring Security 6.x, we will also need to add
@Configurationannotation:
// In Spring Security 6.x, the annotations will be:
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
/* @EnableGlobalMethodSecurity(prePostEnabled = true) is DEPRECATED
* Use @EnableMethodSecurity instead which has prePostEnabled default true
* https://stackoverflow.com/questions/74910066/enableglobalmethodsecurity-is-deprecated-in-the-new-spring-boot-3-0
*/
@EnableMethodSecurity
public class WebSecurityConfiguration {}-
We will create
AccountAuthenticationProviderlater, which will be used to verify if the password entered is not null and it matches the actual account's password (AccountAuthenticationProvideris a custom implementation for authenticating user accounts). -
http.csrf().disable();disables CSRF (Cross-Site Request Forgery) protection. This is acceptable in stateless scenarios, like when using JWT (JSON Web Tokens) for authentication. -
Authorization Configuration:
.authorizeHttpRequests().antMatchers(HttpMethod.POST, "/api/accounts/**").permitAll();permits all POST requests to "/api/accounts/**" without authentication..authorizeHttpRequests().anyRequest().hasAnyRole("USER", "ADMIN")requires any other request to be authenticated and have either "USER" or "ADMIN" role.
-
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);configures session management to be stateless. This is common in RESTful APIs where sessions are not used, and each request is expected to carry authentication information.
From ChatGPT 3.5 - Sunday, January 21, 2024
A stateless session, also known as sessionless or token-based authentication, refers to a type of session management in which the server does not store any information about the client's state between requests. Each request from a client to the server is treated as an independent and self-contained transaction. In the context of web applications and APIs, stateless sessions are often associated with token-based authentication mechanisms.
Key characteristics of stateless sessions:
- No Server-Side Session Storage: - Unlike traditional server-side session management, where the server stores session information on behalf of the client, stateless sessions do not rely on server-side storage. Each request is self-contained and includes all the information needed for authentication.
- Token-Based Authentication: - Stateless sessions often use tokens (e.g., JSON Web Tokens or JWTs) to carry authentication information. The client includes the token in the request header, and the server validates the token to authenticate the user.
- Scalability: - Stateless sessions are inherently more scalable because the server does not need to maintain session state for each client. This makes it easier to distribute requests across multiple servers in a load-balanced environment.
- Reduced Server-Side Complexity: - With stateless sessions, there is no need for the server to manage and persist session data. This simplifies server-side logic and eliminates the need for server affinity, making the system more robust and scalable.
- Independence of Requests: - Each client request contains all the information needed for the server to process the request, including authentication details. The server does not rely on information from previous requests or maintain any client-specific state between requests.
- Token Expiration Handling: - Token-based authentication often involves the use of expiration times within the tokens. Clients need to obtain new tokens periodically by re-authenticating, providing a mechanism for managing access and improving security.
Stateless sessions are particularly well-suited for scenarios where scalability, simplicity, and independence of requests are critical, such as in RESTful APIs and microservices architectures. They are commonly used in combination with token-based authentication to secure communication between clients and servers in a stateless manner.
HTTP Basic Authentication is a simple authentication scheme built into the HTTP protocol. It involves the client sending a base64-encoded string containing both the username and password with each HTTP request. The server, in turn, checks the provided credentials against its authentication system.
In Spring Security, configuring HTTP Basic Authentication with default settings involves using the
httpBasic()method.http.httpBasic(Customizer.withDefaults());
httpBasic(): - This method is part of theHttpSecurityconfiguration in Spring Security. It indicates that you want to configure HTTP Basic Authentication for your application.
Customizer.withDefaults(): - TheCustomizerclass is used to customize various aspects of Spring Security configurations.withDefaults()is a method that provides a set of default settings for HTTP Basic Authentication.When you use
httpBasic(Customizer.withDefaults()), you are essentially configuring your application to use HTTP Basic Authentication with the following default settings:
Username and Password Prompt: - When a client tries to access a secured resource without providing valid credentials, the server responds with an HTTP 401 Unauthorized status and includes a
WWW-Authenticateheader, prompting the client to provide a username and password.Default Realm: - The realm is a description of the protected area. With default settings, the realm is typically set to "Realm", but you can customize it if needed.
Security Configuration for Stateless Authentication: - It aligns with the stateless session management policy, meaning each request must contain valid authentication credentials, and there is no server-side session state (stateless session).
Here's a simplified example of what an HTTP response header might look like when using HTTP Basic Authentication (Note: see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate)
HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="Realm"The client then includes an
Authorizationheader in subsequent requests with the base64-encoded credentials:GET /secured/resource HTTP/1.1 Host: example.com Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=In the provided code,
Customizer.withDefaults()is used to accept the default settings for HTTP Basic Authentication, providing a quick way to enable basic authentication in Spring Security with minimal configuration.
Spring Boot API with Spring Security and Docker - Account Domain Model - 1h13m
- In order to define the
authenticationProvider, we first need to define the user accounts, namely theAccountandRolemodels, repositories, and services. - Inside
modelpackage, createAccountjava class
// Account.java
package com.radubulai.springbootapisecurity.model;
// import jakarta.persistence.*; // for Spring Boot 3.2.1 and its dependencies
import javax.persistence.*; ; // for Spring Boot 2.7.18 and its dependencies
import javax.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Set;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 2024-01-08
*/
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Account {
@Id
@GeneratedValue
private Long id;
@Column(unique = true)
@NotNull
private String username;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@NotNull
private String password;
private boolean enabled = true;
private boolean credentialsExpired = false;
private boolean expired = false;
private boolean locked = false;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(
name = "AccountRole",
joinColumns = @JoinColumn(name = "accountId", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "roleId", referencedColumnName = "id")
)
private Set<Role> roles;
}Note: Using @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) will prevent the password from being exposed in the JSON response of an account (password field will be hidden if we make a GET request to /api/accounts or api/accounts/<username> endpoints).
- Inside
modelpackage, createRolejava class
package com.radubulai.springbootapisecurity.model;
// import jakarta.persistence.*; // for Spring Boot 3.2.1 and its dependencies
import javax.persistence.*; ; // for Spring Boot 2.7.18 and its dependencies
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 2024-01-08
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Role {
@Id
@GeneratedValue
private Long id;
private String code;
private String name;
// Role - High level
// Permissions or authorities - more specific
}Spring Boot API with Spring Security and Docker - Account Repository - 1h28m
- Inside
repositorypackage, createAccountRepositoryjava interface
package com.radubulai.springbootapisecurity.repository;
import com.radubulai.springbootapisecurity.model.Account;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 2024-01-10
*/
public interface AccountRepository extends JpaRepository<Account, Long> {
Account findAccountByUsername(String username);
}Note: The username needs to be unique.
- Inside
repositorypackage, createRoleRepositoryjava interface
package com.radubulai.springbootapisecurity.repository;
import com.radubulai.springbootapisecurity.model.Role;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RoleRepository extends JpaRepository<Role, Long> {
Role findRoleByName(String name);
}Spring Boot API with Spring Security and Docker - Account Service Implementation - 1h32m
- Inside
servicepackage, createAccountServicejava interface
package com.radubulai.springbootapisecurity.service;
import com.radubulai.springbootapisecurity.model.Account;
import java.util.List;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 2024-01-10
*/
public interface AccountService {
Account createAccount(Account account);
Account findAccountByUsername(String username);
List<Account> findAllAccounts();
}- Inside
service/implementationpackage, createAccountServiceImpljava class
// AccountServiceImpl.java
package com.radubulai.springbootapisecurity.service.implementation;
import com.radubulai.springbootapisecurity.model.Account;
import com.radubulai.springbootapisecurity.model.Role;
import com.radubulai.springbootapisecurity.repository.AccountRepository;
import com.radubulai.springbootapisecurity.repository.RoleRepository;
import com.radubulai.springbootapisecurity.service.AccountService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 2024-01-10
*/
@Service
@RequiredArgsConstructor
public class AccountServiceImpl implements AccountService {
private final AccountRepository accountRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
@Override
public Account createAccount(Account account) {
account.setPassword(passwordEncoder.encode(account.getPassword()));
Role role = roleRepository.findRoleByName("ROLE_USER");
Set<Role> roles = new HashSet<>();
roles.add(role);
account.setRoles(roles);
return accountRepository.save(account);
}
@Override
public Account findAccountByUsername(String username) {
return accountRepository.findAccountByUsername(username);
}
@Override
public List<Account> findAllAccounts() {
return accountRepository.findAll();
}
}
Annotations:
@Service: Indicates that this class is a service component in a Spring application. It is used to annotate classes at the service layer, which typically contains business logic.@RequiredArgsConstructor: Lombok annotation that generates a constructor with required fields. In this case, it generates a constructor for theAccountServiceImplclass with theaccountRepository,roleRepository, andpasswordEncoderfields.createAccount Method:
account.setPassword(passwordEncoder.encode(account.getPassword()));: Encodes the user's password using the injectedPasswordEncoder. This is crucial for storing passwords securely/encoded. The "account" object is retrieved from theAccountResource(Controller) when making a POST request to/api/accounts.Role role = roleRepository.findRoleByName("ROLE_USER");: Retrieves the user role ("ROLE_USER") from the database using theRoleRepository(so we can assign the role to the new user account).
- Note: the "ROLE_USER" entry must exist beforehand in the MySQL database. Here it would be better if we check/create an exception for this case.
Set<Role> roles = new HashSet<>(); roles.add(role);: Creates a set of roles and adds the retrieved role to it.account.setRoles(roles);: Associates the set of roles with the user account.return accountRepository.save(account);: Saves the user account with the encoded password and associated role(s) to the database using theAccountRepository. The savedAccountentity is then returned.findAccountByUsername Method:
public Account findAccountByUsername(String username): Method for finding a user account by username.return accountRepository.findAccountByUsername(username);: Retrieves a user account from the database based on the provided username using theAccountRepository.findAllAccounts Method:
public List<Account> findAllAccounts(): Method for retrieving a list of all user accounts.return accountRepository.findAll();: Retrieves all user accounts from the database using theAccountRepository.This service class encapsulates the logic for managing user accounts, including creating accounts, finding accounts by username, and retrieving a list of all accounts. It integrates with repositories for database interaction and uses a password encoder for secure password storage.
Spring Boot API with Spring Security and Docker - Account Service Implementation - 1h37m
For the above AccountServiceImpl, we need to define the password encoder.
- Inside
securitypackage, createSecurityConfigurationjava class
package com.radubulai.springbootapisecurity.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 2024-01-10
*/
@Configuration
public class SecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Annotations:
@Configuration: Indicates that this class is a configuration class for the Spring application context. It is used to define beans, configurations, and other settings.
passwordEncoderBean Method:
@Bean: Indicates that this method produces a bean that should be managed by the Spring container. Note: Beans created using@Beanhave a singleton scope. This means that a single instance of the bean is shared across the entire Spring container. A Spring bean is essentially an instance of a class that is managed by the Spring container, and it is typically created from a class that is annotated with@Component,@Service,@Repository, or defined using@Bean.public PasswordEncoder passwordEncoder(): This method creates and configures aPasswordEncoderbean.return new BCryptPasswordEncoder();: It returns a new instance ofBCryptPasswordEncoder.BCryptPasswordEncoderis one of the implementations of thePasswordEncoderinterface provided by Spring Security.
BCryptPasswordEncoder:
BCryptPasswordEncoderis a password encoder that uses the bcrypt hashing function. It's a popular choice for securely hashing passwords. The bcrypt algorithm incorporates a salt and is designed to be computationally expensive, making it resistant to brute-force attacks.Purpose:
- The purpose of this configuration class is to define a bean for the
PasswordEncoder. ThePasswordEncoderis a crucial component in Spring Security for encoding and verifying passwords. When storing user passwords in a database, it's a best practice to store hashed and salted versions of the passwords to enhance security.- By providing a
BCryptPasswordEncoderbean, the application can use this bean to encode passwords before storing them and to verify passwords during the authentication process.- The
BCryptPasswordEncoderis a sensible default choice for password encoding in Spring Security. However, you can customize the password encoding strategy based on your application's security requirements.In summary, the
SecurityConfigurationclass ensures that aBCryptPasswordEncoderbean is available in the Spring application context, allowing other components, such as theAccountServiceImpl, to use it for secure password storage and verification in the authentication process.
How BCryptPasswordEncoder Works:
Hashing with Salt: BCrypt uses a technique called "salt" to enhance password security. A salt is a random value unique to each password hash.
Cost Factor: BCrypt allows you to configure a cost factor, which determines the computational cost of hashing. Higher cost factors make hashing more computationally expensive and time-consuming, adding an extra layer of security.
Hashing Algorithm: BCrypt uses the Blowfish encryption algorithm internally. It's designed to be slow and computationally intensive, which helps protect against brute-force attacks.
Example Usage:
1] Hashing a Password:
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class BCryptExample { public static void main(String[] args) { String rawPassword = "mySecretPassword"; // Create an instance of BCryptPasswordEncoder with a default strength (cost factor of 10) BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); // Hash the password with salt String hashedPassword = passwordEncoder.encode(rawPassword); System.out.println("Raw Password: " + rawPassword); System.out.println("Hashed Password: " + hashedPassword); } }In this example,
rawPasswordis hashed using theBCryptPasswordEncoder. The resulting hash includes the salt, and the output will look something like this:Raw Password: mySecretPassword Hashed Password: $2a$10$XhBWssBDg/LupX.H6Sc9JeCx8kcKXeKzHDxjz5ntM1kLEyAT53b762] Verifying a Password:
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class BCryptExample { public static void main(String[] args) { String rawPassword = "mySecretPassword"; String hashedPassword = "$2a$10$XhBWssBDg/LupX.H6Sc9JeCx8kcKXeKzHDxjz5ntM1kLEyAT53b76"; // Create an instance of BCryptPasswordEncoder BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); // Check if the raw password matches the hashed password boolean matches = passwordEncoder.matches(rawPassword, hashedPassword); System.out.println("Password Matches: " + matches); } }In this example, the
matchesmethod is used to verify if the raw password matches the hashed password. It returnstrueif the passwords match, indicating a successful verification.This is a simple demonstration of how to hash and verify passwords using
BCryptPasswordEncoder. It's important to note that the actual hashed password will be different each time due to the random salt generated during the hashing process. Thematchesmethod takes care of extracting the salt from the stored hash for verification.
Spring Boot API with Spring Security and Docker - Account Service Implementation - 1h39m
Bafore we create the authenticationProvider (needed in WebSecurityConfiguration > SecurityFilterChain) we need to create the UserDetailsService interface and the UserDetailsServiceImpl class.
UserDetailsService implementation will tell Spring Security how to load the users (that can be now stored in the MySQL database via Account table)
"Junior: We know how to load the users in Spring, but Spring Security does not know yet, and to tell Spring Security how, we need to implement/override the
loadUserByUsernamemethod withinUserDetailsService"
- Inside
service/implementationpackage, createUserDetailsServiceImpljava class- Note: The
UserDetailsServiceinterface already exists inorg.springframework.security.core.userdetails.UserDetailsService;, and we only need to override theloadUserByUsernamemethod
- Note: The
// UserDetailsServiceImpl.java
package com.radubulai.springbootapisecurity.service.implementation;
import com.radubulai.springbootapisecurity.model.Account;
import com.radubulai.springbootapisecurity.service.AccountService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 2024-01-10
*/
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final AccountService accountService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = accountService.findAccountByUsername(username);
if (account == null) {
throw new UsernameNotFoundException(String.format("User %s not found", username));
}
if (account.getRoles() == null || account.getRoles().isEmpty()) {
throw new RuntimeException(String.format("User %s has no roles", username));
}
Collection<SimpleGrantedAuthority> authorities = account.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName())).toList();
return new User(account.getUsername(),
account.getPassword(),
account.isEnabled(),
!account.isExpired(),
!account.isCredentialsExpired(),
!account.isLocked(),
authorities);
}
}The
UserDetailsServiceImplclass is a service class that implements the Spring SecurityUserDetailsServiceinterface. This interface is a core part of Spring Security, responsible for loading user-specific data for authentication.
- Annotations:
@Service: Indicates that this class is a service component in a Spring application.@RequiredArgsConstructor: Lombok annotation that generates a constructor with required fields.- Fields:
private final AccountService accountService;: Autowired instance ofAccountService, which presumably provides methods for accessing user account information.loadUserByUsernameMethod:
- This method is from the
UserDetailsServiceinterface and is overridden here to load user-specific data during authentication.- It takes a username as input and returns a
UserDetailsobject containing user information.- Inside the method, the
AccountServiceis used to find anAccountobject by username.- If the account is not found, it throws a
UsernameNotFoundException.- If the account doesn't have any roles associated with it, it throws a
RuntimeException.- It maps the roles associated with the account to
SimpleGrantedAuthorityobjects, which represent the authorities (roles) granted to the user.- Finally, it constructs and returns a
UserDetailsobject with the username, password, account status (enabled, expired, credentials expired, locked), and authorities.
Spring Boot API with Spring Security and Docker - Account Service Implementation - 1h45m
(Friday, Jan 26, 2024, 16:59)
This class is responsible for authenticating users based on the provided credentials and loading user details using the UserDetailsService created earlier.
- Inside
securitypackage, createAccountAuthenticationProviderjava class
// AccountAuthenticationProvider.java
package com.radubulai.springbootapisecurity.security;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* @author Radu-Alexandru Bulai (<a href="https://radubulai.com">https://radubulai.com</a>)
* @version 1.0
* @since 2024-01-08
*/
@Component
@RequiredArgsConstructor
public class AccountAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
@Override
protected void additionalAuthenticationChecks(
UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null || userDetails.getPassword() == null) {
throw new BadCredentialsException("Credentials may not be null");
}
if (!passwordEncoder.matches((String) authentication.getCredentials(), userDetails.getPassword())) {
throw new BadCredentialsException("Invalid credentials");
}
}
@Override
protected UserDetails retrieveUser(
String username,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
return userDetailsService.loadUserByUsername(username);
}
}Now AccountAuthenticationProvider authenticationProvider can be used/injected within SecurityFilterChain filterChain.
Spring Boot API with Spring Security and Docker - Account Resource - 1h50m
So far we do not have any users (or any roles) stored in the database, therefore the app resources are not accessible.
We can create the account resource class that will allow us to make POST requests to create new users/accounts (Note that in the SecurityFilterChain, we will allow such POST requests to http://localhost:8080/api/accounts/ URL).
- Inside
resourcepackage, createAccountResourcejava class
package com.radubulai.springbootapisecurity.resource;
import com.radubulai.springbootapisecurity.model.Account;
import com.radubulai.springbootapisecurity.model.Response;
import com.radubulai.springbootapisecurity.service.AccountService;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import static java.time.LocalDateTime.now;
@RestController
@RequiredArgsConstructor
@RequestMapping(path = "/api/accounts")
public class AccountResource {
private final AccountService accountService;
@GetMapping
public ResponseEntity<Response> getAllAccounts() {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("accounts", accountService.findAllAccounts()))
.message("Accounts retrieved")
.status(HttpStatus.OK)
.statusCode(HttpStatus.OK.value())
.build());
}
@GetMapping("/{username}")
public ResponseEntity<Response> getAccountByUsername(@PathVariable String username) {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("account", accountService.findAccountByUsername(username)))
.message(String.format("Account %s retrieved", username))
.status(HttpStatus.OK)
.statusCode(HttpStatus.OK.value())
.build());
}
@PostMapping
public ResponseEntity<Response> createAccount(@RequestBody @Valid Account account) {
return ResponseEntity.ok(
Response.builder()
.timeStamp(now())
.data(Map.of("account", accountService.createAccount(account)))
.message("Account created")
.status(HttpStatus.CREATED)
.statusCode(HttpStatus.CREATED.value())
.build());
}
}So far, here is how the codebase will look like:
Or, if we order the components by their implementation order:
Spring Boot API with Spring Security and Docker - Application Runner - 1h55m
Currently, since accounts needs to be associated with a role, we will also need to have some roles stored in the database when the application is running.
We can either insert some roles into the MySQL database by ourselves manually, e.g.:
INSERT INTO `projectmanagerapp`.`role` (`id`, `code`, `name`) VALUES ('1', '123', 'ROLE_USER');
INSERT INTO `projectmanagerapp`.`role` (`id`, `code`, `name`) VALUES ('2', '456', 'ROLE_ADMIN');OR: we can use the ApplicationStartRunner class (implements CommandLineRunner) to insert some roles into the database automatically when the application starts.
- On
\src\main\java\com\radubulai\springbootapisecurity: Createinitializationpackage - Inside
initializationpackage, createApplicationStartRunnerjava class
// ApplicationStartRunner.java
package com.radubulai.springbootapisecurity.initialization;
import com.radubulai.springbootapisecurity.model.Role;
import com.radubulai.springbootapisecurity.repository.AccountRepository;
import com.radubulai.springbootapisecurity.repository.RoleRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import static java.util.Arrays.asList;
@Component
@RequiredArgsConstructor
public class ApplicationStartRunner implements CommandLineRunner {
private final RoleRepository roleRepository;
@Override
public void run(String... args) throws Exception {
Role roleUser = new Role(1L, "123", "ROLE_USER");
Role roleAdmin = new Role(2L, "456", "ROLE_ADMIN");
roleRepository.saveAll(asList(roleUser, roleAdmin));
}
}The
ApplicationStartRunnerclass is a part of the application initialization process. It ensures that the database has the necessary roles available when the application starts.
Annotations:
@Component: Indicates that this class is a Spring-managed component and should be automatically detected and registered as a bean in the Spring application context.@RequiredArgsConstructor: Lombok annotation that generates a constructor with required fields.Fields:
private final RoleRepository roleRepository;: Autowired instance ofRoleRepository, which is used to interact with the database to store role entities.
runMethod:
- This method is from the
CommandLineRunnerinterface, and it's invoked when the Spring Boot application starts up.- It creates instances of the
Roleentity representing different roles in the application. In this case, it creates "ROLE_USER" and "ROLE_ADMIN" roles.- It saves these roles to the database using the
roleRepository.saveAllmethod.Populating the Database:
- When the Spring Boot application starts, the
runmethod of theApplicationStartRunnerclass is executed automatically.- This ensures that the database is populated with the initial roles required for user management and authorization.
Note: The above might update the roles if they already exist in the database when starting the application. Optionally, we can check if the roles already exists in the database:
@Slf4j
@Component
@RequiredArgsConstructor
public class ApplicationStartRunner implements CommandLineRunner {
private final RoleRepository roleRepository;
@Override
public void run(String... args) throws Exception {
createRoleIfNotExists(new Role(1L, "123", "ROLE_USER"));
createRoleIfNotExists(new Role(2L, "456", "ROLE_ADMIN"));
}
private void createRoleIfNotExists(Role newRole) {
Optional<Role> existingRole = Optional.ofNullable(roleRepository.findRoleByName(newRole.getName()));
if (existingRole.isPresent()) {
log.info("Role '{}' already exists in the database.", newRole.getName());
return;
}
try {
roleRepository.save(newRole);
log.info("Role '{}' added in database.", newRole);
} catch (Exception e) {
assert log != null;
log.error("Failed to add role '{}' in database: {}", newRole, e.getMessage());
}
}
}(Sunday, January 28, 2024, 21:30)
Spring Boot API with Spring Security and Docker - Application Runner - 2h00m
🔵 Send a GET request to http://localhost:8080/api/projects without basic auth credentials
- Expected Response: 401 Unauthorized
🔵 Send a POST request to http://localhost:8080/api/accounts/ with new Account information (No auth credentials)
{
"username": "raduu",
"password": "123"
}⬅The server will respond:
{
"timeStamp": "2024-01-28T21:11:40.2450229",
"statusCode": 201,
"status": "CREATED",
"message": "Account created",
"data": {
"account": {
"id": 7,
"username": "raduu",
"enabled": true,
"credentialsExpired": false,
"expired": false,
"locked": false,
"roles": [
{
"id": 1,
"code": "123",
"name": "ROLE_USER"
}
]
}
}
}Note: Using
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)in Account.java model will prevent the password from being exposed in the JSON response of an account (password field will be hidden if we make a GET or POST request to/api/accountsorapi/accounts/<username>endpoints).
🔵 Send a GET request to http://localhost:8080/api/projects with basic auth credentials
⬅The server will respond with Status 200 OK
{
"timeStamp": "2024-01-28T21:27:12.595993",
"statusCode": 200,
"status": "OK",
"message": "Projects retrieved",
"data": {
"projects": [
{
"id": 1,
"keyName": "PORTFOLIO",
"name": "Custom HappyCat Portfolio",
"description": "Customized Portfolio for HappyCat Portfolio via separate contract",
"createdAt": "2024-01-06"
},
{
"id": 2,
"keyName": "CRMMANAGER",
"name": "CRM Manager Backend API in Spring Boot",
"description": "Official Project for the BACKEND of CRM Manager Application",
"createdAt": "2024-01-07T20:26:48.465963800"
}
]
}
}🔵 Send a GET request to http://localhost:8080/api/accounts with basic auth credentials
⬅The server will respond with Status 200 OK
{
"timeStamp": "2024-01-28T21:29:31.0205829",
"statusCode": 200,
"status": "OK",
"message": "Accounts retrieved",
"data": {
"accounts": [
{
"id": 7,
"username": "raduu",
"enabled": true,
"credentialsExpired": false,
"expired": false,
"locked": false,
"roles": [
{
"id": 1,
"code": "123",
"name": "ROLE_USER"
}
]
}
]
}
}Commit message for all Spring Security implementation from above:
Implement Security Filter Chain for REST API in Spring Security 5.7.11
- Downgrade Spring Boot 3.2.1 to Spring Boot 2.7.18
- Spring Security 6.x will be downgrated as well to Spring Security 5.7.11
- In Account.java, Project.java, Role.java models
- Change imports of `import jakarta.persistence.*; import jakarta.validation.constraints.NotEmpty;` (Sprint Boot 3.2.1) to `import javax.persistence.*; import javax.validation.constraints.*;` (Spring Boot 2.7.18)
- In application.yml
- Change automatically dialect `dialect: org.hibernate.dialect.MySQLDialect` (Spring Boot 3.2.1) to `dialect: org.hibernate.dialect.MySQL5InnoDBDialect` (Spring Boot 2.7.18)
- pom.xml
- Uncomment dependency org.springframework.boot
- Account.java, AccountRepository, AccountService + Role.java, RoleRepository
- Implement Account and Role models, repositories and service
- UserDetailsServiceImpl.java
- Implement User Details Service to tell Spring Security how to load the users / accounts
- AccountAuthenticationProvider.java
- Implement Account Authentication Provider for validating user credentials and loading the user details from database (via UserDetailsServiceImpl by overriding the loadUserByUsername method)
- AccountResource.java
- Expose REST API endpoints for Accounts (especially for POST request in order to create a new account)
- WebSecurityConfiguration.java
- Define the SercurityFilterChain to set a series of security filters that intercept incoming HTTP Requests and apply security policies
(Sunday, January 28, 2024, 21:51 - Radu-Alexandru B.)Spring Boot API with Spring Security and Docker - Dockerfile - 2h20m
TODO...
NEXT STEPS AFTER LEARNING BASIC AUTH (with username/pass sent on every request) - Learn token based auth such as OAuth https://auth0.com/intro-to-iam/what-is-oauth-2















