Only this pageAll pages
Powered by GitBook
1 of 13

Spring Book

Loading...

Tutorials

Loading...

Recipes

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

About this book

Inside the Guide

This guide is a concise collection of spring boot knowledge we picked up over the years of Java development, the sections are divided up as follows:

  • Tutorials - longer format deep dives into topics

  • Recipes - quick code first solution oriented bites

Any feedback would be very much appreciated at [email protected]

If you like this content you may want to partner with us for your development needs

While our guide provides the some quick tips, sometimes achieving your project goals requires a bit more. That's where we come in. At Katyella, we provide US based staff augmentation. Whether you're looking to scale your team quickly or need specific expertise, we're here to help.

Interested in learning more about how we can help bring your projects to the next level? Visit us at katyella.com. Let's make your development journey a shared success.

Handling Exceptions in RESTful User Responses

@RestController
@RequestMapping("/api/books")
public class BookController {

    @Autowired
    private BookService bookService;

    // Method to retrieve a book by its ID
    @GetMapping("/{id}")
    public ResponseEntity<Book> getBookById(@PathVariable Long id) {
        Book book = bookService.findBookById(id)
                .orElseThrow(() -> new BookNotFoundException("Book with ID: " + id + " not found."));
        return new ResponseEntity<>(book, HttpStatus.OK);
    }

    // Exception handler for BookNotFoundException
    @ExceptionHandler(BookNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ResponseEntity<String> handleBookNotFoundException(BookNotFoundException ex) {
        // Log the exception details for debugging
        Logger.log(ex.getMessage());

        // Return a user-friendly message and the appropriate HTTP status
        return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body("Book not found. Please check the provided book ID.");
    }

    // ... other methods or exception handlers ...
}

Explanation:

In the code snippet above, the BookController class includes:

  1. Method getBookById:

    • It uses the bookService to attempt to find the book. If the book is not found, bookService.findBookById(id) will return an empty Optional, and the orElseThrow method will throw a BookNotFoundException.

    • The exception message includes the ID of the book that was not found, providing clarity in the logs and to the API users.

  2. Exception Handler handleBookNotFoundException:

    • This method remains the same as before. It handles the BookNotFoundException by logging the exception details and returning a user-friendly message along with a 404 Not Found status.

By including the getBookById method, we demonstrate a typical scenario in a RESTful service where a requested resource (in this case, a book) might not exist, leading to an exception that is gracefully handled by the handleBookNotFoundException method.


This structure ensures that your REST API for book management not only handles requests but also deals with exceptions in a user-friendly manner, improving the reliability and usability of your service.

Global REST Error Responses via @ControllerAdvice

Explanation: In RESTful services, ensuring consistent and user-friendly error responses across the entire application is crucial. The @ControllerAdvice annotation in Spring Boot allows you to handle exceptions globally, avoiding the need to declare exception handlers in each controller. Just by having a class with this annotation in the class path. Here's how you can implement global exception handling for a book management RESTful service:


Code Implementation:

  1. Annotation @ControllerAdvice: This annotation declares the class as a global exception handler, making the exception handling methods within applicable across all controllers.

  2. Method handleBookNotFoundException:

    • This method is marked with @ExceptionHandler(BookNotFoundException.class), indicating it handles exceptions of type BookNotFoundException.

    • The @ResponseStatus(HttpStatus.NOT_FOUND) annotation ensures that a 404 Not Found status is returned.

    • The method logs the exception details and returns a ResponseEntity with a user-friendly error message.

Benefits of Using @ControllerAdvice:

  • Consistency: Ensures that the same exception across different controllers results in a consistent response.

  • Cleaner Code: Reduces duplication by avoiding repetitive exception handlers in each controller.

  • Centralized Configuration: Makes it easier to manage and maintain exception handling logic in one place.

By leveraging @ControllerAdvice or @RestControllerAdvice (for RESTful services where the response body is always expected), you can efficiently manage exceptions globally, ensuring that your RESTful service is robust, consistent, and user-friendly.


This approach to handling exceptions globally streamlines your error management strategy, making your RESTful service more maintainable and your error responses more predictable and informative.

@ControllerAdvice
public class GlobalExceptionHandler {

