SpringBoot creates media classes and filters that store tokens

Posted by jmarcv on Sun, 19 Sep 2021 07:12:10 +0200

The reason why you need to create a media class to store tokens is that the filter interface to be used later.

1, Create ThreadLocalToken class

Purpose of creating ThreadLocalToken class:

Create the ThreadLocalToken class in com.example.emos.wx.config.shiro.
Write the following code:

package com.example.emos.wx.config.shiro;

import org.springframework.stereotype.Component;

@Component
public class ThreadLocalToken {
    private ThreadLocal local=new ThreadLocal();

    //setToken is required because the token is to be saved in ThreadLocal.
    public void setToken(String token){
        local.set(token);
    }

    public String getToken(){
        return (String) local.get();
    }

    public void clear(){
        local.remove();//Deleted the bound data
    }
}


The following figure shows the hierarchical relationship of creating a directory:

2, Create OAuth2Filter class

Purpose of creating filter:

Because the OAuth2Filter class needs to read and write data in ThreadLocal, the OAuth2Filter class must be set to multiple instances, otherwise ThreadLocal will not be used.
In the configuration file, add the key, expiration time and cache expiration time required by JWT.

emos:
  jwt:
    #secret key
    secret: abc123456
    #Token expiration time (days)
    expire:  5
    #Token cache time (days)
    cache-expire: 10

Create the OAuth2Filter class in com.example.emos.wx.config.shiro.

package com.example.emos.wx.config.shiro;

import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

After the @ Scope("prototype") is written, it indicates that Spring will use OAuth2Filter class by default.

@Value("${emos.jwt.cache-expire}")
Investigate a knowledge point, and obtain the attribute value of the attribute file from the xml file.

To operate in Redis, declare private RedisTemplate redisTemplate;
After declaring this object, you can read and write data in redis.


The filter class is used to distinguish which requests should be processed by shiro and which requests should not be processed by shiro.
If the request is processed by shiro, the createToken method is executed,
createToken obtains the token string from the request, and then encapsulates it into the token object oauthtoken, which is handed over to the shiro framework for processing.
getRequestToken is a custom method used to obtain the Token string and pass it to the Token object.

@Component
@Scope("prototype")
public class OAuth2Filter extends AuthenticatingFilter {
    @Autowired
    private ThreadLocalToken threadLocalToken;

    @Value("${emos.jwt.cache-expire}")
    private int cacheExpire;

    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private RedisTemplate redisTemplate;

    /**
	 * After intercepting the request, it is used to encapsulate the token string into a token object
	 */
	@Override
    protected AuthenticationToken createToken(ServletRequest request, 
		ServletResponse response) throws Exception {
        //Get request token
        String token = getRequestToken((HttpServletRequest) request);

        if (StringUtils.isBlank(token)) {
            return null;
        }

        return new OAuth2Token(token);
    }

Let's talk about filter in detail:
isAccessAllowed is to determine which requests can be processed by shiro and which cannot be processed by shiro.
Since request in isAccessAllowed method is ServletRequest, HttpServletRequest needs to be converted,
Then judge whether the request is an options request. If not, it needs to be handled by shiro.

   /**
	 * Intercept the request and judge whether the request needs to be processed by Shiro
	 */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, 
		ServletResponse response, Object mappedValue) {
        HttpServletRequest req = (HttpServletRequest) request;
        // When Ajax submits application/json data, it will first issue an Options request
		// Here, the Options request needs to be released without Shiro processing
		if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
            return true;
        }
		// All requests except Options requests are processed by Shiro
        return false;
    }

So how did shiro handle it?

onAccessDenied method

Set the character set of the response and the request header of the response. The setHeader method is used to set cross domain requests.

   /**
	 * This method is used to process all requests that should be processed by Shiro
	 */
    @Override
    protected boolean onAccessDenied(ServletRequest request, 
		ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

		resp.setHeader("Content-Type", "text/html;charset=UTF-8");
		//Allow cross domain requests
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        
		//The clear method is used to clean up the methods in the threadLocal class,
		threadLocalToken.clear();
		
        //Get the request token. If the token does not exist, it directly returns 401
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("Invalid token");
            return false;
        }

Then verify that the token has expired.
If there is a problem with validation, an exception is thrown.
By catching exceptions, you can know whether there is a problem with the token or the token has expired.
JWTDecodeException is a content exception.

Query whether there is a token in Redis through the hasKey of redisTemplate.
If there is a token, delete the old token and regenerate a token to the client.
The executeLogin method lets shiro execute the realm class.

  try {
            jwtUtil.verifierToken(token); //Check whether the token has expired
        } catch (TokenExpiredException e) {
            //When the client token expires, query whether there is a token in Redis. If there is a token, regenerate a token to the client
            if (redisTemplate.hasKey(token)) {
                redisTemplate.delete(token);//Delete old token
                int userId = jwtUtil.getUserId(token);
                token = jwtUtil.createToken(userId);  //Generate a new token
                //Save the new token to Redis
                redisTemplate.opsForValue().set(token, userId + "", cacheExpire, TimeUnit.DAYS);
                //Bind new token to thread
                threadLocalToken.setToken(token);
            } else {
                //If there is no token in Redis, ask the user to log in again
                resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
                resp.getWriter().print("The token has expired");
                return false;
            }

        } catch (JWTDecodeException e) {
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("Invalid token");
            return false;
        }

        boolean bool = executeLogin(request, response);
        return bool;
    }

Information output after login failure.

    @Override
    protected boolean onLoginFailure(AuthenticationToken token,
		AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
        resp.setContentType("application/json;charset=utf-8");
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        try {
            resp.getWriter().print(e.getMessage());//Capture authentication failure message
        } catch (IOException exception) {

        }
        return false;
    }

Get the token in the request header

    /**
     * Get the token in the request header
     */
    private String getRequestToken(HttpServletRequest httpRequest) {
        //Get token from header
        String token = httpRequest.getHeader("token");

        //If there is no token in the header, get the token from the parameter
        if (StringUtils.isBlank(token)) {
            token = httpRequest.getParameter("token");
        }
        return token;

    }

The doFilterInternal method inherits from the parent doFilterInternal and is in charge of intercepting requests and responses. Not overwritten here.

    @Override
    public void doFilterInternal(ServletRequest request, 
		ServletResponse response, FilterChain chain) throws ServletException, IOException {
        super.doFilterInternal(request, response, chain);
    }
}

Topics: Redis Spring Boot