Analysis of spring Security + JWT authentication process

Time:2021-2-26

It’s easy to feel it on paper, and you need to practice it.

Wedge

This article is suitable for:For those students who know a little about spring security or have run a simple demo but don’t understand the overall running process, those who are interested in spring security can also be used as your introductory tutorial, and there are many comments in the sample code.

Article code:Code cloud address   GitHub address

When we do the system, the first module we usually do isAuthentication and authorizationModule, because this is the entrance of a system, and it is also the most important and basic part of a system. After the authentication and authorization service is designed and built, the remaining modules can be safely accessed.

Generally, the framework of authentication and authorization in the market isshiroandSpring SecurityMost companies choose to develop their own products. I’ve seen a lot beforeSpring SecurityBut I don’t think it’s very good, so I’m working on it these two daysSpring SecurityWhen I was young, I came up with the idea of sharing, hoping to help people who are interested.

Spring SecurityThe framework is mainly used to solve an authentication and authorization function, so my article will be divided into two parts

  • Part I certification
  • Part II authorization (next)

I will show you what I want to talk about with a demo of spring Security + JWT + cache. After all, the brain should be reflected in specific things in order to let you know more intuitively.

When learning a new thing, I recommend using the top-down learning method, so that we can better understand the new thing, rather than blind people feel the elephant.

notes: it only involves user authentication and authorization, not third-party authorization such as oauth2.

1. Workflow of spring security

If you want to get started with spring security, you must first understand its workflow, because it is not like a toolkit. You must have a certain understanding of it, and then customize it according to its usage.

Let’s take a look at its workflow first
staySpring securityThere is a saying in the official document:

Spring Security’s web infrastructure is based entirely on standard servlet filters.

The web foundation of spring security is filters.

This sentence shows thatSpring SecurityThe design idea is as followsThat is to say, the web requests are processed through layer by layer filters.

Put it in the real worldSpring SecurityIn Chinese, words can be expressed as follows:

A web request will go through a filter chain. In the process of passing through the filter chain, authentication and authorization will be completed. If it is found that the request is not authenticated or authorized, exceptions will be thrown according to the permissions of the protected API, and then the exception processor will handle these exceptions.

This is a picture I found in Baidu

Analysis of spring Security + JWT authentication process

As shown in the figure above, if a request wants to access the API, it will go through the filter in the blue box from left to right. The green part is the filter responsible for authentication, the blue part is responsible for exception handling, and the orange part is responsible for authorization.

We won’t talk about the two green filters in the figure today, because they are built-in filters for form authentication and basic authentication of spring security, and our demo is JWT authentication mode, so we can’t use them.

If you’ve used itSpring SecurityYou should know that there are two configurations calledformLoginandhttpBasicIn the configuration, opening them corresponds to opening the above filter.

Analysis of spring Security + JWT authentication process

  • formLoginCorresponding to your form authentication method, namely usernamepasswordauthentication filter.
  • httpBasicIt corresponds to basic authentication filter.

In other words, if you configure these two authentication methods, they will be added to the filter chain, otherwise they will not be added to the filter chain.

becauseSpring SecurityThere is no JWT authentication method in the built-in filter, so our demo willWrite a JWT authentication filter, and then put it in the green position for authentication.

2. Important concepts of spring security

After knowing the general workflow of spring security, we need to know some very important concepts, which can also be said to be components:

  • SecurityContext: context object,AuthenticationObjects will be in it.
  • SecurityContextHolder: static tool class for getting context objects.
  • Authentication: authentication interface, which defines the data form of authentication object.
  • AuthenticationManager: for verificationAuthenticationTo return a message after authenticationAuthenticationObject.

1.SecurityContext

Context object, in which the data after authentication is put. The interface definition is as follows:

public interface SecurityContext extends Serializable {
    //Gets the authentication object
    Authentication getAuthentication();

    //Put authentication object
    void setAuthentication(Authentication authentication);
}

There are only two methods in this interface. Its main function is to get or setAuthentication

2. SecurityContextHolder

public class SecurityContextHolder {

    public static void clearContext() {
        strategy.clearContext();
    }

    public static SecurityContext getContext() {
        return strategy.getContext();
    }
    
    public static void setContext(SecurityContext context) {
        strategy.setContext(context);
    }

}

It can be said that it isSecurityContextTool class for get or set or clearSecurityContextBy default, all data will be stored in the current thread.

3. Authentication

public interface Authentication extends Principal, Serializable {
 