    // Global exception handler for BookNotFoundException
    @ExceptionHandler(BookNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ResponseEntity<String> handleBookNotFoundException(BookNotFoundException ex) {
        // Log the exception details for debugging
        Logger.log(ex.getMessage());

        // Return a user-friendly message and the appropriate HTTP status
        return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body("Book not found. Please check the provided book ID.");
    }

    // ... other global exception handlers ...
}

Disabling OAuth2 Security for Integration Tests with @TestConfiguration

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.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@TestConfiguration
public class IntegrationTestConfig {

    /**
     * Configures a security filter chain to disable OAuth2 security for integration tests.
     *
     * @param http the {@link HttpSecurity} to configure
     * @return the {@link SecurityFilterChain} configured to disable OAuth2 security
     * @throws Exception if an error occurs while configuring security
     */
    @Bean
    public SecurityFilterChain disableOAuth2Security(HttpSecurity http) throws Exception {
        return http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(authorize -> authorize
                        .anyRequest().permitAll())
                .build();
    }
}

TL;DR:

The @TestConfiguration annotation allows for the customization of Spring Boot's application context for testing purposes, without affecting the main application configuration. In the provided code snippet, @TestConfiguration is used to define a SecurityFilterChain bean that disables OAuth2 security, simplifying integration testing by bypassing authentication and authorization steps.

Explanation:

  • Purpose of @TestConfiguration: This specialized configuration annotation is designed for use in tests, enabling developers to override or add additional configuration without influencing the main application context. It's particularly useful for setting up or mocking certain behaviors specific to testing scenarios.

  • Disabling OAuth2 Security: For integration tests, particularly those not focusing on security, it's often practical to bypass security constraints to directly test business logic and integration points. The provided SecurityFilterChain bean method disables CSRF protection and configures Spring Security to permit all requests, effectively neutralizing OAuth2 security for tests.

  • Integration with Test Classes: To apply this configuration, the @Import(IntegrationTestConfig.class) annotation should be added to your test classes. This ensures that the test context includes the overridden security configuration, allowing tests to run without the need for authenticating requests.

Benefits:

  • Simplified Testing Environment: By disabling security features that are not relevant to certain integration tests, developers can focus on the functionality and integration aspects of the application.

  • Isolated Configuration: Since @TestConfiguration is only applied to tests where it's explicitly imported, there's no risk of it affecting the production configuration or other tests.

  • Flexibility and Control: This approach provides fine-grained control over the test environment, allowing for more accurate and efficient testing of specific components or functionalities.

By strategically using @TestConfiguration to disable OAuth2 security, developers can ensure that their integration tests are both effective and efficient, focusing on the core functionality of the application under test.

Custom Acutator Endpoints

import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Map;

@Component
@Endpoint(id = "activeSessions")
public class ActiveSessionsEndpoint {

    private final SessionRegistry sessionRegistry;

    public ActiveSessionsEndpoint(SessionRegistry sessionRegistry) {
        this.sessionRegistry = sessionRegistry;
    }

    @ReadOperation
    public Map<String, Integer> activeSessionsCount() {
        return Collections.singletonMap("activeSessions", sessionRegistry.getAllPrincipals().size());
    }
}

TL;DR:

The @Endpoint annotation in Spring Boot Actuator allows for the creation of custom endpoints to expose specific application metrics or operations. In the provided code snippet, a custom endpoint activeSessions is defined to report the current number of active sessions, leveraging the SessionRegistry for real-time session tracking. This addition enhances monitoring capabilities by providing insights into user engagement and system load.

Detailed Explanation:

  • Purpose of @Endpoint: The @Endpoint annotation marks a class as a custom Actuator endpoint, enabling it to expose specific data or operations through the Actuator framework. It's a powerful feature for extending the built-in monitoring and management capabilities of Spring Boot.

  • Exposing Active Sessions Count: The activeSessions endpoint is designed to help monitor the current load on the application by reporting the number of active user sessions. This information is crucial for understanding user engagement and for scaling and performance tuning efforts.

