Spring Security with JWT for REST API[转]

Spring is considered a trusted framework in the Java ecosystem and is widely used. It’s no longer valid to refer to Spring as a framework, as it’s more of an umbrella term that covers various frameworks. One of these frameworks is Spring Security, which is a powerful and customizable authentication and authorization framework. It is considered the de facto standard for securing Spring-based applications.

Despite its popularity, I must admit that when it comes to single-page applications, it’s not simple and straightforward to configure. I suspect the reason is that it started more as an MVC application-oriented framework, where webpage rendering happens on the server-side and communication is session-based.

If the back end is based on Java and Spring, it makes sense to use Spring Security for authentication/authorization and configure it for stateless communication. While there are a lot of articles explaining how this is done, for me, it was still frustrating to set it up for the first time, and I had to read and sum up information from multiple sources. That’s why I decided to write this article, where I will try to summarize and cover all the required subtle details and foibles you may encounter during the configuration process.

Defining Terminology

Before diving into the technical details, I want to explicitly define the terminology used in the Spring Security context just to be sure that we all speak the same language.

These are the terms we need to address:

  • Authentication refers to the process of verifying the identity of a user, based on provided credentials. A common example is entering a username and a password when you log in to a website. You can think of it as an answer to the question Who are you?.
  • Authorization refers to the process of determining if a user has proper permission to perform a particular action or read particular data, assuming that the user is successfully authenticated. You can think of it as an answer to the question Can a user do/read this?.
  • Principle refers to the currently authenticated user.
  • Granted authority refers to the permission of the authenticated user.
  • Role refers to a group of permissions of the authenticated user.

Creating a Basic Spring Application

Before moving to the configuration of the Spring Security framework, let’s create a basic Spring web application. For this, we can use a Spring Initializr and generate a template project. For a simple web application, only a Spring web framework dependency is enough:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

Once we have created the project, we can add a simple REST controller to it as follows:

@RestController @RequestMapping("hello")
public class HelloRestController {

    @GetMapping("user")
    public String helloUser() {
        return "Hello User";
    }

    @GetMapping("admin")
    public String helloAdmin() {
        return "Hello Admin";
    }

}

After this, if we build and run the project, we can access the following URLs in the web browser:

  • http://localhost:8080/hello/user will return the string Hello User.
  • http://localhost:8080/hello/admin will return the string Hello Admin.

Now, we can add the Spring Security framework to our project, and we can do this by adding the following dependency to our pom.xml file:

<dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

Adding other Spring framework dependencies doesn’t normally have an immediate effect on an application until we provide the corresponding configuration, but Spring Security is different in that it does have an immediate effect, and this usually confuses new users. After adding it, if we rebuild and run the project and then try to access one of the aforementioned URLs instead of viewing the result, we will be redirected to http://localhost:8080/login. This is default behavior because the Spring Security framework requires authentication out of the box for all URLs.

To pass the authentication, we can use the default username user and find an auto-generated password in our console:

Using generated security password: 1fc15145-dfee-4bec-a009-e32ca21c77ce

Please remember that the password changes each time we rerun the application. If we want to change this behavior and make the password static, we can add the following configuration to our application.properties file:

spring.security.user.password=Test12345_

Now, if we enter credentials in the login form, we will be redirected back to our URL and we will see the correct result. Please note that the out-of-the-box authentication process is session-based, and if we want to log out, we can access the following URL: http://localhost:8080/logout

This out-of-the-box behavior may be useful for classic MVC web applications where we have session-based authentication, but in the case of single-page applications, it’s usually not useful because in most use cases, we have client-side rendering and JWT-based stateless authentication. In this case, we will have to heavily customize the Spring Security framework, which we will do in the remainder of the article.

As an example, we will implement a classic bookstore web application and create a back end that will provide CRUD APIs to create authors and books plus APIs for user management and authentication.

Spring Security Architecture Overview

Before we start customizing the configuration, let’s first discuss how Spring Security authentication works behind the scenes.

The following diagram presents the flow and shows how authentication requests are processed:

Spring Security Architecture

Spring Security with JWT for REST API[转]

 

Now, let’s break down this diagram into components and discuss each of them separately.

Spring Security Filters Chain

