In the microservice-based architecture, it is a common practice to expose application functionality in terms of RESTful APIs. These APIs can then be accessed via a range of application devices, such as desktop applications, mobile devices, as well as other APIs.
In this chapter, we’ll introduce you to designing and building RESTful APIs with Spring Boot. You’ll also learn to document the API, so the API consumers can find required details about the API, such as the request, response structures, HTTP return codes, etc. Finally, you’ll learn to develop unit test cases to test the API. Lastly, we’ll show you how to secure your RESTful API. Let’s get started.
A RESTful API (also known as REST API) is an application programming interface that follows the constraints of REST architectural style. REST is an acronym for representational state transfer and was created by Roy Fielding (http://mng.bz/Exyq). In a REST API, when a client requests a resource from the server, the server provides a representation of the state of the requested resource to the client. This representation can be delivered through various formats, such as JSON, plain text, HTML, and others. However, JSON is the most widely used format in the REST API parlance.
Spring Boot provides built-in support in the framework to design and build REST APIs. Spring Boot is one of the most popular frameworks in the Java space for developing REST APIs. In this section, we’ll explore developing a RESTful API with Spring Boot.
In this technique, we’ll demonstrate how to develop a RESTful API using Spring Boot.
Previously, you’ve used the Course Tracker Spring Boot application with Thymeleaf as the frontend. You now need to expose the Course Tracker application as a RESTful API. Exposing application backend functionality as RESTful API allows the decoupling of application backend with the frontend UI. This design approach lets you opt for the application frontend frameworks (e.g., Angular, React, Vue, etc.) of your choice without being tightly coupled with the backend.
Designing RESTful APIs with Spring Boot is relatively easy, as the framework provides built-in support for it. These days Spring Boot is the de facto choice for Java developers to build RESTful APIs. If you are following the previous chapters, then you are already aware of most of the content for building a RESTful API with Spring Boot.
In chapter 3, we discussed the use of Spring Data and talked about the approaches to configuring and using a database in a Spring Boot application. In chapter 5, we demonstrated building Spring Boot applications by using Spring controllers in conjunction with Spring Data repositories.
With this technique, you’ll build a RESTful API for the Course Tracker application. It will expose the REST endpoints shown in table 7.1.
Returns the list of courses with the supplied course category name | ||
Table 7.1 contains the REST endpoints that let you perform the CRUD operations in the Course Tracker application. To keep the example simple, we’ve only introduced a limited number of endpoints. In a production application, you may define more REST endpoints. For instance, you can have a few more GET endpoints that let you filter application data to meet application requirements. However, to demonstrate the concepts, we’ll use these REST endpoints throughout this chapter, as this endpoint covers the fundamental operations (CRUD) that most APIs support.
In the Course Tracker application, we are managing Course
details. Therefore, we will define the course business entity. The following listing shows this class.
package com.manning.sbip.ch07.model; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import lombok.Data; @Data @Entity @Table(name = "COURSES") public class Course { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ID") private Long id; @Column(name = "NAME") private String name; @Column(name = "CATEGORY") private String category; @Column(name = "RATING") private int rating; @Column(name = "DESCRIPTION") private String description; }
The Course
is a Java POJO that models the course details in the application with fields such as course id, name, category, rating, and description. Next, let’s define the CourseRepository
interface, which lets us manage the courses in the database.
package com.manning.sbip.ch07.repository; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; import com.manning.sbip.ch07.model.Course; @Repository public interface CourseRepository extends CrudRepository<Course, Long> { Iterable<Course> findAllByCategory(String category); }
The CourseRepository
interface extends the CrudRepository
interface and defines a custom method findAllByCategory
(..)
that finds all courses belonging to a specific category.
Let’s now define the service layer with an interface that provides supported operations in the application. The following listing shows the CourseService
interface.
package com.manning.sbip.ch07.service; //imports public interface CourseService { Course createCourse(Course course); Optional<Course> getCourseById(long courseId); Iterable<Course> getCoursesByCategory(String category); Iterable<Course> getCourses(); void updateCourse(long courseId, Course course); void deleteCourseById(long courseId); void deleteCourses(); }
The methods defined in listing 7.3 are self-explanatory and contain method declarations that allow us to perform the CRUD operations in the application. Let’s now provide a default implementation that provides implementations of these methods.
Generally, it is a best practice to define an interface consisting of the operations supported in the API. This interface provides a contract to the controller with the operations supported in the service layer. You can then provide a concrete class that implements these operations. Further, in the controller class, you use the interface name instead of specifying the actual implementation class. This allows you to decouple the controller from the actual implementation. In the future, if you need to provide a different implementation of the service layer, your controller class is not impacted, as it uses the interface and is not tied to a specific implementation. Listing 7.4 shows the CourseServiceImpl
class.
package com.manning.sbip.ch07.service; //imports @Service ① public class CourseServiceImpl implements CourseService { @Autowired ② private CourseRepository courseRepository; @Override public Course createCourse(Course course) { return courseRepository.save(course); } @Override public Optional<Course> getCourseById(long courseId) { return courseRepository.findById(courseId); } @Override public Iterable<Course> getCoursesByCategory(String category) { return courseRepository.findAllByCategory(category); } @Override public Iterable<Course> getCourses() { return courseRepository.findAll(); } @Override public void updateCourse(Long courseId, Course course) { courseRepository.findById(courseId).ifPresent(dbCourse -> { dbCourse.setName(course.getName()); dbCourse.setCategory(course.getCategory()); dbCourse.setDescription(course.getDescription()); dbCourse.setRating(course.getRating()); courseRepository.save(dbCourse); }); } @Override public void deleteCourses() { courseRepository.deleteAll(); } @Override public void deleteCourseById(long courseId) { courseRepository.deleteById(courseId); } }
① Annotated with @Service to indicate it’s a service
② Autowires the CourseRepository to perform the database operations
The CourseServiceImpl
class is annotated with @Service
annotation to indicate it’s a service. Recall that @Service
is a Spring stereotype annotation that indicates the annotated class is a service class and contains business logic. Further, it uses the CourseRepository
to perform the necessary database operations.
We are now left with defining the CourseController
that defines the REST endpoints. A Spring controller contains one of more endpoints and accepts the client requests. It then, optionally, uses the services offered by the service layer and generates a response. It wraps the response in a model and shares it with the view layer. A RestContoller
also performs a similar activity. However, instead of wrapping the response in the model and sharing to the view layer, it binds the response to the HTTP response body, which is directly shared with the endpoint requester. The following listing shows the CourseController
class.
package com.manning.sbip.ch07.controller; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.manning.sbip.ch07.model.Course; import com.manning.sbip.ch07.service.CourseService; @RestController ① @RequestMapping("/courses/") public class CourseController { @Autowired private CourseService courseService; @GetMapping ② public Iterable<Course> getAllCourses() { return courseService.getCourses(); } @GetMapping("{id}") ③ public Optional<Course> getCourseById(@PathVariable("id") long ➥ courseId) { return courseService.getCourseById(courseId); } @GetMapping("category/{name}") ④ public Iterable<Course> getCourseByCategory(@PathVariable("name") ➥ String category) { return courseService.getCoursesByCategory(category); } @PostMapping ⑤ public Course createCourse(@RequestBody Course course) { return courseService.createCourse(course); } @PutMapping("{id}") ⑥ public void updateCourse(@PathVariable("id") long courseId, ➥ @RequestBody Course course) { courseService.updateCourse(courseId, course); } @DeleteMapping("{id}") ⑦ void deleteCourseById(@PathVariable("id") long courseId) { courseService.deleteCourseById(courseId); } @DeleteMapping ⑧ void deleteCourses() { courseService.deleteCourses(); } }
① The RequestMapping annotation specified the route or the path to the API. In this example, we have defined the path /courses/ so that all HTTP requests to the /courses/ path are redirected to this controller.
② A GetMapping is a special type of RequestMapping that handles only the HTTP GET request. As no path is specified in this endpoint, it is the default endpoint for the HTTP GET /courses/ endpoint.
③ Handles HTTP GET requests for the path /courses/{id}. The {id} is a path variable and replaced with an appropriate value, e.g. /courses/1, where 1 is the value of the path variable ID.
④ Handles HTTP GET requests for the path /courses/category/{name}. The {name} is a path variable and replaced with an appropriate value (e.g., /courses/ category/Spring, where Spring is the value of the path variable name).
⑤ Handles HTTP POST requests for the path /courses/. An HTTP POST request accepts a request payload. You use the @RequestBody annotation to specify the request body. Note that the requester typically sends a JSON payload, and in the endpoint you expect a Java POJO class that represents the JSON payload. Spring Boot internally performs this deserialization to convert the JSON to the Java type.
⑥ Handles the HTTP PUT operations for the path /courses/{id}. The HTTP PUT operation is used to perform the update operations. In this endpoint, we expect the ID of the resource that needs to be updated and the updated representation of the resource in the HTTP request payload. We use the @RequestBody to accept the request payload.
⑦ Represents the HTTP DELETE operation for the /courses/{id} path. In this endpoint, we delete the course for the supplied course ID.
⑧ Represents the HTTP DELETE operation for the /courses/ path. In this endpoint, we delete all available courses.
Listing 7.5 defines all the endpoints listed in table 7.1. We’ll explore this class in greater detail in the discussion section of this technique. However, one thing you should take note of is the use of @RestController
annotation instead of the previously used @Controller
annotation.
Let us start the application and access the endpoints. First, let’s create a course using the POST /courses/
endpoint. Listing 7.6 shows the HTTPie command to create a course.
> http POST :8080/courses/ name="Mastering Spring Boot" rating=4 ➥ category=Spring description="Mastering Spring Boot intends to teach ➥ Spring Boot with practical examples" HTTP/1.1 200 // Other HTTP Response Headers { "category": "Spring", "description": "Mastering Spring Boot intends to teach Spring Boot with ➥ practical examples", "id": 1, "name": "Mastering Spring Boot", "rating": 4 }
In listing 7.6, although we’ve supplied the request body data in key–value pair, the HTTPie tool internally converts it to a JSON payload. Once this command is executed in the terminal, a new course is created in the Course Tracker application. Let’s view the course details using the GET /courses/{id}
endpoint to retrieve course details with a courseId
obtained in the POST operation of listing 7.6. This is shown in the following listing.
> http GET :8080/courses/1 HTTP/1.1 200 // Other HTTP Response Headers { "category": "Spring", "description": "Mastering Spring Boot intends to teach Spring Boot with ➥ practical examples", "id": 1, "name": "Mastering Spring Boot", "rating": 4 }
You can try accessing other endpoints in the same manner to monitor the output.
With this technique, you’ve learned to create a complete RESTful API. We have kept the application extremely simple to demonstrate the concepts. Let’s now discuss a few best practices we’ve followed while designing the REST API.
If you notice, we’ve used JSON to accept the requests and similarly responded with JSON in the response. It is a best practice that the REST APIs accept request payloads in JSON and provide a response in JSON.
JSON is widely used to store and transfer data. Spring Boot provides built-in support to perform the mapping between JSON and Java POJOs and vice versa. For instance, if you notice in listing 7.6, you’ve sent a JSON request as the payload to create a new course in the application. However, the POST endpoint accepts a Course
instance. Spring Boot performs this deserialization internally for us. By default, it uses the Jackson library (https://github.com/FasterXML/jackson) to perform this mapping.
The next thing to notice is the use of nouns while defining the endpoint paths. It is a best practice to use the plural form of the noun (e.g., Course
, Person
, Vehicle
, etc.) to define the routes. We should not use verbs in the route paths as the HTTP request method already has a verb (e.g., GET, POST, etc.) that defines the actions. Letting the developers use the verbs in paths make the paths lengthy and inconsistent. For instance, to get the course details, one developer may use /getCourses
, whereas another can use /retrieveCourses
. However, the get or retrieve is already defined through the HTTP GET method. Thus, specifying it in the route path makes it redundant. Hence, GET /courses/
is the preferred endpoint path to get all courses. Similarly, the POST /courses/
is the appropriate endpoint to create a new course.
In listing 7.5, we’ve used the @RestController
annotation in place of the previously used @Controller
annotation. The @RestController
annotation is a convenience annotation that is meta-annotated with the @Controller
and @ResponseBody
annotations. The @ResponseBody
annotation indicates that a method’s return value should be bound to the HTTP response body.
Although the above API works well and serves its purpose, it has no exception handling. For instance, let’s try to delete a course that does not exist in the application. You’ll notice that you have been presented with an error and an ugly looking large stack trace. We’ll fix this in the next technique.
Exceptions are inevitable in software code. Numerous factors could cause an exceptional scenario in your code. For instance, in the RESTful API we’ve designed, a user could attempt to access or delete a course with a nonexisting course ID. They could also submit a malformed JSON request payload to create a new course through the POST endpoint. All these scenarios cause exceptions in the API. In this section, we’ll discuss how to handle these exceptions and provide a meaningful response to the user specifying the exception details.
In this technique, we’ll discuss how to handle exceptions in a RESTful API.
The previously defined RESTful API is unable to handle errors, as there is no exception handling in place. It presents the user with a large stack trace that is not intuitive and exposes application internal details. You need to handle exceptions and provide meaningful error responses.
Exception handling is an important aspect of a RESTful API. Typically, your APIs will be consumed by a variety of consumers and being able to provide a meaningful error response in the event of an exception scenario makes your API robust and user friendly.
In the API designed in section 7.1, we’ve not handled the exceptions and the default Spring Boot exception handling mechanism is in place. For instance, deleting a course that does not exist in the application presents the error message, as shown in the following listing.
C:sbip epo>http DELETE :8080/courses/10 HTTP/1.1 500 { "error": "Internal Server Error", "message": "No class com.manning.sbip.ch07.model.Course entity with id ➥ 10 exists!", "path": "/courses/10", "status": 500, "timestamp": "2021-06-23T16:38:20.105+00:00", "trace": "org.springframework.dao.EmptyResultDataAccessException: No ➥ class com.manning.sbip.ch07.model.Course entity with id 10 ➥ exists! at ➥ org.springframework.data.jpa.repository.support.SimpleJpaRepository.lam ➥ bda$deleteById$0(SimpleJpaRepository.java:166) at ➥ java.base/java.util.Optional.orElseThrow(Optional.java:401) at ➥ org.springframework.data.jpa.repository.support.SimpleJpaRepository.del ➥ eteById(SimpleJpaRepository.java:165) at ➥ java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native ➥ Method) at ➥ java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMe ➥ thodAccessorImpl.java:64) at // Remaining section of the exception is omitted
As you may notice, the above error message is not a desired one and contains details that are not of much use to the API users. It also exposes to the caller information about the tech stack used for the implementation of the API, which is generally considered a security flaw. Further, the HTTP response code is also generic (500 Internal Server Error), which indicates that a server-side error has occurred. In this technique, we’ll improve the Course Tracker RESTful API by implementing exception handling in the API.
To begin with, let’s first discuss the type of exceptions we may encounter in the application. For this API, we can have only a handful of exception scenarios. For instance, it may be possible that a user attempts to get, update, or delete a course that does not exist in the application. This should result in an HTTP 404 Not Found error, as the requested resource does not exist in the application. It is also possible that the user is submitting an incomplete/incorrect JSON payload, while creating or updating a course. Let’s handle these exception scenarios. This results in an HTTP 400 Bad Request status code, as the user request could not be processed because the server is unable to parse the request, since it is malformed. To handle the first scenario, let’s create a custom exception called CourseNotFoundException
, as shown in the following listing.
package com.manning.sbip.ch07.exception; public class CourseNotFoundException extends RuntimeException { private static final long serialVersionUID = 5071646428281007896L; public CourseNotFoundException(String message) { super(message); } }
This CourseNotFoundException
is thrown whenever API users attempt to access a course that does not exist in the application. Let’s now redefine the CourseServiceImpl
class, as shown in the following listing.
package com.manning.sbip.ch07.service; //imports @Service public class CourseServiceImpl implements CourseService { // Additional Code @Override public Course updateCourse(long courseId, Course course) { Course existingCourse = courseRepository.findById(courseId) .orElseThrow(() -> new CourseNotFoundException(String.format("No ➥ course with id %s is available", courseId))); existingCourse.setName(course.getName()); existingCourse.setCategory(course.getCategory()); existingCourse.setDescription(course.getDescription()); existingCourse.setRating(course.getRating()); return courseRepository.save(existingCourse); } @Override public void deleteCourseById(long courseId) { courseRepository.findById(courseId).orElseThrow(() -> new ➥ CourseNotFoundException("No course with id %s is available" + ➥ courseId)); courseRepository.deleteById(courseId); } }
Listing 7.10 shows the modified methods of CourseServiceImpl
class. For an update or a delete operation, if a course with the supplied courseId
does not exist in the application, we throw the CourseNotFoundException
.
Now that we’ve thrown the exception, what’s next? We need to define an exception handler that intercepts the thrown exception and executes custom exception handling logic. For instance, for an unhandled exception, the HTTP response code 500 Internal Server Error is returned. However, if a course with the supplied courseid
does not exist in the application, the appropriate HTTP error code should be 404 Not Found. The latter HTTP response code tells the API consumer the course they are accessing does not exist. Let’s define the GlobalExceptionHandler
class that defines the ExceptionHandler
s of our application, as shown in the following listing.
package com.manning.sbip.ch07.exception.handler; //imports @ControllerAdvice public class CourseTrackerGlobalExceptionHandler extends ➥ ResponseEntityExceptionHandler { @ExceptionHandler(value = {CourseNotFoundException.class}) public ResponseEntity<?> handleCourseNotFound(CourseNotFoundException ➥ courseNotFoundException, WebRequest request) { return super.handleExceptionInternal(courseNotFoundException, courseNotFoundException.getMessage(), new HttpHeaders(), ➥ HttpStatus.NOT_FOUND, request); } }
In the class in listing 7.11, you’ve defined a few ExceptionHandler
implementations that handle the exceptions and can be thrown while processing the requests. Let’s explore this class in detail:
This class is annotated with the @ControllerAdvice
annotation. This annotation is a specialized @Component
that allows you to declare the @ExceptionHandler
. The @ControllerAdvice
annotation allows writing global code that applies to a range of controllers
(and RestControllers
). Thus, the ExceptionHandler
defined in listing 7.11 applies to all controllers in the application.
This class extends the ResponseEntityExceptionHandler
class, which is a base class for @ControllerAdvice
annotated classes that provide a centralized exception handling across all @RequestMapping
annotated methods through @ExceptionHandler
methods. This class provides exception handling logic for a variety of exceptions that can occur in the application. We can extend this class and override the exception handling logic at our convenience.
We’ve defined a new ExceptionHandler
for our custom exception CourseNotFoundException
. In this implementation, we are setting the HTTP response code to 404 Not Found and the error message retrieved from the custom exception. Finally, we are invoking the superclass method handleExceptionInternal
(..)
with these details.
Let’s now start the application and try out replicating a few exceptions scenarios and observing the response. Let’s try deleting a course with a course ID that is not present in the application. The HTTPie command and the associated response is shown in the following listing.
C:sbip epo>http DELETE :8080/courses/1 HTTP/1.1 404 // HTTP Response Headers No course with id 1 is available
Notice that we have an appropriate HTTP status code 404 as well as a relevant error message that specifies the error. Moreover, the user does not see any reference to the technology used for the API implementation (i.e., no Spring Boot stack trace appearing anymore).
The ability of a RESTful API to handle various user errors and to respond with appropriate HTTP status codes and error messages makes it robust and user friendly. This makes the application more compliant with the RESTful paradigm itself.
While designing APIs, it is a common practice to first identify the possible error scenarios in the application. You can then define custom exception classes that define the identified error scenario. One advantage of designing a custom exception is that it allows you to model the exception in a better manner and provides flexibility to capture various details about the exception. You can then define the ExceptionHandler
that intercepts these exception classes and allows you to define custom error response. For instance, try defining an exception handler that handles the wrong request payloads and responds with the HTTP 400 bad request. We leave this as an exercise for the readers.
In the previous techniques, you’ve learned to design and build a RESTful API. Once you are done with the development, the next task is to test the endpoints of the API to ensure that the API is working as expected. There are multiple ways to test a REST API, as shown in figure 7.2.
So far, we’ve discussed using the command-line tool HTTPie that can be used to access the endpoints. You can also use the cURL utility to test the endpoints. If you are not comfortable with CLI utilities, GUI-based tools are another great alternative. In the REST API testing, Postman (https://www.postman.com/) is extensively used by API developers to test the APIs. Besides, if you are familiar with the Microsoft VS Code editor (https://code.visualstudio.com/), it also provides several extensions to enable testing support for the REST APIs. We won’t cover these utilities, as there are enough tutorials and how-to guides for these tools available on the internet.
In the next section, we’ll discuss how to test a REST API through integration testing. It is always a best practice to write test cases for the endpoints that are executed while you build the API. Let’s explore it in the next technique.
In this section, we’ll explore how to test a RESTful API.
We haven’t defined any test cases to test the REST API endpoints. To ensure the API endpoints are working correctly and are not broken while introducing new changes in the future, we need to define integration test cases.
In a typical application, to test your application classes, you either instantiate those and invoke the methods defined in it or use mocking frameworks, such as Mockito to mock the class and other components. In a Spring MVC application, we can similarly define test cases. However, that does not verify a few important MVC framework features, such as request mapping, validation, data binding, @ExceptionHandler, and others.
Spring MVC provides a testing framework that provides comprehensive testing capabilities for Spring MVC-based applications without the need for an actual server. This framework, also known as MockMVC, performs the MVC request handling via mock request and response objects.
In this technique, we’ll show you how to use the Spring MockMVC framework in a Spring Boot application to test a REST API. We’ll define integration test cases for the API endpoints we’ve defined in the previous techniques.
Let’s begin by defining the first test case that creates a course in the Course Tracker application. The following listing shows the class.
package com.manning.sbip.ch07; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.assertNotNull; import static ➥ org.springframework.test.web.servlet.request.MockMvcRequestBuilders.del ➥ ete; import static ➥ org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static ➥ org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static ➥ org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static ➥ org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static ➥ org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonP ➥ ath; import static ➥ org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import ➥ org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMo ➥ ckMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; import com.manning.sbip.ch07.model.Course; import com.manning.sbip.ch07.service.CourseService; @SpringBootTest @AutoConfigureMockMvc @ExtendWith(SpringExtension.class) class CourseTrackerApiApplicationTests { @Autowired private CourseService courseService; @Autowired private MockMvc mockMvc; @Test public void testPostCourse() throws Exception { Course course = Course.builder() .name("Rapid Spring Boot Application Development") .category("Spring") .rating(5) .description("Rapid Spring Boot Application ➥ Development").build(); ObjectMapper objectMapper = new ObjectMapper(); MockHttpServletResponse response = mockMvc.perform(post("/courses/") .contentType("application/json") .content(objectMapper.writeValueAsString(course))) .andDo(print()) .andExpect(jsonPath("$.*", hasSize(5))) .andExpect(jsonPath("$.id", greaterThan(0))) .andExpect(jsonPath("$.name").value("Rapid Spring Boot ➥ Application Development")) .andExpect(jsonPath("$.category").value("Spring")) .andExpect(jsonPath("$.rating").value(5)) .andExpect(status().isCreated()).andReturn().getResponse(); Integer id = ➥ JsonPath.parse(response.getContentAsString()).read("$.id"); assertNotNull(courseService.getCourseById(id)); } }
Let’s define various components we used in the class defined in listing 7.13:
The @SpringBootTest
annotation indicates the annotated class runs Spring Boot-based tests and provides necessary environmental support to run the test cases. It creates the Spring application context that creates all Spring beans needed to run the test cases.
The @AutoConfigureMockMvc
annotation enables and auto-configures the MockMVC framework. This annotation performs the heavy lifting to provide the necessary support, so we can simply autowire an instance of MockMVC and use it in the test.
The @ExtendWith(SpringExtension.class)
annotation integrates the Spring TestContext Framework with JUnit 5’s Jupiter programming model. @ExtendWith
is a JUnit 5 annotation that allows you to specify the extension to be used to run the test case.
We autowired the CourseService
and the MockMvc
instance in the class.
We used the mockMvc
instance to perform an HTTP POST operation with a sample course.
Once the request is fired, we use the andExpect
to assert various attributes. We’ve used the jsonpath
to extract the values from the JSON response. Lastly, we validate the HTTP response status code. Let’s now provide the test case to get the course by ID. The following listing shows this test case.
@Test public void testRetrieveCourse() throws Exception { Course course = Course.builder() .name("Rapid Spring Boot Application Development") .category("Spring") .rating(5) .description("Rapid Spring Boot Application ➥ Development").build(); ObjectMapper objectMapper = new ObjectMapper(); MockHttpServletResponse response = mockMvc.perform(post("/courses/") .contentType("application/json") .content(objectMapper.writeValueAsString(course))) .andDo(print()) .andExpect(jsonPath("$.*", hasSize(5))) .andExpect(jsonPath("$.id", greaterThan(0))) .andExpect(jsonPath("$.name").value("Rapid Spring Boot ➥ Application Development")) .andExpect(jsonPath("$.category").value("Spring")) .andExpect(jsonPath("$.rating").value(5)) .andExpect(status().isCreated()).andReturn().getResponse(); Integer id = JsonPath.parse(response.getContentAsString()).read("$.id"); mockMvc.perform(get("/courses/{id}",id)) .andDo(print()) .andExpect(jsonPath("$.*", hasSize(5))) .andExpect(jsonPath("$.id", greaterThan(0))) .andExpect(jsonPath("$.name").value("Rapid Spring Boot ➥ Application Development")) .andExpect(jsonPath("$.category").value("Spring")) .andExpect(jsonPath("$.rating").value(5)) .andExpect(status().isOk()); }
In listing 7.14, we’ve first created a course through the post()
method and then used the get()
method to retrieve the course details. Like the previous test case, we’ve asserted the various response parameters along with the HTTP response status code. Let’s now include the remaining test cases, as shown in the following listing.
@Test public void testInvalidCouseId() throws Exception { mockMvc.perform(get("/courses/{id}",100)) .andDo(print()) .andExpect(status().isNotFound()); } @Test public void testUpdateCourse() throws Exception { Course course = Course.builder() .name("Rapid Spring Boot Application Development") .category("Spring") .rating(3) .description("Rapid Spring Boot Application ➥ Development").build(); ObjectMapper objectMapper = new ObjectMapper(); MockHttpServletResponse response = mockMvc.perform(post("/courses/") .contentType("application/json") .content(objectMapper.writeValueAsString(course))) .andDo(print()) .andExpect(jsonPath("$.*", hasSize(5))) .andExpect(jsonPath("$.id", greaterThan(0))) .andExpect(jsonPath("$.name").value("Rapid Spring Boot ➥ Application Development")) .andExpect(jsonPath("$.category").value("Spring")) .andExpect(jsonPath("$.rating").value(3)) .andExpect(status().isCreated()).andReturn().getResponse(); Integer id = JsonPath.parse(response.getContentAsString()).read("$.id"); Course updatedCourse = Course.builder() .name("Rapid Spring Boot Application Development") .category("Spring") .rating(5) .description("Rapid Spring Boot Application ➥ Development").build(); mockMvc.perform(put("/courses/{id}", id) .contentType("application/json") .content(objectMapper.writeValueAsString(updatedCourse))) .andDo(print()) .andExpect(jsonPath("$.*", hasSize(5))) .andExpect(jsonPath("$.id").value(id)) .andExpect(jsonPath("$.name").value("Rapid Spring Boot ➥ Application Development")) .andExpect(jsonPath("$.category").value("Spring")) .andExpect(jsonPath("$.rating").value(5)) .andExpect(status().isOk()); } @Test public void testDeleteCourse() throws Exception { Course course = Course.builder() .name("Rapid Spring Boot Application Development") .category("Spring") .rating(5) .description("Rapid Spring Boot Application ➥ Development").build(); ObjectMapper objectMapper = new ObjectMapper(); MockHttpServletResponse response = mockMvc.perform(post("/courses/") .contentType("application/json") .content(objectMapper.writeValueAsString(course))) .andDo(print()) .andExpect(jsonPath("$.*", hasSize(5))) .andExpect(jsonPath("$.id", greaterThan(0))) .andExpect(jsonPath("$.name").value("Rapid Spring Boot ➥ Application Development")) .andExpect(jsonPath("$.category").value("Spring")) .andExpect(jsonPath("$.rating").value(5)) .andExpect(status().isCreated()).andReturn().getResponse(); Integer id = JsonPath.parse(response.getContentAsString()).read("$.id"); mockMvc.perform(delete("/courses/{id}", id)) .andDo(print()) .andExpect(status().isOk()); }
In listing 7.15, we’ve defined three test cases:
The first test case attempts to get the course details for a course ID that is not available. The application returns an HTTP 404 status code, and we expect the same in the test case.
The second test case performs an HTTP PUT
operation to test the update course endpoint.
The last test case performs the HTTP DELETE
operation to delete a course with a courseId
.
Spring MockMVC framework provides an excellent way to test Spring MVC-based applications. Moreover, Spring Boot autoconfiguration of MockMVC has simplified defining the test cases even further. With this technique, we’ve demonstrated how to define test cases for the REST API endpoints with Spring’s MockMVC framework. The MockMVC framework provides a fluent API that allows you to perform the assertion of various response parameters. You can find further details regarding MockMVC at http://mng.bz/do5D.
Spring also provides an alternate test client called WebTestClient that lets you verify the response in a much better manner. We’ll demonstrate the use of WebTestClient in the next chapter.
As part of modern-day application development, APIs play a critical role in the success of an application. As application features are consumed by a variety of devices, it is important that APIs are documented. Further, an API represents a contract between an API provider and consumers. Therefore, a good API should ensure that the API details are available to its consumers, so consumers can develop their code accordingly. These details include the HTTP request and response structure, HTTP status code that an endpoint returns, security configurations, and various other details. You can refer to https://petstore.swagger.io/ for a quick glimpse of the documentation of the Spring Petclinic application (https://github.com/spring-projects/spring-petclinic). In this section, we’ll discuss documenting the RESTful APIs through OpenAPI (https://swagger.io/specification/), which is the most popular and de facto standard of RESTful API documentation.
In this technique, we’ll learn how to document a RESTful API.
The Course Tracker API is currently undocumented, and there are no means other than exploring the application source code to find out the details regarding the API. We need to document this API with OpenAPI, so the API consumers can find the required details about the API.
The OpenAPI Specification provides a standard approach to document RESTful APIs, so the API consumers can find out the details and capabilities of the API in a consistent manner.
The OpenAPI specification is language-agnostic, which means it is not only limited to Spring Boot, but it is available for other languages and frameworks as well. For instance, we can use OpenAPI to document the RESTful API developed through a Spring Boot application, and the same is possible for a RESTful API developed through Express JS (https://expressjs.com/).
In this section, we’ll demonstrate how to document the Course Tracker API with OpenAPI. To proceed with that, let’s first add the following Maven dependency in the pom.xml file, as shown in the following listing.
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>1.5.9</version> </dependency>
The springdoc-openapi
(https://springdoc.org/) library automates the generation of API documentation in a Spring Boot project. It does so by inspecting a Spring Boot application at runtime to infer the API semantics based on Spring configurations, class structure, and other annotations. The springdoc-openapi-ui
dependency provides integration between Spring Boot and Swagger UI. It automatically deploys the swagger-ui
to a Spring Boot application and makes it available at http://{server}:{port}/{context-path}/swagger-ui.html.
Notice that we’ve introduced Swagger in our discussion. Let’s clarify the difference between Swagger and OpenAPI. The OpenAPI is the specification that dictates the guidelines for the API documentation. Swagger is the tool that implements this specification. Swagger consists of various components, such as Swagger Editor, Swagger UI, Swagger Codegen, and a few other modules. Please refer to http://mng.bz/VlNX for a detailed discussion on Swagger vs. OpenAPI.
Let’s now proceed with documenting the Course Tracker API. To document the API, we annotate the endpoints with various annotations. These annotations contain custom details about the endpoint, such as the purpose of the endpoint, the HTTP status code it returns, and more. The following listing shows the updated CourseController
annotated with the OpenAPI annotations.
package com.manning.sbip.ch07.controller; // imports import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @RestController @RequestMapping("/courses/") @Tag(name = "Course Controller", description = "This REST controller ➥ provide services to manage courses in the Course Tracker application") public class CourseController { private CourseService courseService; @Autowired public CourseController(CourseService courseService) { this.courseService = courseService; } @GetMapping @ResponseStatus(code = HttpStatus.OK) @Operation(summary = "Provides all courses available in the Course ➥ Tracker application") public Iterable<Course> getAllCourses() { return courseService.getCourses(); } @GetMapping("{id}") @ResponseStatus(code = HttpStatus.OK) @Operation(summary = "Provides course details for the supplied course ➥ id from the Course Tracker application") public Optional<Course> getCourseById(@PathVariable("id") long courseId) { return courseService.getCourseById(courseId); } @GetMapping("category/{name}") @ResponseStatus(code = HttpStatus.OK) @Operation(summary = "Provides course details for the supplied course ➥ category from the Course Tracker application") public Iterable<Course> getCourseByCategory(@PathVariable("name") String category) { return courseService.getCoursesByCategory(category); } @PostMapping @ResponseStatus(code = HttpStatus.CREATED) @Operation(summary = "Creates a new course in the Course Tracker ➥ application") public Course createCourse(@Valid @RequestBody Course course) { return courseService.createCourse(course); } @PutMapping("{id}") @ResponseStatus(code = HttpStatus.NO_CONTENT) @Operation(summary = "Updates the course details in the Course Tracker ➥ application for the supplied course id") public void updateCourse(@PathVariable("id") long courseId, @Valid @RequestBody Course course) { courseService.updateCourse(courseId, course); } @DeleteMapping("{id}") @ResponseStatus(code = HttpStatus.NO_CONTENT) @Operation(summary = "Deletes the course details for the supplied ➥ course id from the Course Tracker application") public void deleteCourseById(@PathVariable("id") long courseId) { courseService.deleteCourseById(courseId); } @DeleteMapping @ResponseStatus(code = HttpStatus.NO_CONTENT) @Operation(summary = "Deletes all courses from the Course Tracker ➥ application") public void deleteCourses() { courseService.deleteCourses(); } }
In listing 7.17, we annotated the class with @Tag
and the endpoints with @ResponseStatus
and @Operation
annotations. The @Tag
provides information about the controller. The @ResponseStatus
indicates the HTTP status code the endpoint returns. Notice that the HTTP status code is critical for the API consumer to code their application logic, as it defines the status of the API call. Thus, we must take care while determining the HTTP Status code for the endpoints. Lastly, the @Operation
annotation captures details regarding the purpose of the endpoint.
Let’s now capture a few custom details about the API, such as API version, title, description, license details, and more. You can do this by defining a Spring bean of type OpenAPI. Listing 7.18 shows the OpenAPI bean definition. For simplicity, we’ve defined this bean in the Spring Boot main class, as shown in the following listing. In a typical application, you should define a separate Spring configuration class that should contain this @Bean
definition.
package com.manning.sbip.ch07; //imports import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; @SpringBootApplication public class CourseTrackerApiApplication { public static void main(String[] args) { SpringApplication.run(CourseTrackerApiApplication.class, args); } @Bean public OpenAPI customOpenAPI(@Value("${app.description}") String ➥ appDescription, @Value("${app.version}") String appVersion) { return new OpenAPI().info(new Info().title("Course Tracker ➥ API").version(appVersion) .description(appDescription).termsOfService("http:/ /swagger.io/terms/") .license(new License().name("Apache ➥ 2.0").url("http:/ /springdoc.org"))); } }
In listing 7.18, we defined the OpenAPI bean, which contains custom API details. In the following listing, we define the app.description
and app.version
properties in the application.properties file.
That’s all. Let’s start the application and access the swagger-ui
to view the API documentation. You can access swagger-ui
for this application at http://localhost:8080/ swagger-ui.html. Figure 7.3 shows the swagger-ui
for the Course Tracker API.
OpenAPI is the de facto choice to document RESTful APIs. As you’ve seen in the previous example, by adding a few dependencies you have a nice HTML-based API document that captures the details about the API. However, one issue with the HTML is that it is difficult to share with the API consumers. To handle this, Swagger also lets you extract the API documentation in JSON format. You can retrieve this JSON by accessing the http://localhost:8080/v3/api-docs URL. This is shown in the following listing.
{ "openapi":"3.0.1", "info":{ "title":"Course Tracker API", "description":"Spring Boot Course Tracker API", "termsOfService":"http:/ /swagger.io/terms/", "license":{ "name":"Apache 2.0", "url":"http:/ /springdoc.org" }, "version":"v1" }, "servers":[ { "url":"http:/ /localhost:8080", "description":"Generated server url" } ], "tags":[ { "name":"Course Controller", "description":"This REST controller provides services to manage ➥ courses in the Course Tracker application" } ], "paths":{ "/courses/{id}":{ "get":{ "tags":[ "Course Controller" ], // Remaining part of the JSON is omitted
Swagger provides the Swagger Editor (https://editor.swagger.io/), which allows you to import this JSON and renders the same HTML layout shown in figure 7.4.
You can ship this JSON shown in listing 7.20 with API consumers to let them render it through Swagger Editor. To make life even simpler, Swagger also provides a Codegen
utility that allows you to generate client applications from this JSON. For instance, let’s assume that the API client uses Node JS as their preferred language. You can generate this Node JS client stub with Swagger Codegen. Swagger Codegen also allows you to generate the client stub for a lot of different languages. Refer to https://swagger.io/tools/swagger-codegen/ for more details on Swagger Codegen. For further details on Spring Doc and OpenAPI integration, refer to Spring Doc reference documentation available at https://springdoc.org/.
In this section, we’ll discuss the various approaches to versioning a RESTful API. However, before proceeding with the discussion of various versioning techniques, let’s discuss REST API versioning and why it’s necessary.
In simple words, versioning a REST API means the ability for the API to support multiple versions. It is a common occurrence to enhance or upgrade the application features over time. Various factors could drive these changes. For instance, it could be the implementation of new business features, adoption of a new technology stack, or refinement of the existing APIs.
However, the issue with a breaking API change is that it directly impacts the API consumers and breaks their application. It also causes a cascading impact on the API invocation chain. One way to resolve this issue is to implement versioning while designing your APIs. This way, you may have a version that is stable and available for your API consumers. For any breaking changes, you can introduce a newer version of the API that can be progressively adopted by various consumers.
In this section, we’ll discuss the available techniques to implement API versioning. Following is the list of techniques we’ll discuss in this chapter:
Request parameter versioning—Uses an HTTP request parameter to identify the version
Custom HTTP header versioning—Uses an HTTP request header to distinguish the version
Media type versioning—Uses the accept header request header in the request to identify the version
We’ll demonstrate the different versioning techniques in the next technique. Later, we’ll provide an analysis on the merits and demerits of the approaches. To better explain the versioning techniques, we’ll simplify the CourseController
class and only use the GET/courses
/
and POST /courses/
endpoint for versioning. Let’s discuss this in the next technique.
In this technique, we’ll discuss how to implement versioning in a RESTful API.
The Course Tracker API has not implemented any versioning strategy. We need to implement a versioning technique to ensure that the API can handle any breaking changes.
In this section, we’ll first discuss the URI versioning technique. This is a straightforward approach, as it includes a version identifier in the REST URI. For instance, /courses/v1
represents version 1 of the API, and /courses/v2
represents version 2 of the API.
Let’s assume we now need to enhance Course Tracker API, and it needs to also support an additional attribute of course price
along with the previous course details. Introduction of course price
could also mean that we can have additional REST endpoints, such as finding courses between a price range or retrieving courses based on the price order.
Note For simplicity reasons and demonstration purposes we are introducing the price attribute to the Course
entity to design a new version of the API. In actual scenarios, there should be more appropriate reasons for API versioning.
To demonstrate this change, we’ll make changes to the CourseController
class in the Course Tracker application. We’ll rename the existing CourseController
class to LegacyCourseController
and keep only GET /courses
/
and POST /courses/
endpoints in it. The following listing shows the modified class.
package com.manning.sbip.ch07.controller; // imports @RestController @RequestMapping("/courses/v1") ① public class LegacyCourseController { private CourseService courseService; @Autowired public LegacyCourseController(CourseService courseService) { this.courseService = courseService; } @GetMapping @ResponseStatus(code = HttpStatus.OK) public Iterable<Course> getAllCourses() { return courseService.getCourses(); } @PostMapping @ResponseStatus(code = HttpStatus.CREATED) public Course createCourse(@Valid @RequestBody Course course) { return courseService.createCourse(course); } }
① The request mapping URL contains the version number. We’ve appended version v1 to indicate the first version of the API.
The most notable change in listing 7.21 is that we’ve updated the @RequestMapping
URI to /courses/v1
. This is now the v1
version of the API. We’ll also introduce a new RestController
called ModernCourseController
. This controller class contains the changes related to the course price
. The following listing shows the ModernCourseController
class.
package com.manning.sbip.ch07.controller; //imports @RestController @RequestMapping("/courses/v2") public class ModernCourseController { private ModernCourseRepository modernCourseRepository; @Autowired public ModernCourseController(ModernCourseRepository ➥ modernCourseRepository) { this.modernCourseRepository = modernCourseRepository; } @GetMapping @ResponseStatus(code = HttpStatus.OK) public Iterable<ModernCourse> getAllCourses() { return modernCourseRepository.findAll(); } @PostMapping @ResponseStatus(code = HttpStatus.CREATED) public ModernCourse createCourse(@Valid @RequestBody ModernCourse ➥ modernCourse) { return modernCourseRepository.save(modernCourse); } }
Listing 7.22 represents the v2
version of the API, and we have done this by defining the @RequestMapping
to /courses/v2
URI. We’ve also defined a new JPA entity class called ModernCourse
that contains the new course attribute price
along with other parameters and a new Spring Data repository interface called ModernCourseRepository
available at http://mng.bz/Ax5z. For simplicity, we have skipped the service layer in the new version of the API.
That’s it. Now, let’s start the application and access both versions of the API. Listing 7.23 shows the output of creating and accessing a course with the v1
version of the API.
>http POST :8080/courses/v1 name="Mastering Spring Boot" rating=4 ➥ category=Spring description="Mastering Spring Boot intends to teach ➥ Spring Boot with practical examples" HTTP/1.1 201 // Other HTTP Response Headers { "category": "Spring", "description": "Mastering Spring Boot intends to teach Spring Boot with ➥ practical examples", "id": 1, "name": "Mastering Spring Boot", "rating": 4 } >http GET :8080/courses/v1 HTTP/1.1 200 // Other HTTP Response Headers [ { "category": "Spring", "description": "Mastering Spring Boot intends to teach Spring Boot ➥ with practical examples", "id": 1, "name": "Mastering Spring Boot", "rating": 4 } ]
Let’s now create and retrieve courses with the v2
version of the API. The following listing shows the output.
>http POST :8080/courses/v2 name="Mastering Spring Boot" rating=4 ➥ category=Spring description="Mastering Spring Boot intends to teach ➥ Spring Boot with practical examples" price=42.34 ① HTTP/1.1 201 // Other HTTP Response Headers { "category": "Spring", "description": "Mastering Spring Boot intends to teach Spring Boot with ➥ practical examples", "id": 1, "name": "Mastering Spring Boot", "price": 42.34, "rating": 4 } >http GET :8080/courses/v2 HTTP/1.1 200 // Other HTTP Response Headers [ { "category": "Spring", "description": "Mastering Spring Boot intends to teach Spring Boot ➥ with practical examples", "id": 1, "name": "Mastering Spring Boot", "price": 42.34, "rating": 4 } ]
① Creating a new course with the new version (/courses/v2) of the course API. Notice that we’ve included a new field named price in this endpoint.
As you may have noticed, both versions of the APIs are working fine. In the v1
version of the API, there is no price parameter. In the v2
version of the API, the price parameter is shown.
Let’s now discuss the second versioning technique of using an HTTP request parameter to determine the version. We’ll use the same Course Tracker application to demonstrate this versioning type.
For the HTTP request parameter-based versioning technique, you’ll provide a request parameter in the REST endpoint URI that dictates which version of the API should be invoked. Let’s define a new RestController
class called RequestParameterVersioningCourseController
. The following listing shows the RequestParameterVersioningCourseController
class.
package com.manning.sbip.ch07.controller; //imports @RestController @RequestMapping("/courses/") public class RequestParameterVersioningCourseController { @Autowired private CourseService courseService; @Autowired private ModernCourseRepository modernCourseRepository; @GetMapping(params = "version=v1") @ResponseStatus(code = HttpStatus.OK) public Iterable<Course> getAllLegacyCourses() { return courseService.getCourses(); } @PostMapping(params = "version=v1") @ResponseStatus(code = HttpStatus.CREATED) public Course createCourse(@Valid @RequestBody Course course) { return courseService.createCourse(course); } @GetMapping(params = "version=v2") @ResponseStatus(code = HttpStatus.OK) public Iterable<ModernCourse> getAllModernCourses() { return modernCourseRepository.findAll(); } @PostMapping(params = "version=v2") @ResponseStatus(code = HttpStatus.CREATED) public ModernCourse createCourse(@Valid @RequestBody ModernCourse ➥ modernCourse) { return modernCourseRepository.save(modernCourse); } }
In listing 7.25, notice the use of version=v1
and version=v2
request parameters that determines the endpoint to be invoked. Also notice that we’ve used the CourseService
class for the v1
version of the API and ModernCourseRepository
for the v2
version of the API. Ideally, we should define a service class to wrap the functionalities of the ModernCourseRepository
interface for the version v2
API as well. For simplicity and demonstration purposes, we have skipped this step. In a real production application, you should define a service class for the controller.
You can start the application and access the new endpoints with the version=v2
parameter. The following listing shows the output.
>http POST :8080/courses/?version=v2 name="Mastering Spring Boot" rating=4 ➥ category=Spring description="Mastering Spring Boot intends to teach ➥ Spring Boot with practical examples" price=42.34 HTTP/1.1 201 // Other HTTP Response Headers { "category": "Spring", "description": "Mastering Spring Boot intends to teach Spring Boot with ➥ practical examples", "id": 1, "name": "Mastering Spring Boot", "price": 42.34, "rating": 4 } >http GET :8080/courses/?version=v2 // Other HTTP Response Headers [ { "category": "Spring", "description": "Mastering Spring Boot intends to teach Spring Boot ➥ with practical examples", "id": 1, "name": "Mastering Spring Boot", "price": 42.45, "rating": 4 } ]
In the v1
version of the API, you’ll notice that the price parameter is not available.
Let’s now discuss the third API versioning technique that uses a custom HTTP header to identify the endpoint that needs to be invoked. This is quite similar to the second technique of using the HTTP request parameter. In this case, instead of an HTTP request parameter in the URI, we use a custom HTTP header in the HTTP request. Let’s define a new class that implements this versioning strategy.
Listing 7.27 shows the CustomHeaderVersioningCourseController
class.
package com.manning.sbip.ch07.controller; // imports @RestController @RequestMapping("/courses/") public class CustomHeaderVersioningCourseController { private CourseService courseService; private ModernCourseRepository modernCourseRepository; @Autowired public CustomHeaderVersioningCourseController(CourseService ➥ courseService, ModernCourseRepository modernCourseRepository) { this.courseService = courseService; this.modernCourseRepository = modernCourseRepository; } @GetMapping(headers = "X-API-VERSION=v1") @ResponseStatus(code = HttpStatus.OK) public Iterable<Course> getAllLegacyCourses() { return courseService.getCourses(); } @PostMapping(headers = "X-API-VERSION=v1") @ResponseStatus(code = HttpStatus.CREATED) public Course createCourse(@Valid @RequestBody Course course) { return courseService.createCourse(course); } @GetMapping(headers = "X-API-VERSION=v2") @ResponseStatus(code = HttpStatus.OK) public Iterable<ModernCourse> getAllModernCourses() { return modernCourseRepository.findAll(); } @PostMapping(headers = "X-API-VERSION=v2") @ResponseStatus(code = HttpStatus.CREATED) public ModernCourse createCourse(@Valid @RequestBody ModernCourse ➥ modernCourse) { return modernCourseRepository.save(modernCourse); } }
In listing 7.27, we used a custom HTTP header X-API-VERSION
to determine the endpoint that needs to be invoked. To invoke a REST endpoint, you need to supply the X-API-VERSION
header in your HTTP request. The following listing shows the use of this custom HTTP header.
>http POST :8080/courses/ X-API-VERSION:v2 name="Mastering Spring Boot" ➥ rating=4 category=Spring description="Mastering Spring Boot intends to ➥ teach Spring Boot with practical examples" price=42.34 HTTP/1.1 201 // Other HTTP Response Headers { "category": "Spring", "description": "Mastering Spring Boot intends to teach Spring Boot with ➥ practical examples", "id": 1, "name": "Mastering Spring Boot", "price": 42.34, "rating": 4 } >http GET :8080/courses/ X-API-VERSION:v2 // Other HTTP Response Headers [ { "category": "Spring", "description": "Mastering Spring Boot intends to teach Spring Boot ➥ with practical examples", "id": 1, "name": "Mastering Spring Boot", "price": 42.34, "rating": 4 } ]
The last versioning technique we’ll discuss in this section is media-type versioning. This is also known as the Content Negotiation
or Accept Header
versioning strategy. This is due to the use of the Accept
HTTP request header. In this technique, instead of using a custom HTTP header, we leverage the built-in Accept
HTTP header. With the Accept
HTTP header, a client indicates a server the content types (through MIME types) that the client understands. In the HTTP request, the client provides the Accept
header. In the content negotiation (http://mng.bz/2jB8) phase, the server uses its internal algorithm to determine one of the Accept
header values and inform the choice with the Content-Type
response header.
Let’s define the AcceptHeaderVersioningCourseController
class that implements the versioning technique with the Accept
HTTP header. This implementation is shown in the following listing.
package com.manning.sbip.ch07.controller; //imports @RestController @RequestMapping("/courses/") public class AcceptHeaderVersioningCourseController { private CourseService courseService; private ModernCourseRepository modernCourseRepository; @Autowired public AcceptHeaderVersioningCourseController(CourseService ➥ courseService, ModernCourseRepository modernCourseRepository) { this.courseService = courseService; this.modernCourseRepository = modernCourseRepository; } @GetMapping(produces = "application/vnd.sbip.app-v1+json") @ResponseStatus(code = HttpStatus.OK) public Iterable<Course> getAllLegacyCourses() { return courseService.getCourses(); } @PostMapping(produces = "application/vnd.sbip.app-v1+json") @ResponseStatus(code = HttpStatus.CREATED) public Course createCourse(@Valid @RequestBody Course course) { return courseService.createCourse(course); } @GetMapping(produces = "application/vnd.sbip.app-v2+json") @ResponseStatus(code = HttpStatus.OK) public Iterable<ModernCourse> getAllModernCourses() { return modernCourseRepository.findAll(); } @PostMapping(produces = "application/vnd.sbip.app-v2+json") @ResponseStatus(code = HttpStatus.CREATED) public ModernCourse createCourse(@Valid @RequestBody ModernCourse ➥ modernCourse) { return modernCourseRepository.save(modernCourse); } }
In the following listing, we’ve used the produces
attribute of the @GetMapping
and @PostMapping
annotations that declares the content the endpoint produces. The application/vnd.sbip.app-v1+json
is a custom MIME type that indicates the v1
version of the API, and application/vnd.sbip.app-v2+json
specifies the v2
version of the API. The following listing shows the use of the Accept
HTTP header.
>http POST :8080/courses/ Accept:application/vnd.sbip.app-v2+json ➥ name="Mastering Spring Boot" rating=4 category=Spring ➥ description="Mastering Spring Boot intends to teach Spring Boot with ➥ practical examples" price=42.34 HTTP/1.1 201 Connection: keep-alive Content-Type: application/vnd.sbip.app-v2+json Date: Fri, 25 Jun 2021 18:42:15 GMT Keep-Alive: timeout=60 Transfer-Encoding: chunked { "category": "Spring", "description": "Mastering Spring Boot intends to teach Spring Boot with ➥ practical examples", "id": 1, "name": "Mastering Spring Boot", "price": 42.34, "rating": 4 } >http GET :8080/courses/ Accept:application/vnd.sbip.app-v2+json HTTP/1.1 200 Connection: keep-alive Content-Type: application/vnd.sbip.app-v2+json Date: Mon, 08 Nov 2021 02:39:29 GMT Keep-Alive: timeout=60 Transfer-Encoding: chunked [ { "category": "Spring", "description": "Mastering Spring Boot intends to teach Spring Boot ➥ with practical examples", "id": 1, "name": "Mastering Spring Boot", "price": 42.34, "rating": 4 } ]
With this technique, we’ve seen the various techniques to implement versioning in a REST API. Now that you have several choices to implement versioning, the immediate next question that comes to mind is which approach is better and preferable. This is a difficult question, and there is no straightforward answer to it. This is because none of the solutions we’ve discussed are perfect.
For instance, many developers reject the idea of assigning a version number in the endpoint URI, as it creates URI pollution. Since the version is not part of the actual URI, many argue the presence of the version identifier is a bad practice. Versioning in the URI exposes to the API consumers that there are multiple versions of the API that exist. Many organizations do not expose this fact to the API consumers.
Similarly, many developers reject the idea of using Accept
header for versioning purposes, as the Accept
HTTP header is not designed for this purpose. Using Accept
header for versioning is just a workaround and is not considered a preferred solution to implement versioning. A similar type of counterargument is available for the other two versioning techniques.
If there are multiple versions of the same endpoint available, it causes issues while documenting the API. For instance, the API consumers may get confused if they find two different approaches to invoke the same service.
As you may notice, there are both merits and demerits of the discussed approaches. Thus, selecting a versioning strategy is a design choice of API designers or the organizations after analyzing the pros and cons of the approach before adopting it to practice. The following list shows the API versioning approaches adopted by several major API providers:
In previous sections, we discussed various aspects of developing a RESTful API that includes developing an API, its documentation, its testing, and its versioning. We are still left with another core aspect of API development. And it’s the security of the API. Presently, this API is not secure, and anyone who knows the API endpoints can access the API.
There are several ways an API can be secured. The most straightforward approach is using HTTP basic authentication to secure the API. This is the simplest one to implement, as it uses a username and password to authenticate the users. You may remember that in chapter 5, we demonstrated how to implement HTTP basic authentication to secure a Spring Boot application. You can refer to section 5.3.6 in chapter 5 to implement HTTP basic authentication in the Course Tracker API.
However, you should limit the use of HTTP basic authentication to the extent possible due to its various shortcomings. Only consider it for your internal testing or development purposes. An attentive reader may ask why we are discussing it here if it is not recommended to use. The use of basic authentication is still widespread (http://mng.bz/PWKY) due to its simplicity and ease of use. Only recently, some organizations are deprecating the use of this authentication strategy (http://mng.bz/J1pK).
Let’s discuss the reasons we should not use it in a production application in the first place. First, HTTP basic authentication uses the username and password in plain-text format with Base64 encoding to authenticate the users. The Base64 encoding is not an encryption technique, and it is extremely easy to retrieve the credentials from a Based64 encoded string. Thus, without HTTPS, there is a high chance credentials could be exposed. Second, with HTTP basic authentication technique, both the client application and the server application act as the password keeper and manage the user credentials for authentication and authorization purposes. This is again problematic, as there are chances that credentials could be compromised by either party.
A preferred approach would be managing the user credentials in a centralized authorization server instead of allowing either the server or the client application to deal with the user password. The authorization server can issue a token that could be used for authentication and authorization purposes. Let’s discuss this approach in the next technique.
In this technique, we will discuss how to authorize RESTful API requests using JWT.
The Course Tracker RESTful API has not implemented any security measures that can secure the REST endpoints. Without security configurations, anyone can access the application endpoints.
With this technique, we’ll demonstrate how to secure the endpoint access with the Bearer Token approach. As mentioned previously, we’ll use an authorization server for authorizing access. However, before proceeding with the implementation, let’s provide a high-level overview of the REST request and response flow between the client, REST API server, and the authorization server. Figure 7.5 shows a block diagram of this flow:
Let’s understand the flow discussed in the figure:
A client requests to get course details from the Course Tracker REST API by invoking the GET /courses
endpoint.
As the client is not authenticated, the API responds with 401 Unauthorized and indicates in the HTTP response header that it needs to authenticate itself with a Bearer Token.
The client then requests the authorization server to get a Bearer Token. While making this request, the client supplied the required details, such as client_id
, username
, password
, scope
, and others. Note that the user for which the Bearer Token is requested needs to be configured before a token is requested.
For a valid token request, the authorization server returns an access_token
in JSON Web Token (JWT) format.
The client application makes a new request to the Course Tracker REST API and supplies the Bearer token in the request.
The Course Tracker REST API validates the token with the authorization server and receives a response.
For a valid response, the Course Tracker REST API returns the requested course details. For an invalid response from the authorization server, it returns an error response to the client.
Note that the flow in figure 7.5 is for a new request if the API client attempts to access the endpoint without supplying the JWT token. If the client is supplying the token, the communication starts at step 5.
Let’s now begin with the implementation. The first thing that needs to be done is to configure the authorization server. We’ll use Keycloak (https://www.keycloak.org/) as the authorization server. We’ll configure two users, namely john
and steve
, in the authorization server. You can refer to the following GitHub wiki http://mng.bz/q27J to set up the authorization server. It is strongly recommended that you set up the authorization server before continuing with the next steps.
To keep the example simple, we’ve simplified the Course Tracker application a bit. The course domain entity now contains only three fields: a course ID, a name, and an author. The following listing shows the updated course class.
package com.manning.sbip.ch07.model; // imports @Entity @Data @NoArgsConstructor @AllArgsConstructor public class Course { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ID") private Long id; @NotEmpty @Column(name = "NAME") private String name; @NotEmpty @Column(name = "AUTHOR") private String author; }
We’ve also simplified the CourseController
class, and it has the following endpoints:
To enable JSON Web Token (JWT) support, we need to update the pom.xml with the dependencies shown in the following listing.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency>
The first dependency makes the Course Tracker application an OAuth2 resource server. The second dependency provides support for JWT (https://jwt.io/introduction). Let’s now include the property in the application.properties file shown in the following listing.
Listing 7.33 configures the Keycloak JWT issuer URL. Let’s now explore the updated CourseController
class, as shown in the following listing.
package com.manning.sbip.ch07.controller; //imports @RestController @RequestMapping("/courses/") public class CourseController { private CourseRepository courseRepository; @Autowired public CourseController(CourseRepository courseRepository) { this.courseRepository = courseRepository; } @GetMapping public Iterable<Course> getAllCourses(@AuthenticationPrincipal Jwt ➥ jwt) { ① String author = jwt.getClaim("user_name"); return courseRepository.findByAuthor(author); } @GetMapping("{id}") public Optional<Course> getCourseById(@PathVariable("id") long courseId) { return courseRepository.findById(courseId); } @PostMapping public Course createCourse(@RequestBody String name, ➥ @AuthenticationPrincipal Jwt jwt) { Course course = new Course(null, name, jwt.getClaim("user_name")); return courseRepository.save(course); } }
① The user_name is a custom claim defined in the authorization server. In this context, we use it to get the author name to look up the courses authored by a user.
In listing 7.34, we used the @AuthenticationPrincipal
annotation to get access to the JWT token. This JWT instance contains the various details about the user request. From the JWT, we retrieve the user_name
claim, which is the course author name in this context. Let’s now create two courses: one for the author john
and another for author steve
, as shown in the following listing.
@Bean CommandLineRunner createCourse(CourseRepository courseRepository) { return (args) -> { Course spring = new Course(null, "Spring", "john"); Course python = new Course(null, "Python", "steve"); courseRepository.save(spring); courseRepository.save(python); }; }
That’s all. Let’s now start the application and try accessing the endpoints. Listing 7.36 shows the outcome while we try to access the GET /courses/
endpoint without supplying a JWT token.
The request is denied with an HTTP 401 unauthorized error response. The API has also responded with the WWW-Authenticate:Bearer
response header indicating the client needs to provide a Bearer Token in the HTTP request. This is automatically done by Spring Security. As we are using Bearer Token-based authentication, Spring Security uses the BearerTokenAuthenticationFilter
to process the incoming request. It attempts to parse the request and generates a JwtAuthenticationToken
, which contains the JWT token details. In the discussion section, we’ll provide more details on the classes used to process the request. For now, remember that the BearerTokenAuthenticationFilter
is the Spring Security filter that performs the authentication. Let’s now try to obtain a Bearer Token for the user john
, so the same can be included in the HTTP request. The following listing shows the command to obtain a token.
C:Usersmusib>http --form POST http:/ /localhost:9999/auth/realms/master/protocol/openid-connect/token ➥ grant_type=password client_id=course-tracker scope=course:read ➥ username=john password=password Content-Type:application/x-www-form- ➥ urlencoded HTTP/1.1 200 OK // HTTP Response Headers { "access_token": ➥ "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJxY2lKalIxSWNocTk4Qk ➥ VMcEo5cDJiWDBRaF80MzZ1S0ktbkx4UlF3Zk53In0.eyJleHAiOjE2MjQ3NzczOTgsImlhd ➥ CI6MTYyNDc3Mzc5OCwianRpIjoiYTY4OWM0Y2ItYTVhZC00YTM5LWE1YjQtNjFjNGNhNGZk ➥ MjMzIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5OTk5L2F1dGgvcmVhbG1zL21hc3RlciI ➥ sImF1ZCI6ImNvdXJzZS10cmFja2VyIiwic3ViIjoiNmQxMTE4MTktZmFlZC00NzQzLWFiNT ➥ EtMzk0YmVmNGQ0ZjBlIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiY291cnNlLXRyYWNrZXIiL ➥ CJzZXNzaW9uX3N0YXRlIjoiOWIyMTdiOTUtOWM1MS00ZGY0LWI3NTYtYTI3NzdmNmI0MDk2 ➥ IiwiYWNyIjoiMSIsInNjb3BlIjoiY291cnNlOndyaXRlIGNvdXJzZTpyZWFkIiwidXNlcl9 ➥ uYW1lIjoiam9obiIsImF1dGhvcml0aWVzIjpbInVzZXIiXX0.NgBcrpPvDB36sd2ytaeMUk ➥ qM_1_psUDMsHHkB9zZlT_9sIwF3kdPOhSLSmoMqhFtGpOOJI5CmB92WEBu4rVcNa2lnuh16 ➥ lkksnC-0ASn23z8TIRtucrQ- ➥ Px2bOgFyducmRH7ec93gOsLKeZSUnjup0YA9FT_0o7eroKFdWrrqoyOiAxOua9nGg307Lkv ➥ _VKXtCB5wSrPFfPQrp6muw-gcREJaBgcYSx- ➥ 5QKC5UK30cFSsWlKXC9i2ov2O3aPA4DlHIqWx06a_M7AKmvgG3fVpyJSztbi0XHDnU9Y_mJ ➥ Vug-WH5MOIpgRUmYYnSL1Ki3PV24tZ11LolyA13XsA859vg", "expires_in": 3600, "not-before-policy": 0, "refresh_expires_in": 1800, "refresh_token": ➥ "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyYzI4MTNiNy05NmIzLT ➥ RkMzctYmUwOS1lMTE0ZTkzZjJlNTcifQ.eyJleHAiOjE2MjQ3NzU1OTgsImlhdCI6MTYyND ➥ c3Mzc5OCwianRpIjoiMTU4Y2E1ZGQtMDMyNy00NTE4LTk4NWItZGQ5ZTliNzcwNjg5Iiwia ➥ XNzIjoiaHR0cDovL2xvY2FsaG9zdDo5OTk5L2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6 ➥ Imh0dHA6Ly9sb2NhbGhvc3Q6OTk5OS9hdXRoL3JlYWxtcy9tYXN0ZXIiLCJzdWIiOiI2ZDE ➥ xMTgxOS1mYWVkLTQ3NDMtYWI1MS0zOTRiZWY0ZDRmMGUiLCJ0eXAiOiJSZWZyZXNoIiwiYX ➥ pwIjoiY291cnNlLXRyYWNrZXIiLCJzZXNzaW9uX3N0YXRlIjoiOWIyMTdiOTUtOWM1MS00Z ➥ GY0LWI3NTYtYTI3NzdmNmI0MDk2Iiwic2NvcGUiOiJjb3Vyc2U6d3JpdGUgY291cnNlOnJl ➥ YWQifQ.a1O4SuspoN5u_RvYdXZsb6WLC3INx1smroEIVdYWG_E", "scope": "course:write course:read", "session_state": "9b217b95-9c51-4df4-b756-a2777f6b4096", "token_type": "Bearer" }
In listing 7.37, we used the Keycloak authorization server’s token endpoint with the required parameters. If you recall, we’ve configured all the attributes in the command while setting up and configuring the client application and the users in the Keycloak server. Revisit the GitHub wiki link to understand the purpose of these parameters. Let’s explain the various request parameters we’ve used to access the token details:
We have used x-www-form-urlencoded
as the content type, since the Keycloak server understands this request.
The grant_type
refers to how an application gets an access token. The grant_ type=password
tells the token endpoint that the application is using the Password
grant type.
A client_id
is generated in the authorization server once an application is registered in the server.
Scope refers to one or more space-separated strings indicating which permission the application is requesting. In this case, the scope value we are requesting is course:read
.
The username
and password
fields supply the username and password of the user.
In the HTTP response, the Keycloak server returns the access_token
and the client scopes configured for the user
and token_type
. For now, we’ll use the access_token
from this response to include this token in the HTTP GET request to the Course Tracker API. Note that we’ve configured the access token to be valid for one hour. Typically, in a production application, tokens are configured to be short-lived for security reasons. For simplicity and testing purposes, we’ve configured the token to be valid for one hour. In your testing, you should generate a new token and should not use the token provided in listing 7.37. The following listing shows the HTTP GET /courses/
request with the token.
C:Usersmusib>http GET :8080/courses/ "Authorization:Bearer ➥ eyJhbGciOiJSUzI1NiIsInR5cCIgOiAi..." ① HTTP/1.1 200 // HTTP Response Headers [ { "author": "john", "id": 1, "name": "Spring" } ]
① For brevity and readability purposes, we’ve elided the complete token details.
This time the HTTP status code is 200 OK
, and we can retrieve the courses authored by user john
.
Although this approach works well, there is a flaw in the implementation. With the current security implementation, we can use the token of one user to get details of the other users. For instance, in this case, we can use the token of john
to access the courses authored by steve
, as shown in the following lisitng.
>http GET :8080/courses/2 "Authorization:Bearer ➥ eyJhbGciOiJSUzI1NiIsInR5cCIgOiAi..." HTTP/1.1 200 // HTTP Response Headers { "author": "steve", "id": 2, "name": "Python" }
Ouch! We can access author Steve’s course details (which is course ID 2) with the token of author John. This is an access control issue in the application known as the insecure direct object reference (IDOR) problem (see http://mng.bz/7WBe).
This problem occurred because the token for user john
is a valid token, and the endpoint GET /courses/{id}
is not performing any access control check. To avoid this issue, we’ll implement method level security with Spring Security. Simply put, the method level security allows you to secure the methods. We’ll leverage the Spring Security @PreAuthorize
or @PostAuthorize
annotations to implement this. These annotations take Spring Expression Language (SpEL) expression, which is evaluated to make the access control decisions.
Let’s demonstrate the use of the @PostAuthorize
annotation to prevent the Insecure Direct Object Reference
problem. The access problem happened because there were no checks for whether the supplied token belongs to the author requesting access to the course details performed at the endpoint (with the supplied course ID). We can retrieve the author name (using the user_name
claim) from the token and compare it with the returned course author name. If there is a mismatch, then we’ll forbid this access.
To use the method level security, you need to include the @EnableGlobalMethodSecurity(prePostEnabled = true)
in the Spring Boot main class. This annotation enabled the method level security in the application, as shown in the following listing.
package com.manning.sbip.ch07; import ➥ org.springframework.security.config.annotation.method.configuration.Ena ➥ bleGlobalMethodSecurity; //Other imports @SpringBootApplication @EnableGlobalMethodSecurity(prePostEnabled = true) public class CourseTrackerApiApplication { public static void main(String[] args) { SpringApplication.run(CourseTrackerApiApplication.class, args); } }
Next, you need to include the @PostAuthorize
annotation on the offending endpoint. The following listing shows the updated endpoint.
@GetMapping("{id}") @PostAuthorize("@getAuthor.apply(returnObject, ➥ principal.claims['user_name'])") public Optional<Course> getCourseById(@PathVariable("id") long courseId) { return courseRepository.findById(courseId); }
We supplied two attributes to a BiFunction
implementation that performs the comparison of the token-supplied author name and the method-returned author name and returns a Boolean value. We’ve supplied the SpEL expression @getAuthor .apply(returnObject, principal.claims['user_name'])
to perform the access control. The returnObject
is the method return object, which is Optional<Course>
, and the principal.claims['user_name'])
provides the author name. Listing 7.42 shows this BiFunction
implementation as a bean definition in the Spring Boot main class. For simplicity, we’ve included this @Bean
definition in the Spring Boot main class. In a real application, define a Spring configuration class to define this bean.
@Bean BiFunction<Optional<Course>, String, Boolean> getAuthor() { return (course, userId) -> course.filter(c -> ➥ c.getAuthor().equals(userId)).isPresent(); }
Let’s again try accessing course ID 2 with the access token of author john
. The following listing shows the outcome.
C:Usersmusib>http GET :8080/courses/2 "Authorization:Bearer ➥ eyJhbGciOiJSUzI1NiIsInR5cCIgOiAi.." HTTP/1.1 403 // HTTP Response Headers
We ended up with the 403 Forbidden HTTP status code. The 403 HTTP return code indicates that the requested user was successfully authenticated to the application but failed in the authorization while accessing the endpoint.
The next thing we’ll discuss in this technique is the use of a scope to perform access control in the application. For instance, we can use a scope called course:read
to ensure that tokens with this scope can access an endpoint.
A scope defines the access level provided in the token to a client application by a user. Imagine, you (as the user) have granted access to a third-party client application to read all the courses authored by you, but you want to restrict that the client application should not be able to perform any write operation. Thus, you can grant (through grant_type=password
) the third-party client application to obtain a token (by accessing the Keycloak server) only with the course:read
scope. If the application attempts to perform a write operation for any reason, it will receive a 403 Forbidden error, as the write operation requires a different scope (e.g. course:write
), which is not provided while granting the token.
We’ll use the @PreAuthorize
annotation to implement this. Let’s add the following annotation in the getCourseById(..)
method to the CourseController
class, as shown in the following listing.
@GetMapping("{id}") @PreAuthorize("hasAuthority('SCOPE_course:read')") @PostAuthorize("@getAuthor.apply(returnObject, ➥ principal.claims['user_name'])") public Optional<Course> getCourseById(@PathVariable("id") long courseId) { return courseRepository.findById(courseId); }
Spring Security appends the SCOPE_
prefix in the scope. Thus, we’ve configured the course:read
scope as SCOPE_course:read
. The @PreAuthorize
annotation checks whether the requester (the client application) has the defined scope and, based on that, decides the access. We leave it as an exercise to the reader to play around with the Keycloak server to configure various scopes and explore the access control outcomes.
In this technique, you’ve explored using JWT with an authorization server to secure REST endpoints. Explaining the OAuth2 and the authorization server in depth is beyond the scope of this text. You can refer to books dedicated to OAuth2 (https://www.manning.com/books/oauth-2-in-action), OpenID connect (https://www.manning.com/books/openid-connect-in-action), and Spring Security (https://www.manning.com/books/spring-security-in-action) for a better understanding of these subjects.
In chapter 5, we demonstrated the use of Spring Security to secure Spring Boot applications. We also discussed that Spring Security uses a FilterChain
and a list of filters that enforces security in the application. For Bearer Token-based authentication, Spring Security provides BearerTokenAuthenticationFilter
. Figure 7.6 shows the flow of how the JWT is processed and a final JwtAuthenticationToken
is generated.
The BearerTokenAuthenticationFilter
delegates the JWT processing to an AuthenticationManager
to perform the authentication. The AuthenticationManager
uses JwtAuthenticationProvider
to perform the actual authentication task. It uses a JwtDecoder
and JwtAuthenticationConverter
that process the request and generate the JwtAuthenticationToken
.
Let’s summarize the key takeaways of this chapter:
We developed a RESTful API with Spring Boot application and discussed a few best practices for developing an API.
We explored how to perform exception handling and provide appropriate HTTP response codes.
We explored various techniques to implement versioning in a REST API. The techniques we discussed are URI versioning, request parameters, custom headers, and Accept
header-based versioning.
We implemented Bearer Token-based authentication and authorization techniques to secure the REST API.
3.144.230.82