When it comes to implementing user authentication in RESTful API server, there’re several options like Spring Security, Apache Shiro, or writing our own version of Filters and Servlets. If the server already uses Spring Boot, then Spring Security is really a good fit, for it integrates quite well with Spring Boot project, thanks to all those automatic configurations. However, Spring Security’s login facility is originally built for web forms or basic HTTP authentication, while modern apps usually lean on RESTful API. We can either adapt the frontend client to utilizing the built-in login methods as in this tutorial Spring Security and Angular JS, or write custom Filter to extract user credentials from input JSON.
Having said that, personally I still prefer to maintain a consistent API style in user authentication, and I don’t want to write awkward logics with raw Servlet request/response objects in Filter, instead of using what Spring MVC provides, i.e. @RestController
, @RequestBody
, form validation, etc. Luckily, Spring Security provides integration for Servlet API, so that we can login/logout user within the Controller. In this article, I will demonstrate how to use Spring Security to guard your RESTful API server, with the following functions:
- Login/logout with JSON API.
- Return 401 for unauthenticated requests.
- Custom table for user data.
- CSRF protection.
- Remember me.
- Session persistence.
Defining user authentication API
Let’s define three APIs for user login, logout, and one that returns the currently logged-in user. All the requests and responses should be in the form of application/json
.
1 | POST /api/login |
With Spring Boot, creating RESTful APIs is effortless. In the following example, we also add form validation and a custom exception handled by a global contoller. But these functions are beyond the scope of this article. The Spring Boot version I’m using is 3.x, with Spring Security 6.x, and Java 17.
1 |
|
Configure Spring Security filter chain
Add the Spring Security dependency into the project, along with the JDBC related ones, since we’re going to retrieve user information from own version of user
table. Note the dependency versions are managed by Spring Boot parent pom.
1 | <dependency> |
It’s not that Spring Security doesn’t come with good defaults for table schema, but we probably want to have more control over them or we already have a set of user tables. If you’re interested, here’s the link to the default User Schema. Instead, I’m using the following schema in this demo.
1 | CREATE TABLE user ( |
The default password-hashing algorithm used by Spring Security is BCrypt. The following snippet can be used to generate such password digest. Other options can be found here.
1 | var encoder = new BCryptPasswordEncoder(); |
By default, Spring Security will guard all API endpoints including /api/login
, so we first need to tell it to back down at certain requests, by configuring the SecurityFilterChain
:
1 |
|
In addition, we tell Spring Security that when an unauthenticated user tries to access the restricted routes, it’ll respond with 401 Unauthorized, so that the client, usually a single page application, can redirect to its login page. This facility is called authentication entry point. In the old days, it was the server’s job to redirect to a login page, so the default entry point is an HTML page resided in the /login
URL.
Retrieve user credentials from database
Again, with Spring Boot, this task is much simplified. Let’s create the User
entity and its corresponding repository.
1 |
|
Note the User
class implements the UserDetails
interface, which tells Spring Security that this class can be used for authentication. To wire it into the mechanism, we need another class that implements UserDetailsService
interface, mainly for retrieving the User
instances from wherever we store them.
1 |
|
It’ll find the table row by username, and use the aforementioned password encoder to check the authenticity.
User login in Controller methods
From Servlet 3+, HttpServletRequest
adds login
/logout
methods to help authenticate user credential programmatically, and Spring Security integrates with this function. So in our /api/login
handler, we simply invoke this method:
1 |
|
request.logout
can be used accordingly, and for /api/current-user
, the @AuthenticationPrincipal
annotation can be used on parameter to access the currently logged-in user:
1 |
|
Now we can test these APIs with httpie, a commandline HTTP client:
1 | % http localhost:8080/api/current-user |
As expected, since we’re not logged in, the server responds with 401. Then let’s try authenticate with username and password:
1 | % http localhost:8080/api/login username=admin password=888888 |
Unfortunately, the server denies us agian even if we provide the correct credential. The reason is Spring Security, by default, enables CSRF protection for all non-idempotent requests, such as POST, DELETE, etc. This can be disabled by configuration, and next section I’ll show you how to use it properly to protect the API.
1 |
|
Now test the API again. Note that in the second request, we pass the Session ID as Cookie. You may notice the key SESSION
is different from the default JSESSIONID
, that is because I’m using Spring Session for session persistence, which I’ll cover in the last section.
1 | % http localhost:8080/api/login username=admin password=888888 |
Enable CSRF protection
CSRF protection prevents malicious site from tricking user to submit a form unwillingly. Every form will be embedded with a server-generated token known as the CSRF token. Since the token cannot be attained by third-party, and it is validated in every submission, thus making the request safe. In the old days, again, web forms are generated on server side, while the token is saved in a hidden <input>
and got submitted together with the form data. For instance, in Thymeleaf the token can be retrieved by a request attribute named _csrf
:
1 | <input |
But with SPA (Single Page Application), we need another way to retrieve the token. One approach is mentioned in the Angular tutorial I linked to earlier, in which the CSRF token is saved in Cookie, and every Ajax POST request is equipped with a header containing this token. Here I take a different approach, that is creating a dedicated endpoint for token retrieval:
1 |
|
This API should also be excluded from Spring Security:
1 | requestMatchers("/api/csrf").permitAll() |
The client could fetch the CSRF token when it needs to do a POST/DELETE request. This token can also be cached in localStorage
for further use, as long as the session is not timed out. Don’t forget to clear the cache when user logs out.
1 | async function getCsrfToken() { |
Remember-me authentication
When implementing this demo, the most tricky part is to utilize Spring Security’s built-in remember-me authentication, in that Spring Security basically functions as a series of Filters, so when I decide to authenticate user in Controller instead of Filter, there’ll be some extra work to do. Normally, with form login or filter-based auth, remember-me can be switched on by the following config:
1 | http.rememberMe(customizer -> customizer.alwaysRemember(true).key("demo")) |
Under the hood, when user has logged in successfully, RememberMeServices#loginSuccess
is invoked to generate and save a remember-me
Cookie to the client. Next time the user can login without providing username and password.
1 | % http localhost:8080/api/login username=admin password=888888 \ |
Unfortunately, HttpServletRequest#login
does not call RememberMeServices#loginSuccess
for us, so we need to invoke the method by ourselves. Worse still, the RememberMeServices
instance, in this case TokenBasedRememberMeServices
, is only available within the Filter chain, meaning it is not registered in the Spring IoC container. After some digging in the source code, I managed to expose this instance to other Spring components.
1 |
|
A RememberMeServices
instance is created in the configuration phase by Spring Security, and we save it into the IoC container, making it available in the AuthController
. The @DependsOn
annotation ensures that RememberMeServices
is registered before the AuthController
is created. Next, the loginSuccess
method can be invoked like this:
1 |
|
Session persistence
Login state and CSRF token are stored in HTTP Session, and by default Session data are kept in Java process memory, so when the server restarts or there’re multiple backends, users may need to login several times. The solution is simple, use Spring Session to store data in a third-party persistent storage. Take Redis for an example.
1 | <dependency> |
Due to Spring Boot’s auto-configuration feature, adding the dependencies will suffice to use Redis as the Session storage. To specify the Redis instance in production, add the following configs in application.properties
.
1 | spring.redis.host=localhost |
The demo project can be found on GitHub.