This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This repo is my first attempt to learn SpringBoot
following this tutorial. The setup is Visual Code IDE alongside with SpringBoot plugin. It is advised to use Obsidian to read this wiki.
Spring Initializr
In VsCode press cmd+shif+p
and type Spring Initilizr
. Choose:
- Gradle Project
- SpringBoot version: latest (3.1.1)
- project language: Java
- group id: djmil
- artifact id (aka project name): cashcard
- packaging type: jar
- java version: 17
- dependencies:
- SpringWeb
Essentially, this will generate a default minimalistic jet functional SpringBoot project. Entry point, aka main()
can be found in src/main/java/djmil/cashcard/CashcardApplication.java. To run the application - press ctrl+F5
or Play button in the top right corner of an editor.
TDD
Software development teams love to move fast. So how do you go fast forever? By continuously improving and simplifying your code – this is called refactoring. Refactoring is the act of altering the implementation of a software system without altering its inputs, outputs, or behavior. One of the only ways you can safely refactor is when you have a trustworthy test suite. Which brings us to the TDD Cycle:
- Red: Write a failing test for the desired functionality.
- Green: Implement the simplest thing that can work to make the test pass.
- Refactor: Look for opportunities to simplify, reduce duplication, or otherwise improve the code without changing any behavior - to refactor.
- Repeat!
Different tests can be written at different levels of the system. At each level, there is a balance between the speed of execution, the “cost” to maintain the test, and the confidence it brings to system correctness. This hierarchy is often represented as a “testing pyramid”.
Unit Tests
UnitTests exercises a small “unit” of the system that is isolated from the rest of the system. They should be simple and speedy. You want a high ratio of Unit Tests in your testing pyramid as they’re key to designing highly cohesive, loosely coupled software.
Integration Tests
IntegrationTests exercise a subset of the system and may exercise groups of units in one test. They are more complicated to write and maintain, and run slower than unit tests.
End-to-End Tests
An End-to-End Test exercises the system using the same interface that a user would, such as a web browser. While extremely thorough, End-to-End Tests can be very slow and fragile because they use simulated user interactions in potentially complicated UIs. Implement the smallest number of these tests.
RESTful API
REST is not exactly a standard; it’s merely a way to use HTTP to perform data operations. REST contains a number of guidelines, which developers shall follow to create coherent web-application.
In a RESTful system, data objects are called Resource Representations. The purpose of a RESTful API is to manage the state of these Resources. The chart below shows details about RESTful CRUD operations of an application.
Operation | API Endpoint | HTTP Method | Response Status |
---|---|---|---|
Create | /cashcards |
POST |
201 (CREATED) |
Read | /cashcards/{id} |
GET |
200 (OK) |
Update | /cashcards/{id} |
PUT |
204 (NO DATA) |
Delete | /cashcards/{id} |
DELETE |
204 (NO DATA) |
Another common concept associated with REST is the Hypertext Transfer Protocol. In HTTP, a caller sends a Request to a URI. A web server receives the request, and routes it to a request handler. The handler creates a Response, which is then sent back to the caller.
REST in Spring Boot
One of the main things Spring does is to configure and instantiate objects. These objects are called Spring Beans, and are usually created by Spring (as opposed to using the Java new
keyword). You can direct Spring to create Beans in several ways.
We will annotate a class with a
@RestController
Spring Annotation, which directs Spring to create an instance of the class during Spring’s Component Scan phase. This happens at application startup. The Bean is stored in Spring’s IoC Container. From here, the bean can be injected into any code that requests it.
@RestController
In Spring Web, Requests are handled by Controllers. We are going to use the more specific RestController
annotation. The actual class shall be placed in src/main/java/djmil/cashcard/CashCardController.java
@RestController
public class CashCardController {
}
That’s all it takes to tell Spring: “create a REST Controller”. The Controller gets injected into Spring Web, which routes API requests (handled by the Controller) with help of Get#@GetMapping annotation to the correct method.
Get
In Get requests, the body is empty. So, the request to read the Cash Card with an id of 123 would be:
Request:
Method: GET
URL: http://cashcard.example.com/cashcards/123
Body: (empty)
The response to a successful Read request has a body containing the JSON representation of the Resource which was requested, with a Response Status Code of 200 (OK). So the response to the above Read request would look like this:
Response:
Status Code: 200
Body:
{
"id": 123,
"amount": 25.00
}
Create
Our REST API can now Get Cash Cards with a specific ID. Now it's time to add the Create endpoint to the API. Four questions we’ll need to answer while doing this are:
- Who specifies the ID - the client, or the server?
Here we’ll choose to let the server create the ID. Why? Because it’s the simplest solution, and databases are efficient at managing unique IDs. However, for completeness, let’s discuss our alternatives:
- We could require the client to provide the ID. This might make sense if there were a pre-existing unique ID, but that’s not the case.
- We could allow the client to provide the ID optionally (and create it on the server if the client does not supply it). However, we don’t have a requirement to do this, and it would complicate our application. If you think you might want to do this “just in case”, the Yagni article might dissuade you.
- In the API Request, how do we represent the object to be created?
- Which HTTP method should we use in the Request?
- What does the API send as a Response?
Idempotence and HTTP
An idempotent operation is defined as one which, if performed more than once, results in the same outcome. In a REST API, an idempotent operation is one that even if it were to be performed several times, the resulting data on the server would be the same as if it had been performed only once.
For each method, the HTTP standard specifies whether it is idempotent or not. GET
, PUT
, and DELETE
are idempotent, whereas POST
and PATCH
are not.
Since we’ve decided that the server will create IDs for every Create operation, the Create operation in our API is NOT idempotent. Since the server will create a new ID (on every Create request), if you call Create twice - even with the same content - you’ll end up with two different objects with the same content, but with different IDs. To summarize: every Create request will generate a new ID, thus no idempotency.
This leaves us with the POST
and PATCH
options. As it turns out, REST permits POST
as one of the proper methods to use for Create operations, so we'll use it. We’ll revisit PATCH
in a later lesson.
The Post Request
We want our Cash Card API to behave as semantically correctly as possible. Meaning, users of our API should not be surprised by how it behaves. We'll begin by writing a failing test of what we expect success to look like.
Let's refer to the official Request for Comments for HTTP Semantics and Content (RFC 7231) for guidance as to how our API should behave. For our POST
endpoint, review this section about HTTP POST; note that we have added emphasis:
If one or more resources has been created on the origin server as a result of successfully processing a POST request, the origin server SHOULD send a 201 (Created) response containing a Location header field that provides an identifier for the primary resource created ...
The POST
method allows a Body, so we will use the Body to send a JSON representation of the object in the Request:
- Method:
POST
- URI:
/cashcards/
- Body:
{ amount: 123.45 }
In contrast, if you recall from a previous lesson, the GET
operation includes the ID of the Cash Card in the URI and but not in the request Body.
So why is there no ID in the Request? Because we decided to allow the server to create the ID. Thus, the data contract for the Read operation is different from that of the Create operation.
The Response
On successful creation, what HTTP Response Status Code should be sent? We could use 200 OK
(the response that Read returns), but there’s a more specific, more accurate code for REST APIs: 201 CREATED
.
The fact that CREATED
is the name of the code makes it seem intuitively appropriate, but there’s another, more technical reason to use it: A response code of 200 OK
does not answer the question “Was there any change to the server data?”. By returning the 201 CREATED
status, the API is specifically communicating that data was added to the data store on the server.
In addition to Status Code and a Body, HTTP Response also contains Headers. Headers have a name and a value. The HTTP standard specifies that the Location
header in a 201 CREATED
response should contain the URI of the created resource. This is handy because it allows the caller to easily fetch the new resource using the GET endpoint (the one we implemented prior).
Here is the complete Response:
- Status Code:
201 CREATED
- Header:
Location=/cashcards/42
Spring Web Convenience Methods
Spring Web provides methods which are geared towards the recommended use of HTTP and REST. For example, we’ll use the ResponseEntity.created(uriOfCashCard)
method to create the above response. This method requires you to specify the location, ensures the Location URI is well-formed (by using the URI
class), adds the Location
header, and sets the Status Code for you. And by doing so, this saves us from using more verbose methods. For example, the following two code snippets are equivalent (as long as uriOfCashCard
is not null
):
return ResponseEntity
.created(uriOfCashCard)
.build();
Versus:
return ResponseEntity
.status(HttpStatus.CREATED)
.header(HttpHeaders.LOCATION, uriOfCashCard.toASCIIString())
.build();
Aren’t you glad Spring Web provides the .created()
convenience method? Check the complete Post implementation.
Update
Our new Update endpoint:
- will use the Put verb.
- accepts a Cash Card, and replaces the existing Cash Card with it.
- on success, will return
204 NO CONTENT
with an empty body. - will return a
404 NOT FOUND
for an unauthorized update, as well as attempts to update nonexistent IDs.
Continue reading to learn why in our case PUT
won’t support creating a Cash Card.
Post, Put or Patch?
A next logical step is the ability to adjust a card balance! How to implement the Update operation in a RESTful API is somewhat of a hot topic, which is what we’ll tackle here. When we say “adjust the balance” on a Cash Card, what we really mean is to update the amount
on an existing database record. Doing so will entail:
- Creating a new endpoint to receive an HTTP request with a verb, URI, and body
- Returning an appropriate response from the endpoint for success and error conditions
We’re already familiar with the HTTP POST
verb, which we used for the Create endpoint. Now let's talk about HTTP PUT
and PATCH
, and how all three of these are related.
PUT and PATCH
Both PUT
and PATCH
can be used for updating, but they work in different ways. Essentially, PUT
means “create or replace the complete record”, whereas PATCH
means “update only some fields of the existing record” - in other words, a partial update.
Why would you want to do a partial update? Partial updates free the client from having to load the entire record and then transmit the entire record back to the server. If the record is large enough, this can have a non-trivial impact on performance.
For our application, we’ll choose to not implement a partial update.
PUT and POST
The HTTP standard does not specify whether the POST
or PUT
verb is preferred for a Home#Create operation! This is relevant because we’ll use the PUT
verb for our Update endpoint, so we need to decide whether our API will support using PUT
to either Create or Update a resource.
There are different ways to look at the relationship between the Create and Update operations and how they are implemented in REST using HTTP verbs. The important takeaway from the following sections is not to memorize all the details, but simply to realise that there are lots of different choices, and that you, as Cash Card API author, are consciously making decisions about how to implement REST.
Surrogate and Natural Keys
Why would we want to use a PUT
operation to create a resource? This has to do with the HTTP definition of the two verbs. The difference is subtle. Let’s explain it by comparing two different systems: Our Cash Card API, and another API that we'll introduce for explanatory purposes, called the Invoice API. The Invoice API accepts the Invoice Number as the unique identifier. This is an example of using a Natural Key (supplied by the client to the API) instead of a Surrogate Key (usually generated by the server, which is what we are doing in our Cash Card API).
The important difference is whether the URI (which includes the ID of the resource) needs to be generated by the server or not. Here is how PUT and POST think about it:
If you need the server to return the URI of the created resource (or the data you use to construct the URI), then you must use
POST
.
This is the case for our Cash Card API: To create a Cash Card, we provide the POST /cashcards
endpoint. The actual URI for the created Cash Card depends on the generated ID, and is provided by the server, for example, /cashcards/101
if the ID of the created card is 101.
Alternatively, when the resource URI is known at creation time (as is the case in our example Invoice API), you can use
PUT
.
For the Invoice API, we could write a Create endpoint that accepts requests such as PUT /invoice/1234-567
. The corresponding Read call would use the exact same URI: GET /invoice/1234-567
.
Resources and Sub-Resources
Another way to look at the difference is in terms of URIs and collections of sub-resources. This is the language used by the HTTP documentation, so it's good to be familiar with it. Following the above examples, we’d find:
POST
creates a sub-resource (child resource) under (after), or within the request URI. This is what the Cash Card API does: The client calls the Create endpoint atPOST /cashcards
, but the actual URI of the created resource contains a generated ID at the end:/cashcards/101
PUT
creates or replaces (updates) a resource at a specific request URI. For the/invoice
example above, the Create endpoint would bePUT /invoice/1234-567
, and the URI of the created resource would be the same as the URI sent in thePUT
request.
Response Body and Status Code
Related to deciding whether to allow PUT
to create objects, you need to decide what the response status code and body should be. Two different options are:
Return
201 CREATED
(if you created the object), or200 OK
(if you replaced an existing object). In this case it's recommended to return the object in the response body. This is useful if data was added to the object by the server (for example, if the server records the creation date).
or
Return
204 NO CONTENT
, and an empty response body. The rationale in this case is that since aPUT
simply places on object at the URI in the request, the client doesn't need any information back - it knows that the object in the request has been saved, verbatim, on the server.
Summary
HTTP Method | Operation | Definition of Resource URI | What does it do? | Response Status Code | Response Body |
---|---|---|---|---|---|
POST |
Create | Server generates and returns the URI | Creates a sub-resource ("under" or "within" the passed URI) | 201 CREATED |
The created resource |
PUT |
Create | Client supplies the URI | Creates a resource (at the Request URI) | 201 CREATED |
The created resource |
PUT |
Update | Client supplies the URI | Replaces the resource: The entire record is replaced by the object in the Request | 204 NO CONTENT |
(empty) |
PATCH |
Update | Client supplies the URI | Partial Update: modify only fields included in the request on the existing record | 200 OK |
The updated resource |
In the Cash Card API, we don’t need to allow PUT
to create resources. We also have no need to add data on the server side for an Update operation, nor do we need to allow for partial update. So, our PUT
endpoint is limited to row 3 of the above table.
The bold rows in the above table are implemented by Cash Card API. The non-bold ones are not.
Delete
By now, you should be familiar with what question we should ask first in order to implement Delete: What is the API’s data specification for the Delete endpoint? The specification includes the details of the Request and Response.
Request:
- Verb:
DELETE
- URI:
/cashcards/{id}
- Body: (empty)
Response:
- Status code:
204 NO CONTENT
- Body: (empty)
We’ll return the 204 NO CONTENT
status code for a successful delete, but there are additional cases:
Response Code | Use Case |
---|---|
204 NO CONTENT |
- The record exists, and - The Principal is authorized, and - The record was successfully deleted. |
404 NOT FOUND |
- The record does not exist (a non-existent ID was sent). |
404 NOT FOUND |
- The record does exist but the Principal is not the owner. |
Why do we return 404 for the "ID does not exist" and "not authorized to access this ID" cases? In order to not "leak" information: If the API returned different results for the two cases, then an unauthorized user would be able to discover specific IDs that they are not authorized to access.
Hard and Soft Delete
So what does it mean to delete a Cash Card from a database’s point of view? Similar to how we decided that our Update operation means ”replace the entire existing record” (as opposed to supporting partial update), we need to decide what happens to resources when they are deleted.
A simple option, called hard delete, is to delete the record from the database. With a hard delete, it’s gone forever. So what can we do if we need data that existed prior to its deletion?
An alternative is soft delete which works by marking records as "deleted" in the database (so that they are retained, but marked as deleted). For example, we can introduce an IS_DELETED
boolean or a DELETED_DATE
timestamp column and then set that value-instead of fully removing the record by deleting the database row(s). With a soft delete, we also need to change how Repositories interact with the database. For example, a repository needs to respect the “deleted” column and exclude records marked deleted from Read requests.
Audit Trail and Archiving
When working with databases, you’ll find that there’s often a requirement to keep a record of modifications to data records. For example:
- A customer service representative might need to know when a customer deleted their Cash Card.
- There may be data retention compliance regulations which require deleted data to be retained for a certain period of time.
If the Cash Card is hard-deleted then we would need to store additional data to be able to answer this question. Let’s discuss some ways to record historical information:
- Archive (move) the deleted data into a different location.
- Add audit fields to the record itself. For example, the
DELETED_DATE
column that we mentioned already. Additional audit fields can be added, for exampleDELETED_BY_USER
. Again, this is not limited to Delete operations, but Create and Update also.
APIs which implement soft delete and audit fields can return the state of the object in the response, and the 200 OK
status code. So why did we choose to use 204 instead of 200? Because the 204 NO CONTENT
status implies that there is no body in the response.
- Maintain an audit trail. The audit trail is a record of all important operations done to a record. It can contain not only Delete operations, but Create and Update as well.
The advantage of an audit trail over audit fields is that a trail records all events, whereas audit fields on the record capture only the most recent operation. An audit trail can be stored in a different database location, or even in log files.
It’s worth mentioning that a combination of several of the above strategies is possible. Here are some examples:
- We could implement soft delete, and then have a separate process which hard-deletes or archives soft-deleted records after a certain time period, like once per year.
- We could implement hard delete, and archive the deleted records.
- In any of the above cases, we could keep an audit log of which operations happened when.
Finally, observe that even the simple specification that we’ve chosen doesn’t determine whether we implement hard or soft delete. It also doesn’t determine whether we add audit fields or keep an audit trail. However, the fact that we chose to return 204 NO CONTENT
implies that soft-delete is not happening, since if it was, we’d probably choose to return 200 OK
with the record in the body.
Database
The Separation of Concerns principle states that well-designed software should be modular, with each module having distinct and separate concerns from any other module.
Up until now, our codebase only returns a hard-coded response from the Controller. This setup violates the Separation of Concerns principle by mixing the concerns of a Controller, which is an abstraction of a web interface, with the concerns of reading and writing data to a data store, such as a database. In order to solve this, we’ll use a common software architecture pattern to enforce data management separation via the Repository pattern.
A common architectural framework that divides these layers, typically by function or value, such as business, data, and presentation layers, is called Layered Architecture. Here we can think of our Repository and Controller as two layers in a Layered Architecture. The Controller is in a layer near the Client (as it receives and responds to web requests) while the Repository is in a layer near the data store (as it reads from and writes to the data store). There may be intermediate layers as well, as dictated by business needs.
The Repository is the interface between the application and the database, and provides a common abstraction for any database, making it easier to switch to a different database when needed.
Spring Data works with Spring Boot to make database integration simple.
Choosing a Database
For our database selection, we’ll use an embedded, in-memory database. “Embedded” simply means that it’s a Java library, so it can be added to the project just like any other dependency. “In-memory” means that it stores data in memory only, as opposed to persisting data permanent, durable storage.
The specific in-memory database we’ll use is H2. Fortunately, H2 is highly compatible with other relational databases, so dev-prod parity (application might behave differently when running the in-memory database than when running in production) won’t be a big issue. We’ll use H2 for convenience for local development, but want to recognize the tradeoffs.
Auto Configuration
Simply by adding Database#Spring Data and a database dependencies we are getting full database functionality. This wonderfully showcases one of the most powerful features of Spring Boot: Auto Configuration. Without Spring Boot, we’d have to configure Spring Data to speak to H2. However, because we’ve included the Spring Data dependency (and a specific data provider, H2), Spring Boot will automatically configure your application to communicate with H2.
Spring Data’s CrudRepository
For our Repository selection, we’ll use a specific type of Repository: Spring Data’s CrudRepository
. At first glance, it’s slightly magical, but let’s unpack that magic.
The following is a complete implementation of all CRUD operations by extending CrudRepository
:
public interface CashCardRepository extends CrudRepository<CashCard, Long> {
}
With just the above code, a caller can call any number of predefined CrudRepository
methods, such as findById
:
cashCard = cashCardRepository.findById(99);
You might immediately wonder: where is the implementation of the CashCardRepository.findById()
method? CrudRepository
and everything it inherits from is an Interface with no actual code! Well, based on the specific Spring Data framework used, which for us will be Spring Data JDBC, Spring Data takes care of this implementation for us during the IoC container startup time. The Spring runtime will then expose the repository as yet another bean that you can reference wherever needed in your application.
As we’ve learned, there are typically trade-offs. For example the CrudRepository
generates SQL statements to read and write your data, which is useful for many cases, but sometimes you need to write your own custom SQL statements for specific use cases. But for now, we’re happy to take advantage of its convenient, out-of-the-box methods.
Pagination
We can expect each of our Family Cash Card users to have a few cards, thus we have to implement the “Read Many” endpoint.
[
{
"id": 1,
"amount": 123.45
},
{
"id": 2,
"amount": 50.00
}
]
It turns out that CrudRepository
has a findAll
method that can be used to easily fetch all the Cash Cards in the database.
@GetMapping()
public ResponseEntity<Iterable<CashCard>> findAll() {
return ResponseEntity.ok(cashCardRepository.findAll());
}
However, it turns out there’s a lot more to this operation than just returning all the Cash Cards in the database. Some questions come to mind:
- How do I return only the Cash Cards that the user owns? This question discussed in the Home#Spring Security section.
- What if there are hundreds (or thousands?!) of Cash Cards? Should the API return an unlimited number of results or return them in “chunks”? This is known as Pagination.
- Should the Cash Cards be returned in a particular order i.e. somehow sorted?
In order to ensure that an API response doesn’t include an astronomically large number of Cash Cards, let’s utilize Spring Data’s pagination functionality. Pagination in Spring (and many other frameworks) is to specify the page length (e.g. 10 items), and the page index (starting with 0). For example, if a user has 25 Cash Cards, and you elect to request the second page where each page has 10 items, you would request a page of size 10, and page index of 1.
Sorting
There are a few good reasons to opt for ordering (aka sorting paged response) by a specific field:
- Minimize cognitive overhead: Other developers (not to mention users) will probably appreciate a thoughtful ordering when developing it.
- Minimize future errors: What happens when a new version of Spring, or Java, or the database, suddenly causes the “random” order to change overnight?
Thankfully, Spring Data provides the PageRequest
and Sort
classes for pagination. Let’s look at a query to get page 2 with page size 10, sorting by amount in descending order (largest amounts first):
Page<CashCard> page2 = cashCardRepository.findAll(
PageRequest.of(
1, // page index for the second page - indexing starts at 0
10, // page size (the last page might have fewer items)
Sort.by(new Sort.Order(Sort.Direction.DESC, "amount"))));
For this code to work, it is important for our repository implementation to support pagination. src\main\java\djmil\cashcard\CashCardRepository.java
:
import org.springframework.data.repository.PagingAndSortingRepository;
public interface
CashCardRepository
extends
CrudRepository<CashCard, Long>,
PagingAndSortingRepository<CashCard, Long> {
}
But how do we get query configuration data for sorting?
The URL
The HTTP request parameters is used to transfer values that is used to configure pagination query: (we've omitted the https://domain
prefix in the following)
- Get the second page /cashcards**?page=1**
- …where a page has length of 3 /cashcards?page=1**&size=3**
- …sorted by the current Cash Card balance /cashcards?page=1&size=3**&sort=amount**
- …in descending order (highest balance first) /cashcards?page=1&size=3&sort=amount**,desc**
Security
Detailed implementation of features provided by Spring Security can be seen in Security page.
Authentication
A user of an API can actually be a person or another program, so often we’ll use the term Principal as a synonym for “user”. Authentication is the act of a Principal proving its identity to the system. One way to do this is to provide credentials (e.g. a username and password using Basic Authentication). We say that once the proper credentials have been presented, the Principal is authenticated, or in other words, the user has successfully logged in.
HTTP is a stateless protocol, so each request must contain data that proves it’s from an authenticated Principal. Although it’s possible to present the credentials on every request, doing so is inefficient because it requires more processing on the server. Instead, an Authentication Session (or Auth Session, or just Session) is created when a user gets authenticated. Sessions can be implemented in many ways. We’ll use a common mechanism: A Session Token (a string of random characters) that is generated, and placed in a Cookie. A Cookie is a set of data stored in a web client (such as a browser), and associated with a specific URI.
A couple of nice things about Cookies:
- Cookies are automatically sent to the server with every request (no extra code needs to be written for this to happen). As long as the server checks that the Token in the Cookie is valid, unauthenticated requests can be rejected.
- Cookies can persist for a certain amount of time even if the web page is closed and later re-visited. This ability typically improves the user experience of the web site.
Spring Security implements authentication in the Filter Chain. The Filter Chain is a component of Java web architecture which allows programmers to define a sequence of methods that get called prior to the Controller. Each filter in the chain decides whether to allow request processing to continue, or not. Spring Security inserts a filter which checks the user’s authentication and returns with a 401 UNAUTHORIZED
response if the request is not authenticated.
Authorization
Up until now we’ve discussed authentication. But in reality, authentication is only the first step. Authorization happens after authentication, and allows different users of the same system to have different permissions.
Spring Security provides Authorization via **Role-Based Access Control (RBAC)**. This means that a Principal has a number of Roles. Each resource (or operation) specifies which Roles a Principal must have in order to perform actions with proper authorization. For example, a user with an Administrator Role is likely to be authorized to perform more actions than a user with a Card Owner Role. You can configure role-based authorization at both a global level and a per-method basis.
Same Origin Policy
The web is a dangerous place, where bad actors are constantly trying to exploit security vulnerabilities. The most basic mechanism of protection relies on HTTP clients and servers implementing the **Same Origin Policy (SOP)**. This policy states that only scripts which are contained in a web page are allowed to send requests to the origin (URI) of the web page.
SOP is critical to the security of web sites because without the policy, anyone could write a web page containing a script which sends requests to any other site. For example, let’s look at a typical banking web site. If a user is logged into their bank account and visits a malicious web page (in a different browser tab or window), the malicious requests could be sent (with the Auth Cookies) to the banking site. This could result in unwanted actions–like a withdrawal from the user’s bank account!
Cross-Origin Resource Sharing
Sometimes a system consists of services running on several machines with different URIs (i.e. Microservices). Cross-Origin Resource Sharing (CORS) is a way that browsers and servers can cooperate to relax the SOP. A server can explicitly allow a list of “allowed origins” of requests coming from an origin outside the server’s.
Spring Security provides the @CrossOrigin
annotation, allowing you to specify a list of allowed sites. Be careful! If you use the annotation without any arguments, it will allow all origins, so bear this in mind!
Common Web Exploits
Along with exploiting known security vulnerabilities, malicious actors on the web are also constantly discovering new vulnerabilities. Thankfully, Spring Security provides a powerful tool set to guard against common security exploits. Let’s discuss two common exploits, how they work, and how Spring Security helps to mitigate them.
Cross-Site Request Forgery
One type of vulnerability is a **Cross-Site Request Forgery (CSRF)** which is often pronounced “Sea-Surf”, and also known as Session Riding. Session Riding is actually enabled by Cookies. CSRF attacks happen when a malicious piece of code sends a request to a server where a user is authenticated. When the server receives the Authentication Cookie, it has no way of knowing if the victim sent the harmful request unintentionally.
To protect against CSRF attacks, you can use a CSRF Token. A CSRF Token is different from an Auth Token because a unique token is generated on each request. This makes it harder for an outside actor to insert itself into the “conversation” between the client and the server.
Thankfully, Spring Security has built-in support for CSRF tokens which is enabled by default.
Cross-Site Scripting
Perhaps even more dangerous than CSRF vulnerability is [Cross-Site Scripting](Cross-Site Scripting) (XSS). This occurs when an attacker is somehow able to “trick” the victim application into executing arbitrary code. There are many ways to do this. A simple example is saving a string in a database containing a <script>
tag, and then waiting until the string is rendered on a web page, resulting in the script being executed.
XSS is potentially more dangerous than CSRF. In CSRF, only actions that a user is authorized to do can be executed. However in XSS, arbitrary malicious code executes on the client or on the server. Additionally, XSS attacks don’t depend on Authentication. Rather, XSS attacks depend on security “holes” caused by poor programming practices.
The main way to guard against XSS attacks is to properly process all data from external sources (like web forms and URI query strings). In the case of our <script>
tag example, attacks can be mitigated by properly escaping the special HTML characters when the string is rendered.