  • Utilizing SessionRegistry: The SessionRegistry is a Spring Security class that tracks all active sessions. By injecting SessionRegistry into the custom endpoint, it's possible to count and report the number of active sessions in real-time.

  • Accessing the Custom Endpoint:

    • HTTP GET Request: http://localhost:8080/actuator/activeSessions

    • Response Example:

      {
        "activeSessions": 42
      }

      This response indicates the current number of active sessions, providing immediate insight into application usage.

Benefits:

  • Enhanced Monitoring: Adding a custom endpoint for active sessions directly addresses the need for real-time monitoring of user engagement, offering a clear view of the application's current load.

  • Operational Efficiency: With this endpoint, administrators and developers can quickly assess the application's performance and responsiveness, enabling proactive adjustments to infrastructure and application settings.

  • Strategic Decision Making: Information on active sessions aids in capacity planning and scalability strategies, ensuring that the application can handle peak loads efficiently.

By integrating a custom Actuator endpoint to monitor active sessions, Spring Boot applications can significantly improve their operational monitoring and management capabilities. This approach not only provides valuable insights into application performance but also supports informed decision-making for enhancing user experience and system reliability.

WebScrapping on a Schedule

Spring Boot application for web scraping with JSoup

TL;DR:

  • Setup: This Spring Boot application is configured to perform web scraping tasks using JSoup. It includes the @EnableScheduling annotation to enable scheduled tasks.

  • Scheduled Task: The WebScrapingService class contains a method scrapeWebsite annotated with @Scheduled, set to execute every 10 seconds. This method uses JSoup to connect to a specified URL, retrieves the document, and prints the website's title.