When you add the Spring Security framework to your application, it automatically registers a filters chain that intercepts all incoming requests. This chain consists of various filters, and each of them handles a particular use case.

For example:

  • Check if the requested URL is publicly accessible, based on configuration.
  • In case of session-based authentication, check if the user is already authenticated in the current session.
  • Check if the user is authorized to perform the requested action, and so on.

One important detail I want to mention is that Spring Security filters are registered with the lowest order and are the first filters invoked. For some use cases, if you want to put your custom filter in front of them, you will need to add padding to their order. This can be done with the following configuration:

spring.security.filter.order=10

Once we add this configuration to our application.properties file, we will have space for 10 custom filters in front of the Spring Security filters.

AuthenticationManager

You can think of AuthenticationManager as a coordinator where you can register multiple providers, and based on the request type, it will deliver an authentication request to the correct provider.

AuthenticationProvider

AuthenticationProvider processes specific types of authentication. Its interface exposes only two functions:

  • authenticate performs authentication with the request.
  • supports checks if this provider supports the indicated authentication type.

One important implementation of the interface that we are using in our sample project is DaoAuthenticationProvider, which retrieves user details from a UserDetailsService.

UserDetailsService

UserDetailsService is described as a core interface that loads user-specific data in the Spring documentation.

In most use cases, authentication providers extract user identity information based on credentials from a database and then perform validation. Because this use case is so common, Spring developers decided to extract it as a separate interface, which exposes the single function:

  • loadUserByUsername accepts username as a parameter and returns the user identity object.

Authentication Using JWT with Spring Security

After discussing the internals of the Spring Security framework, let’s configure it for stateless authentication with a JWT token.

To customize Spring Security, we need a configuration class annotated with @EnableWebSecurity annotation in our classpath. Also, to simplify the customization process, the framework exposes a WebSecurityConfigurerAdapter class. We will extend this adapter and override both of its functions so as to:

  1. Configure the authentication manager with the correct provider
  2. Configure web security (public URLs, private URLs, authorization, etc.)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // TODO configure authentication manager
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // TODO configure web security
    }

}

In our sample application, we store user identities in a MongoDB database, in the users collection. These identities are mapped by the User entity, and their CRUD operations are defined by the UserRepo Spring Data repository.

Now, when we accept the authentication request, we need to retrieve the correct identity from the database using the provided credentials and then verify it. For this, we need the implementation of the UserDetailsService interface, which is defined as follows:

public interface UserDetailsService {

    UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException;

}

Here, we can see that it is required to return the object that implements the UserDetails interface, and our User entity implements it (for implementation details, please see the sample project’s repository). Considering the fact that it exposes only the single-function prototype, we can treat it as a functional interface and provide implementation as a lambda expression.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserRepo userRepo;

    public SecurityConfig(UserRepo userRepo) {
        this.userRepo = userRepo;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(username -> userRepo
            .findByUsername(username)
            .orElseThrow(
                () -> new UsernameNotFoundException(
                    format("User: %s, not found", username)
                )
            ));
    }

    // Details omitted for brevity

}

Here, the auth.userDetailsService function call will initiate the DaoAuthenticationProvider instance using our implementation of the UserDetailsService interface and register it in the authentication manager.

Along with the authentication provider, we need to configure an authentication manager with the correct password-encoding schema that will be used for credentials verification. For this, we need to expose the preferred implementation of the PasswordEncoder interface as a bean.

In our sample project, we will use the bcrypt password-hashing algorithm.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserRepo userRepo;

    public SecurityConfig(UserRepo userRepo) {
        this.userRepo = userRepo;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(username -> userRepo
            .findByUsername(username)
            .orElseThrow(
                () -> new UsernameNotFoundException(
                    format("User: %s, not found", username)
                )
            ));
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // Details omitted for brevity

}

Having configured the authentication manager, we now need to configure web security. We are implementing a REST API and need stateless authentication with a JWT token; therefore, we need to set the following options:

【转自】https://www.toptal.com/spring/spring-security-tutorial

上一篇:liunux 查看系统参数、网络参数的命令


下一篇:消息中间件系列教程(21) -Kafka- 集群搭建(自带Zookeeper)