    Collection<? extends GrantedAuthority> getAuthorities();
    Object getCredentials();
    Object getDetails();
    Object getPrincipal();
    boolean isAuthenticated();
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

The effects of these methods are as follows:

  • getAuthorities: to obtain user permissions. In general, what you get isUser’s role information
  • getCredentials: get the information to prove the user’s authentication, usually get the password and other information.
  • getDetails: get additional information of users (this part of information can be in our user table).
  • getPrincipal: get the user’s identity information, and the user name is obtained without authentication,In the case of authentication, the user details is obtained.
  • isAuthenticated: get the currentAuthenticationWhether it has been certified.
  • setAuthenticated: set currentAuthenticationIs authenticated (true or false).

AuthenticationIt only defines what the data form of the data authenticated in spring security should be, including permissions, passwords, identity information and additional information.

4. AuthenticationManager

public interface AuthenticationManager {
    //Authentication method
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}

AuthenticationManagerAn authentication method is defined, which converts anAuthenticationPass in and return an authenticatedAuthentication, the default implementation class is: providermanager.

Next, you can imagine how to connect these four parts in series to form the process of spring security authentication

  1. First, a request came in with identity information
  2. afterAuthenticationManagerCertification of,
  3. Pass againSecurityContextHolderobtainSecurityContext
  4. Finally, the information after authentication is put into theSecurityContext

3. Preparation before coding

Before we really start talking about our authentication code, we first need to import the necessary dependencies. For database related dependencies, we can choose which JDBC framework to use. Here, I use myabtis plus, which is developed by Chinese people for the second time.

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

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.0</version>
        </dependency>
        
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

Next, we need to define several required components.

Since the spring boot I use is 2. X, we have to define an encryptor ourselves

1. Define the encryptor bean

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

This bean is indispensable,Spring SecurityThe encryptor we defined will be used in the authentication operation. If not, an exception will appear.

2. Define authentication manager

@Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

Here will beSpring SecurityBring your ownauthenticationManagerDeclare it as a bean. The purpose of declaring it is to use it to help us carry out authentication operation and call the beanauthenticateThe method will be determined bySpring SecurityAutomatic certification for us.

3. Implement userdetailsservice

public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleInfoService roleInfoService;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        log.debug ("start login verification, user name: {}", s));

        //User authentication based on user name
        QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(UserInfo::getLoginAccount,s);
        UserInfo userInfo = userService.getOne(queryWrapper);
        if (userInfo == null) {
            Throw new usernamenotfoundexception ("user name does not exist, login failed. ");
        }

        //Building the userdetail object
        UserDetail userDetail = new UserDetail();
        userDetail.setUserInfo(userInfo);
        List<RoleInfo> roleInfoList = roleInfoService.listRoleByUserId(userInfo.getUserId());
        userDetail.setRoleInfoList(roleInfoList);
        return userDetail;
    }
}

realizationUserDetailsServiceAnd returns anUserDetailsObject. In the authentication process, spring security will call this method to access the database to search for users. The logic can be customized, whether from the database or from the cache. However, we need to assemble the user information and permission information that we find out into a databaseUserDetailsreturn.

UserDetailsIt is also an interface that defines the data form, which is used to save the data we find out from the database. Its main function is to verify the account status and obtain permissions. The specific implementation can refer to the code of our warehouse.

4. TokenUtil

Because we are the authentication mode of JWT, we also need a tool class to help us operate token. Generally speaking, it has the following three methods:

  • Create token
  • Verify token
  • Anti parsing information in token

In my code below, jwtprovider acts as a token tool class. Please refer to the code of my warehouse for the specific implementation.

four ✍ The concrete realization in the code

With the previous explanation, we should all know how to useSpringSecurityTo do JWT certification, we need to write a filter to do JWT verification, and then put the filter in the green part.

Before we write this filter, we need to perform an authentication operation, because we need to access the authentication interface to get the token before we can put the token on the request header for the next request.

If you don’t quite understand, it doesn’t matter. I’ll sort it out again at the end of this section.

1. Authentication method

Access to a system, generally the first access is the authentication method, here I wrote the simplest authentication steps, because in the actual system, we also need to write login records, foreground password decryption, these operations.