  • Running: Upon running the application, the scheduled task will automatically scrape the website at the defined interval, demonstrating a basic use case of web scraping in a Spring Boot application.

This example provides a streamlined approach to integrating web scraping capabilities into a Spring Boot application, showcasing the ease of setting up scheduled tasks with JSoup for HTML parsing.

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@SpringBootApplication
@EnableScheduling
public class WebScrapingApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebScrapingApplication.class, args);
    }

    @Service
    public static class WebScrapingService {

        @Scheduled(fixedRate = 10000) // Runs every 10 seconds
        public void scrapeWebsite() {
            try {
                Document doc = Jsoup.connect("https://example.com").get();
                String title = doc.title();
                System.out.println("Website Title: " + title);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Simplifying Spring Services with Lombok

Lombok is a Java library that helps reduce boilerplate code, particularly useful in Spring applications for creating cleaner, more maintainable codebases.

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {

    private final UserRepository userRepository;

    public User findUserById(Long id) {
        log.info("Finding user by ID: {}", id);
        return userRepository.findById(id).orElse(null);
    }
}

import lombok.Data;

@Data
public class User {
    private Long id;
    private String name;
    private String email;
}

TL;DR:

Lombok's @RequiredArgsConstructor, @Slf4j, and @Data annotations significantly reduce the boilerplate code in Spring services and entities. @RequiredArgsConstructor automatically generates a constructor for dependency injection, @Slf4j provides a logging capability, and @Data bundles common methods like getters, setters, and toString() for data classes.

Detailed Explanation

Before Lombok

Traditionally, a Spring service with a dependency on a repository and a simple data class might look like this:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Service
public class UserService {

    private static final Logger log = LoggerFactory.getLogger(UserService.class);
    private UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User findUserById(Long id) {
        log.info("Finding user by ID: {}", id);
        return userRepository.findById(id).orElse(null);
    }
}

public class User {
    private Long id;
    private String name;
    private String email;

    public User() {}

    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 String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    @Override
    public String toString() {
        return "User{" +
               "id=" + id +
               ", name='" + name + '\'' +
               ", email='" + email + '\'' +
               '}';
    }
}

After Lombok

With Lombok, the same functionality is achieved with less code:

  • @RequiredArgsConstructor eliminates the need for an explicit constructor.

  • @Slf4j automatically injects a log instance, removing the manual logger declaration.

  • @Data generates getters, setters, toString(), equals(), and hashCode() methods for the User class.

Additional Lombok Features Demonstrated

  • @Slf4j: Simplifies logging throughout the application. Just use log.info(), log.debug(), etc., without manually setting up the logger.

  • @Data: Ideal for data classes or entities. It not only includes getters and setters but also essential methods like toString(), making object debugging and logging more straightforward.

Benefits of Using Lombok in Spring Services

  • Reduced Boilerplate Code: Lombok annotations decrease the amount of manually written code, making the service and model layers cleaner and more readable.

  • Enhanced Maintainability: Less code means easier maintenance and fewer places for bugs to hide.

  • Improved Development Speed: Developers can focus on business logic rather than writing and maintaining repetitive code structures.

Conclusion

Integrating Lombok into Spring Boot applications offers a clear path to more concise, maintainable code. By automating common coding tasks, Lombok allows developers to concentrate on what's truly important, enhancing productivity and the overall quality of the code. Whether you're building complex business services or simple data classes, Lombok's suite of annotations can significantly streamline your development process.

Referencing Values from Properties File in Components

  1. Properties File (application.properties)

  1. Spring Component

In Spring Boot applications, you can easily externalize configuration and access properties in your components. The above code demonstrates how you can define properties in the application.properties file and inject them into your Spring components using the @Value annotation.

Explanation:

  • Properties File (application.properties):

    • Contains key-value pairs defining properties and their values.

    • bookstore.name and bookstore.location are the properties used in this example.

  • Spring Component (BookstoreProperties):

    • Annotated with @Component, making it a Spring-managed bean.

    • Uses the @Value annotation to inject property values from the application.properties file.

    • The syntax ${property.name} within @Value specifies the property to be injected.

    • printBookstoreDetails is a method to display the injected values, demonstrating that the values have been successfully injected.

Benefits:

  • Decoupling of Configuration and Code: Enables changing the application's behavior without code changes by modifying the properties file.

  • Ease of Maintenance: Centralizes configuration management, simplifying changes and management.

  • Flexibility for Different Environments: Allows for different configurations in development, testing, and production environments without changing the codebase.

By leveraging the @Value annotation in Spring Boot, you can maintain clean separation between your configuration and code, making your application more adaptable and easier to manage across different environments.

# Define properties
bookstore.name=My Awesome Bookstore
bookstore.location=Main Street, Springfield
@Component
public class BookstoreProperties {

    @Value("${bookstore.name}")
    private String name;

    @Value("${bookstore.location}")
    private String location;

    // getters and setters

    public void printBookstoreDetails() {
        System.out.println("Bookstore Name: " + name);
        System.out.println("Bookstore Location: " + location);
    }
}

Logging Entities in Spring with Lombok

Effectively use Lombok for logging while cautiously managing entities with lazy-loaded relationships to circumvent potential performance issues.

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.persistence.*;
import java.util.Set;

@Entity
@Getter
@Setter
@ToString(exclude = "orders") // Exclude lazy-loaded fields from toString
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private Set<Order> orders; // Lazy-loaded relationship
}

Service Class with Lombok Logging:

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class UserService {

    public void updateUser(User user) {
        // Safe logging without initializing lazy-loaded 'orders'
        log.info("Updating user: {}", user);
    }
}

Log Output Example:

Updating user: User(id=1, name=John Doe, [email protected])

TL;DR:

Utilizing Lombok's @ToString annotation with the exclude attribute allows for the exclusion of lazy-loaded fields, such as orders in a User entity, preventing unintended loading during logging. The @Slf4j annotation simplifies logging setup, enabling efficient and safe logging practices in Spring applications. This approach ensures that entity logging is both informative and performance-conscious.

Detailed Explanation:

Before Lombok Integration:

Traditionally, logging entity information and managing lazy-loaded relationships required manually writing boilerplate code for logging setup and toString() methods, carefully avoiding lazy-loaded fields to prevent performance issues.

After Lombok Integration:

  • Simplified Logging: The @Slf4j annotation automatically injects a logger, reducing the boilerplate code for logger initialization.

  • Safe toString() Implementation: The @ToString(exclude = "orders") annotation generates a toString() method that excludes specified lazy-loaded fields, mitigating the risk of accidentally triggering database queries for uninitialized data.

Benefits of This Approach:

  • Performance Optimization: By excluding lazy-loaded fields from the toString() method, applications avoid unnecessary database hits and potential LazyInitializationException errors outside of transactional contexts.

  • Enhanced Maintainability: Reducing boilerplate code for logging and toString() methods makes the codebase cleaner and easier to maintain.

  • Improved Developer Productivity: Developers can focus on business logic rather than worrying about logging setup and the intricacies of lazy loading with Hibernate.

Conclusion

Lombok provides powerful annotations like @Slf4j and @ToString that simplify logging and toString implementations in Spring applications. When working with Hibernate entities, especially those with lazy-loaded relationships, Lombok's features help maintain performance and prevent common issues associated with lazy loading. This approach allows developers to enjoy the benefits of concise code and efficient logging while ensuring application performance remains optimal.

Proper vs. Improper Way to Implement CRUD in RESTful Services

In this quick recipe, we'll explore the proper and improper ways to implement CRUD (Create, Read, Update, Delete) operations in RESTful services

Proper Way to Implement CRUD in Spring Boot

Service Layer:

Assuming basic CRUD operations are defined in the UserService.

Improper Way to Implement CRUD in Spring Boot

Controller:

Key Points:

  • HTTP Methods: The proper implementation uses HTTP methods according to REST principles (POST for creation, GET for reading, PUT for updating, and DELETE for deletion). The improper way misuses HTTP methods, leading to semantic confusion and potential security risks.

  • URI Structure: A RESTful URI should be descriptive and hierarchical, focusing on resources rather than actions. The proper example uses /api/users with resource IDs for specific operations, while the improper example uses action-based URIs like /createUser, which is not recommended.

  • Response Entity: The proper way uses ResponseEntity to provide more control over the HTTP response, allowing for the setting of status codes and headers. The improper way returns the model directly, limiting the ability to respond with appropriate HTTP status codes or headers.

This recipe highlights the importance of adhering to RESTful principles and Spring Boot best practices to create clear, efficient, and maintainable API endpoints.

@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserService userService;

    // Create
    @PostMapping
    public ResponseEntity<Object> createUser(@RequestBody User user, UriComponentsBuilder uriBuilder) {
        User createdUser = userService.saveUser(user);

        // Create URI of the created user using UriComponentsBuilder parameter
        URI location = uriBuilder.path("/api/users/{id}")
                                 .buildAndExpand(createdUser.getId())
                                 .toUri();

        return ResponseEntity.created(location).build();
    }

    // Read
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = userService.findById(id);
        return ResponseEntity.ok(user);
    }

    // Update
    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User userDetails) {
        User updatedUser = userService.updateUser(id, userDetails);
        return ResponseEntity.ok(updatedUser);
    }

    // Delete
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}
@RestController
@RequestMapping("/api")
public class UserController {

    @Autowired
    private UserService userService;

    // Create
    @GetMapping("/createUser") // Incorrect HTTP method and URI
    public User createUser(@RequestBody User user) {
        return userService.saveUser(user);
    }

    // Read
    @PostMapping("/getUser") // Incorrect HTTP method and URI
    public User getUserById(@RequestParam Long id) {
        return userService.findById(id);
    }

    // Update
    @GetMapping("/updateUser/{id}") // Incorrect HTTP method and URI
    public User updateUser(@PathVariable Long id, @RequestBody User userDetails) {
        return userService.updateUser(id, userDetails);
    }

    // Delete
    @GetMapping("/deleteUser") // Incorrect HTTP method and URI
    public void deleteUser(@RequestParam Long id) {
        userService.deleteUser(id);
    }
}

JWT Authentication and Role-Based Authorization with Java Spring Boot

Introduction to JSON Web Tokens (JWTs), Authentication, and Authorization

JSON Web Tokens (JWTs) are a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret or a public/private key pair using the RSA or ECDSA algorithms.

Role in Authentication

Authentication is the act of validating that users are who they claim to be using passwords, biometrics, one-time passwords, and more. JWTs can be used to establish the identity of users. When a user logs in, the server generates a JWT that encodes a set of claims. The server then returns this token to the client, and the client sends the token back with each request. The server verifies the token and only allows the request to proceed if the token is valid.