@Override
    public ApiResult login(String loginAccount, String password) {
        //1 create a usernamepasswordauthenticationtoken
        UsernamePasswordAuthenticationToken usernameAuthentication = new UsernamePasswordAuthenticationToken(loginAccount, password);
        //2 certification
        Authentication authentication = this.authenticationManager.authenticate(usernameAuthentication);
        //3 save authentication information
        SecurityContextHolder.getContext().setAuthentication(authentication);
        //4 generate custom token
        UserDetail userDetail = (UserDetail) authentication.getPrincipal();
        AccessToken accessToken = jwtProvider.createToken((UserDetails) authentication.getPrincipal());

        //5 put in cache
        caffeineCache.put(CacheName.USER, userDetail.getUsername(), userDetail);
        return ApiResult.ok(accessToken);
    }

There are five steps, but the first four steps are relatively unfamiliar

  1. Pass in the user name and password to create aUsernamePasswordAuthenticationTokenObject, this is what we said beforeAuthenticationImplementation class, pass in the user name and password as construction parameters, this object is created by us without authenticationAuthenticationObject.
  2. Use the bean we have previously declared-authenticationManagerCall itsauthenticateMethod to authenticate and return an authenticated resultAuthenticationObject.
  3. If there is no exception after authentication, you will go to the third step and use theSecurityContextHolderobtainSecurityContextAfter that, theAuthenticationObject, put in the context object.
  4. fromAuthenticationWe got ourUserDetailsObject, as we said before, after authenticationAuthenticationObject to call itsgetPrincipal()Method can get our previous database query after the assemblyUserDetailsObject, and then create the token.
  5. holdUserDetailsThe object is put into the cache to facilitate the use of the following filters.

In this way, even if it is completed, it feels very simple, because the main authentication operations will be performed by the userauthenticationManager.authenticate()Help us finish it.


Next, we can take a look at the source code to see how spring security can help us with this authentication (omitting part of the code)

// AbstractUserDetailsAuthenticationProvider

    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {

        //Check whether there is a user name in the authentication object that is not authenticated
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

        boolean cacheWasUsed = true;
        //Look up the object with user name xxx from the cache
        UserDetails user = this.userCache.getUserFromCache(username);

        //If not, go to this method
        if (user == null) {
            cacheWasUsed = false;

            try {
                //Call the loaduserbyusername method that we override the userdetailsservice
                //Get the userdetails object we assembled ourselves
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {
                logger.debug("User '" + username + "' not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                }
                else {
                    throw notFound;
                }
            }

        }

        try {
            
            //Check whether the account is disabled
            preAuthenticationChecks.check(user);
            //Check whether the password found in the database is consistent with the password we passed in
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }

}

After looking at the source code, you will find that the main logic is to look up the database and then compare the password as we usually write.

After login, the effect is as follows:

Analysis of spring Security + JWT authentication process

After we return the token, the next time we request other APIs, we need to bring this token in the request header, just according to the JWT standard.

2. JWT filter

With the token, we need to put the filter in the filter chain to parse the token. Because we don’t have a session, every time we identify the user’s request, we will parse the current user according to the token in the request.

So we need a filter to intercept all requests. As we said earlier, we will put this filter in the green part to replace itUsernamePasswordAuthenticationFilterSo we build a new oneJwtAuthenticationTokenFilterThen register it as a bean, and add this when writing the configuration file:

@Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilter();
    }

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(jwtAuthenticationTokenFilter(),
                        UsernamePasswordAuthenticationFilter.class);
    }

addFilterBeforeThe semantics of is to add a filter to xxfilter before putting it hereJwtAuthenticationTokenFilterPut inUsernamePasswordAuthenticationFilterPreviously, because the implementation of filters is also sequential, we have to put our filters in the green part of the filter chain to achieve the effect of automatic authentication.

Next we can take a lookJwtAuthenticationTokenFilterThe concrete realization of this method is as follows

@Override
    protected void doFilterInternal(@NotNull HttpServletRequest request,
                                    @NotNull HttpServletResponse response,
                                    @NotNull FilterChain chain) throws ServletException, IOException {
        log.info (JWT filter automatically logs in by verifying the request header token...);

        //Get the information in the authorization request header
        String authToken = jwtProvider.getToken(request);

        //Determine whether the content is empty and starts with (bearer)
        if (StrUtil.isNotEmpty(authToken) && authToken.startsWith(jwtProperties.getTokenPrefix())) {
            //Remove the token prefix (bearer) and get the real token
            authToken = authToken.substring(jwtProperties.getTokenPrefix().length());

            //Get the login account in the token
            String loginAccount = jwtProvider.getSubjectFromToken(authToken);

            if (StrUtil.isNotEmpty(loginAccount) && SecurityContextHolder.getContext().getAuthentication() == null) {
                //There is no need to log in again.
                UserDetail userDetails = caffeineCache.get(CacheName.USER, loginAccount, UserDetail.class);

                //After getting the user information, verify the user information and token
                if (userDetails != null && jwtProvider.validateToken(authToken, userDetails)) {

                    //Assemble the authentication object, and the construction parameters are principal credentials and authorities
                    //Later interceptors will use the grantedauthorities method
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());

                    //Put the authentication information into the context object
                    SecurityContextHolder.getContext().setAuthentication(authentication);

                    log.info (JWT filter automatically logs in successfully by verifying the request header token, user: {}), userDetails.getUsername ());
                }
            }
        }

        chain.doFilter(request, response);
    }