Role in Authorization

Authorization is the process by which individual users or groups of users are granted access to a specific resource or function. JWTs also play a crucial role in authorization, as they enable servers to know what resources a user can have access to. When the JWT includes claims like user roles or permissions, the server can determine whether to grant or deny access to certain operations or resources.

Importance in API Security and Access Control

Securing APIs and managing access control are critical in web applications. JWTs help in securing APIs by allowing the server to process requests without having to look up too much user information since the token is self-contained. The authorization process using JWTs enables role-based access control, ensuring that users only have access to the resources and operations their role permits.

Overall, JWTs are integral to the security architecture of modern web applications, enabling reliable authentication and fine-grained authorization while also being lightweight and easily transmittable over the web.

Project Setup

Overview of Tools and Technologies

In this project, we are going to employ several key tools and technologies:

  • Java Spring Boot: An open-source Java-based framework used to create a microservice. It is easy to create stand-alone, production-grade Spring based applications with minimal effort.

  • H2 Database: A lightweight in-memory database that can be embedded in Java applications or run in the client-server mode. It's perfect for development and testing since it doesn't require any setup like a regular database.

  • JSON Web Tokens (JWT): A standard token format used in authentication and authorizations that allows you to securely transmit data between parties as a JSON object.

Creating Your Spring Boot Project

  1. Visit the Spring Initializr website.

  2. Select Maven as your Project Type and Java as your language

  3. Choose your desired version of Spring Boot. This tutorial uses 3.2.2.

  4. Provide the Project Metadata for your project.

  5. Choose Jar as the packaging option.

  6. Select the version of Java you are using. This tutorial uses 21.

  7. Click on "Generate" to download the project template.

Once downloaded and extracted, create the following Java Packages in your <ArtifactId> folder (com in this diagram)

<ProjectName>
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       ├── configs
|   |   |       ├── controllers
|   |   |       ├── dto
|   |   |       ├── filters
|   |   |       ├── models
|   |   |       ├── repositories
|   |   |       ├── services
│   │   └── resources
│   └── test
│       └── java
├── .gitignore
├── mvnw
├── mvnw.cmd
├── pom.xml
└── README.md

Adding Necessary Dependencies

To include the necessary dependencies for Spring Security, the H2 database, and JWT, open the pom.xml file and add the following dependencies:

<dependencies>
	<!-- H2 Database -->
	<dependency>
		<groupId>com.h2database</groupId>
		<artifactId>h2</artifactId>
	<version>2.2.220</version>
	</dependency>
	<dependency>
		<groupId>io.github.cdimascio</groupId>
		<artifactId>dotenv-java</artifactId>
		<version>3.0.0</version>
	</dependency>
	<dependency>
		<groupId>org.hibernate.validator</groupId>
		<artifactId>hibernate-validator</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-jpa</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-oauth2-client</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<version>1.18.30</version>
	</dependency>
	<!-- Spring Security -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-security</artifactId>
		<version>3.1.2</version>
	</dependency>
	<dependency>
		<groupId>io.jsonwebtoken</groupId>
		<artifactId>jjwt-jackson</artifactId>
		<version>0.11.5</version>
	</dependency>
	<!-- JWT -->
	<dependency>
		<groupId>io.jsonwebtoken</groupId>
		<artifactId>jjwt-impl</artifactId>
		<version>0.11.5</version>
	</dependency>
	<dependency>
		<groupId>org.apache.commons</groupId>
		<artifactId>commons-lang3</artifactId>
		<version>3.12.0</version>
	</dependency>
	<dependency>
		<groupId>io.jsonwebtoken</groupId>
		<artifactId>jjwt-api</artifactId>
		<version>0.11.5</version>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-devtools</artifactId>
		<version>3.1.2</version>
	</dependency>
</dependencies>

Make sure to consult the official documentation for up-to-date version numbers.

Set Properties in `application.resources`

We will need to configure a few properties related to our H2 database, logging, JWT authentication, and authorization. Here are the settings used in our project:

# H2 Database Configuration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.h2.console.enabled=true
spring.datasource.initialize=true
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Logging
logging.level.root=WARN
logging.level.web=INFO
logging.level.com.example=DEBUG
logging.level.org.springframework.security=DEBUG

spring.jpa.generate-ddl=true

# Generate a secret token.secret.key for our application
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
token.secret.key=<JWT Secret>

# JWT expiration is 1 hour
token.expirationms=3600000

Configuring Spring Security

To secure our web application, we implement Spring Security by defining a SecurityConfig class. This class utilizes a combination of annotations to integrate Spring Security with our project effectively.

Below is an overview of what the SecurityConfig class accomplishes:

  • @EnableWebSecurity: This annotation activates Spring Security’s web security support and provides the Spring MVC integration. It also extends the Spring Security configuration using HTTPSecurity.

  • @EnableMethodSecurity: This enables method-level security based on annotations. It allows us to secure methods in our controllers by specifying roles and conditions for access.

  • @Configuration: The class is marked as a configuration class, and it defines Spring Beans.

  • @RequiredArgsConstructor: This is a Lombok annotation that generates a constructor with required dependencies (in this case, JwtAuthenticationFilter, UserService, and PasswordEncoder).

The SecurityConfig includes the following Beans:

  • AuthenticationProvider: This Bean sets the custom user details service and password encoder. It is necessary for authenticating users based on username and password.

  • AuthenticationManager: This Bean gets the authentication manager from Spring Security's AuthenticationConfiguration which in turns help manage the authentication procedure.

  • SecurityFilterChain: This crucial Bean within the Spring Security filter chain, allows us to configure the rules and conditions for security along different paths in our application. We disable CSRF protection as it is not needed for REST APIs and define stateless session management. Furthermore, we specify URL patterns and the associated security settings, permitting everyone to access the signup and signin endpoints, whereas protecting all other endpoints. Finally, we ensure that the JWT filter is applied before the UsernamePasswordAuthenticationFilter to extract and verify the token.

Here's how we set up the SecurityConfig:

package dev.katyella.oauth.configs;

import dev.katyella.oauth.filters.JwtAuthenticationFilter;
import dev.katyella.oauth.services.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final UserService userService;
    private final PasswordEncoder passwordEncoder;

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userService.userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder);
        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers(HttpMethod.POST, "/api/v1/signup", "/api/v1/signin").permitAll()
                        .requestMatchers(HttpMethod.GET, "/api/v1/test/**").permitAll()
                        .anyRequest().authenticated()
                )
                .authenticationProvider(authenticationProvider()).addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

This setup is pivotal for JWT-based authentication, ensuring that each request is properly authenticated and has the correct authority to access various resources.

Implement JWT Authentication

JWT Authentication is a key aspect in securing web applications by validating the identity of users via tokens. Let's dive into how the JwtService class in the provided code snippet helps in implementing JWT authentication:

The JwtService class performs several essential actions:

  • Token Generation: generateToken(UserDetails userDetails) method generates a new token for a user based on their UserDetails. It includes the user's username as the subject, current time as the issue time, and sets the expiration time based on the jwtExpirationMs value.

  • Token Validation: isTokenValid(String token, UserDetails userDetails) method checks if the token is valid by comparing the username in the token with the username in the passed UserDetails, and also checks if the token has expired.

  • Extract Claims: Various methods such as extractAllClaims, extractExpiration, and extractUserName help in retrieving specific claims from the token like subject (username) and expiration.

  • Secret Key: It uses the jwtSecretKey declared in application.properties to decode and sign the tokens.

Here's the entire JwtService code for clarity:

package dev.katyella.oauth.services;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;

@Service
public class JwtService {

    @Value("${token.secret.key}")
    String jwtSecretKey;

    @Value("${token.expirationms}")
    Long jwtExpirationMs;

    public String extractUserName(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public String generateToken(UserDetails userDetails) {
        return generateToken(new HashMap<>(), userDetails);
    }

    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String userName = extractUserName(token);
        return (userName.equals(userDetails.getUsername())) && !isTokenExpired(token);
    }

    private <T> T extractClaim(String token, Function<Claims, T> claimsResolvers) {
        final Claims claims = extractAllClaims(token);
        return claimsResolvers.apply(claims);
    }