Although the steps in the code are very detailed, it may be that the code is too long to read. I’d like to talk about it briefly. You can also go to the warehouse to check the source code directly

  1. Get itAuthorizationThe token information corresponding to the request header
  2. Remove the header of the token (bearer)
  3. Analyze the token and get the login account we put in it
  4. Because we have logged in before, we take our data directly from the cacheUserDetailInformation is enough
  5. Check whether the userdetail is null and whether the token is expired,UserDetailWhether the user name is consistent with the token.
  6. Assemble oneauthenticationObject, put it in the context object, so that the later filter can see that there areauthenticationObject, which means that we have already authenticated.

In this way, every request with the correct token will find its account information and put it in the context object, which we can useSecurityContextHolderIt’s very convenient to get the information in the context objectAuthenticationObject.

After the completion, start our demo, and you can see that there are the following filters in the filter chain, among which the fifth one is customized:

Analysis of spring Security + JWT authentication process

As a matter of fact, the account information and role information we get after logging in will be put into the cache. When the request with token comes, we will take it out of the cache and put it into the context object again.

Combined with the authentication method, our logical chain becomes:

Log in to get the token request, take the token JWT filter to intercept and verify the token, and put the object found in the cache into the context

After that, our authentication logic is complete.

4. Code optimization

After the completion of authentication and JWT filter, the JWT project can actually run and achieve the effect we want. If we want to make the program more robust, we need to add some auxiliary functions to make the code more friendly.

1. Authentication failure processor

Analysis of spring Security + JWT authentication process

When the user is not logged in or the token resolution fails, the processor will be triggered and an illegal access result will be returned.

Analysis of spring Security + JWT authentication process

2. Insufficient permissions

Analysis of spring Security + JWT authentication process

When the user’s own permissions do not meet the permissions required by the API, the processor will be triggered to return a result of insufficient permissions.

Analysis of spring Security + JWT authentication process

3. Exit method

Analysis of spring Security + JWT authentication process

User exit is to clear the context object and cache. You can also do some additional operations. These two steps are necessary.

4. Token refresh

Analysis of spring Security + JWT authentication process

JWT project token refresh is also essential. Here, the main method to refresh the token is put in the token tool class. After the refresh, just reload the cache, because the cache has a validity period. Re put can reset the expiration time.

Postscript

I have been working on this article since last Sunday. In order to understand what I can say, I revised it several times before sending it out.

Spring SecurityIt’s really difficult to get started. When I first went to learn about it, I saw the tutorial of Shangsi valley. The lecturer of that video combined it with thymeleaf, which led to a lot of blogs on the InternetSpring SecurityIt’s the same way when it comes to the front and back ends, without paying attention to the separation of the front and back ends.

There are also tutorials to do when the filter is directly inheritedUsernamePasswordAuthenticationFilterThis method is also feasible, but after we understand the overall operation process, you will know that there is no need to do so. You don’t need to inherit XXX, just write a filter and put it in that position.

Well, after the end of authentication, the next part is dynamic authentication. This is the first article I’m thinking about. It’s my first knowledge output. I hope you will continue to pay attention to it.

Each of your likes and comments is a great affirmation of my knowledge output. If there is any mistake or doubt in the article or my advice, you can leave a message at the bottom of the comment area and discuss it together.

I am ear, a person who always wants to export knowledge. See you next time.

Article code:Code cloud address   GitHub address

Recommended Today

Deeply analyze the principle and practice of RSA key

1、 Preface After experiencing many dark moments in life, when you read this article, you will regret and even be angry: why didn’t you write this article earlier?! Your darkest moments include: 1. Your project needs to be connected with the bank, and the other party needs you to provide an encryption certificate. You have […]