    private String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
        return Jwts
                .builder()
                .setClaims(extraClaims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    private Claims extractAllClaims(String token) {
        return Jwts
                .parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    private Key getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(jwtSecretKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

Role-Based Authorization

This code snippet is a sample controller which demonstrates how role-based access-control can be implemented using Spring Security's @PreAuthorize annotation. It provides access to different REST endpoints based on the authenticated user's role.

  • GET /api/v1/test/users is secured with @PreAuthorize("hasRole('USER')"), restricting access to this endpoint to only allow authenticated users with the USER role.

  • GET /api/v1/test/admins is annotated with @PreAuthorize("hasRole('ADMIN')"), restricting access to this endpoint to only allow authenticated users with the ADMIN role.

  • GET /api/v1/test/role_dependent programmatically checks the user's role without using @PreAuthorize, offering a more dynamic approach to role-based access-control.

A more robust BookControllerthat interfaces with our database will be implemented in the next step. This BookController will use these principles of role-based authorization to manage access to book-related resources.

@RestController
@RequestMapping("/api/v1/test")
public class TestController {

    @GetMapping("/anon")
    public String anonEndPoint() {
        return "everyone can see this";
    }

    @GetMapping("/users")
    @PreAuthorize("hasRole('USER')")
    public String usersEndPoint() {
        return "ONLY users can see this";
    }

    @GetMapping("/admins")
    @PreAuthorize("hasRole('ADMIN')")
    public String adminsEndPoint() {
        return "ONLY admins can see this";
    }

    @GetMapping("/role_dependent")
    public String roleDependentEndPoint() {
        // Get authenticated user
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        // Check if admin role
        if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
            return "Data for admins";
        } else {
            String username = authentication.getName();
            return "Data for user: " + username;
        }
    }
}

Build the Books API

Now that we have set up our project, implemented JWT-based authentication and role-based authorization, let's build the Books API to manage book-related resources. We'll create a BookController to handle CRUD (Create, Read, Update, Delete) operations on books.

@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class BookController {
    @Autowired
    private BookRepository bookRepository;

    @GetMapping("/books")
    public List<Book> getAllBooks() {
        // Get the authenticated user
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = authentication.getName();

        // If user is admin, return all books
        if (authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
            return bookRepository.findAll();
        } else {
            // Otherwise, return books created by the user
            return bookRepository.findByCreatedBy(username);
        }
    }

    @GetMapping("/books/{id}")
    public ResponseEntity<Book> getBookById(@PathVariable(value = "id") Long bookId) throws ConfigDataResourceNotFoundException {
        // Get the authenticated user
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String username = authentication.getName();

        Book book = bookRepository.findById(bookId)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Book not found for this id :: " + bookId));

        // Check if the book belongs to the authenticated user or if the user is admin
        if (book.getCreatedBy().equals(username) || authentication.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
            return ResponseEntity.ok().body(book);
        } else {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You are not authorized to access this resource");
        }
    }
    
    // More endpoints for handling creation, updating, deleting...

Test the Application

Now we can start our application, sign up and sign with a new user or sign in with a pre-populated one, and use the JWT returned from that request to access our various endpoints according to the role assigned to our user!

Here's how we can sign up our user:

POST http://localhost:8080/api/v1/signup
Content-Type: application/json

{
    "firstName": "John",
    "lastName": "Doe",
    "email": "[email protected]",
    "password": "password"
}

POST http://localhost:8080/api/v1/signin
Content-Type: application/json

{
    "email": "[email protected]",
    "password": "password"
}

Or log in with our admin user:

POST http://localhost:8080/api/v1/signin
Content-Type: application/json

{
    "email": "[email protected]",
    "password": "password"
}

Experiment with different calls with different roles to see how our application restricts what data is available to a user based on their role. Check the /src/test directory in the repo to view some example API calls. You will need to provide the JWT token fetched in the /signin step, and remember, it expires after 1 hour!

Here's an example of accessing the user-restricted endpoint as a user role:

GET http://localhost:8080/api/v1/test/users
Authorization: Bearer {{USER_AUTH_TOKEN}}