SoFunction
Updated on 2025-04-11

Springboot+vue3 no perception refresh token practical tutorial

In web websites, when interacting with front and back ends, the token mechanism is usually used for authentication. The token generally sets the validity period. When the token has passed the validity period, the user needs to log in and authorize again to obtain a new token. However, in some business scenarios, users do not want to frequently log in and authorize, but security considerations, the validity period of the token cannot be set for too long. Therefore, with the design of refreshing tokens, the mechanism of refreshing tokens without perception further optimizes the user experience. This article is a practical code battle based on springboot and vue3 unconscious refresh tokens in the blogger's actual business project.

First, let’s introduce the implementation idea of ​​refreshing tokens without perception:

① When authorizing tokens for the first time, we write two cookies to the front-end request response through the back-end

  • - access_token
  • - refresh_token (timeout time is longer than access_token)

Need to note:

- httpOnly=true when setting Cookie in the backend (restriction cookies can only be carried and used by http requests, and cannot be operated by js)

- Front-end axios request parameter withCredentials=true (autoken is automatically carried when http request)

  • ② When access_token fails, a special exception is thrown. The front and back end agrees to http response code (401), and the refresh token logic is triggered.
  • ③In the previous http request hook, if the http response code is 401, the refresh token logic will be triggered immediately, and subsequent requests will be cached. After the refresh token is completed, the cached request will be continued in turn.

1. Java backend

Backend Java framework uses springboot, spring-security

Login interface:

/**
 * @author lichenhao
 * @date 2023/2/8 17:41
 */
@RestController
public class AuthController {

    /**
      * Login method
      *
      * @param loginBody Login Information
      * @return Results
      */
    @PostMapping("/oauth")
    public AjaxResult login(@RequestBody LoginBody loginBody) {
        ITokenGranter granter = (());
        return (loginBody);
    }
}


import ;

/**
  * User login object
  *
  * @author lichenhao
  */
@Data
public class LoginBody {

    /**
      * username
      */
    private String username;

    /**
      * User password
      */
    private String password;

    /**
      * Verification code
      */
    private String code;

    /**
      * Unique ID
      */
    private String uuid;

    /*
      * grantType Authorization Type
      * */
    private String grantType;

    /*
     * Whether to force the account to log in to other clients
     * */
    private Boolean forceLogoutFlag;
}

Token construct interface class and token implement class constructor are as follows:

/**
  * @author lichenhao
  * @date 2023/2/8 17:29
  * <p>
  * Get token
  */
public interface ITokenGranter {

    AjaxResult grant(LoginBody loginBody);
}


/**
 * @author lichenhao
 * @date 2023/2/8 17:29
 */
@AllArgsConstructor
public class TokenGranterBuilder {

    /**
      * TokenGranter cache pool
      */
    private static final Map&lt;String, ITokenGranter&gt; GRANTER_POOL = new ConcurrentHashMap&lt;&gt;();

    static {
        GRANTER_POOL.put(CaptchaTokenGranter.GRANT_TYPE, ());
        GRANTER_POOL.put(RefreshTokenGranter.GRANT_TYPE, ());
    }

    /**
      * Get TokenGranter
      *
      * @param grantType Authorization type
      * @return ITOkenGranter
      */
    public static ITokenGranter getGranter(String grantType) {
        ITokenGranter tokenGranter = GRANTER_POOL.get((grantType, PasswordTokenGranter.GRANT_TYPE));
        if (tokenGranter == null) {
            throw new ServiceException("no grantType was found");
        } else {
            return tokenGranter;
        }
    }

}

Here, the actual token construct implementation class is specified through the grantType property of LoginBody; at the same time, there is a token

In this article, we used the verification code method and refresh token method, as follows:

1. Token construct implementation class

① Verification code implementation class

/**
 * @author lichenhao
 * @date 2023/2/8 17:32
 */
@Component
public class CaptchaTokenGranter implements ITokenGranter {

    public static final String GRANT_TYPE = "captcha";

    @Autowired
    private SysLoginService loginService;

    @Override
    public AjaxResult grant(LoginBody loginBody) {
        String username = ();
        String code = ();
        String password = ();
        String uuid = ();
        Boolean forceLogoutFlag = ();

        AjaxResult ajaxResult = validateLoginBody(username, password, code, uuid);
        // Verification code        (username, code, uuid);
        // Log in        (username, password, uuid, forceLogoutFlag);
        // Delete the verification code        (uuid);
        return ajaxResult;
    }

    private AjaxResult validateLoginBody(String username, String password, String code, String uuid) {
        if ((username)) {
            return ("Username Required");
        }
        if ((password)) {
            return ("Password required");
        }
        if ((code)) {
            return ("Verification code required");
        }
        if ((uuid)) {
            return ("Uuid Required");
        }
        return ();
    }
}


    /**
      * Login verification
      *
      * @param username Username
      * @param password
      * @return Results
      */
    public void login(String username, String password, String uuid, Boolean forceLogoutFlag) {
        // Verify basic auth        IClientDetails iClientDetails = ();
        // User verification        Authentication authentication = null;
        try {
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
            (authenticationToken);
            // This method will be called            authentication = (authenticationToken);
        } catch (Exception e) {
            if (e instanceof BadCredentialsException) {
                ().execute((username, Constants.LOGIN_FAIL, ("")));
                throw new UserPasswordNotMatchException();
            } else {
                ().execute((username, Constants.LOGIN_FAIL, ()));
                throw new ServiceException(());
            }
        } finally {
            ();
        }
        LoginUser loginUser = (LoginUser) ();
        (loginUser);
        Long customerId = ().getCustomerId();
        Boolean singleClientFlag = ();
        if(customerId != null){
            Customer customer = (customerId);
            singleClientFlag = ();
            (("Customer【%s】On-account login limit switch: %s", (), singleClientFlag));
        }
        if(singleClientFlag){
            List&lt;SysUserOnline&gt; userOnlineList = (null, username);
            if((userOnlineList)){
                if(forceLogoutFlag != null &amp;&amp; forceLogoutFlag){
                    // Kick off other clients who use this account to log in                    (userOnlineList);
                }else{
                    throw new ServiceException("【" + username + "】Login, is it still logged in", 400);
                }
            }
        }
        // Generate tokens        (iClientDetails, loginUser, uuid);
        ().execute((username, Constants.LOGIN_SUCCESS, ("")));
        recordLoginInfo(());
    }

②Refresh the token method to implement the class

/**
 * @author lichenhao
 * @date 2023/2/8 17:35
 */
@Component
public class RefreshTokenGranter implements ITokenGranter {

    public static final String GRANT_TYPE = "refresh_token";

    @Autowired
    private TokenService tokenService;

    @Override
    public AjaxResult grant(LoginBody loginBody) {
        ();
        return ();
    }
}

2. Token related operations: setCookie

①createToken

    /**
      * Create a token
      * Note: access_token and refresh_token use the same tokenId
      */
    public void createToken(IClientDetails clientDetails, LoginUser loginUser, String tokenId) {

        if(loginUser == null){
            throw new ForbiddenException("The user information is invalid, please log in again!");
        }

        (tokenId);

        String username = ();
        String clientId = ();

        // Set the user information to carry by jwt        Map&lt;String, Object&gt; claimsMap = new HashMap&lt;&gt;();
        initClaimsMap(claimsMap, loginUser);

        long nowMillis = ();
        Date now = new Date(nowMillis);

        int accessTokenValidity = ();
        long accessTokenExpMillis = nowMillis + accessTokenValidity * MILLIS_SECOND;
        Date accessTokenExpDate = new Date(accessTokenExpMillis);
        String accessToken = createJwtToken(SecureConstant.ACCESS_TOKEN, accessTokenExpDate, now, JWT_TOKEN_SECRET, claimsMap, clientId, tokenId, username);

        int refreshTokenValidity = ();
        long refreshTokenExpMillis = nowMillis + refreshTokenValidity * MILLIS_SECOND;
        Date refreshTokenExpDate = new Date(refreshTokenExpMillis);
        String refreshToken = createJwtToken(SecureConstant.REFRESH_TOKEN, refreshTokenExpDate, now, JWT_REFRESH_TOKEN_SECRET, claimsMap, clientId, tokenId, username);

        // Write to cookie        HttpServletResponse response = ();
        (response, SecureConstant.ACCESS_TOKEN, accessToken, accessTokenValidity);
        (response, SecureConstant.REFRESH_TOKEN, refreshToken, refreshTokenValidity);

        //Insert the cache (expiration time is the longest expiration time = the expiration time of refresh_token. In theory, it will be refreshed all the time when the operation is maintained)        (nowMillis);
        (refreshTokenExpMillis);
        updateUserCache(loginUser);
    }

    private void initClaimsMap(Map&lt;String, Object&gt; claims, LoginUser loginUser) {
        // Add jwt custom parameters    }

    /**
      * Generate jwt token
      *
      * @param jwtTokenType token type: access_token, refresh_token
      * @param expDate token expiration date
      * @param now Current date
      * @param signKey Signature key
      * @param claimsMap jwt custom information (can carry additional user information)
      * @param clientId Application id
      * @param tokenId unique identifier of token (it is recommended to use one for the same group of access_token and refresh_token)
      * @param subject user ID issued by jwt
      * @return token string
      */
    private String createJwtToken(String jwtTokenType, Date expDate, Date now, String signKey, Map&lt;String, Object&gt; claimsMap, String clientId, String tokenId, String subject) {

        JwtBuilder jwtBuilder = ().setHeaderParam("typ", "JWT")
                .setId(tokenId)
                .setSubject(subject)
                .signWith(SignatureAlgorithm.HS512, signKey);

        //Set JWT parameters (user dimension)        (jwtBuilder::claim);

        //Set the application id        (SecureConstant.CLAIMS_CLIENT_ID, clientId);

        //Set token type        (SecureConstant.CLAIMS_TOKEN_TYPE, jwtTokenType);

        //Add token expiration time        (expDate).setNotBefore(now);
        return ();
    }

    /*
      * Update the cached user information
      * */
    public void updateUserCache(LoginUser loginUser) {
        //Cach loginUser according to tokenId        String userKey = getTokenKey(());
        (userKey, loginUser, parseIntByLong(() - ()), );
    }

    private String getTokenKey(String uuid) {
        return "login_tokens:" + uuid;
    }

②refreshToken

    /**
      * Refresh token validity period
      */
    public void refreshToken() {
        // Get refreshToken from cookies        String refreshToken = ((), SecureConstant.REFRESH_TOKEN);
        if ((refreshToken)) {
            throw new ForbiddenException("Certification failed!");
        }
        // Verify that refreshToken is valid        Claims claims = parseToken(refreshToken, JWT_REFRESH_TOKEN_SECRET);
        if (claims == null) {
            throw new ForbiddenException("Certification failed!");
        }
        String clientId = ((SecureConstant.CLAIMS_CLIENT_ID));
        String tokenId = ();
        LoginUser loginUser = getLoginUserByTokenId(tokenId);
        if(loginUser == null){
            throw new ForbiddenException("The user information is invalid, please log in again!");
        }
        IClientDetails clientDetails = getClientDetailsService().loadClientByClientId(clientId);
        // Delete the original token cache        delLoginUserCache(tokenId);
        // Regenerate token        createToken(clientDetails, loginUser, ());
    }

    /**
      * Get user information based on tokenId
      *
      * @return User Information
      */
    public LoginUser getLoginUserByTokenId(String tokenId) {
        String userKey = getTokenKey(tokenId);
        LoginUser user = (userKey);
        return user;
    }

    /**
      * Delete user cache
      */
    public void delLoginUserCache(String tokenId) {
        if ((tokenId)) {
            String userKey = getTokenKey(tokenId);
            (userKey);
        }
    }

③Exception code

  • 401: access_token is invalid, start refreshing token logic
  • 403: Refresh_token is invalid, or other scenarios that require redirecting to the login page

2. Front-end (vue3+axios)

// Create an axios instanceconst service = ({
    // The request in axios has a baseURL option, indicating that the public part of the request URL is    baseURL: .VITE_APP_BASE_API,
    // time out    timeout: 120000,
    withCredentials: true
})

// request interceptor(config =&gt; {
    // do something
    return config
}, error =&gt; {

})


// Response Interceptor(res =&gt; {
        loadingInstance?.close()
        loadingInstance = null
        // If the status code is not set, the default successful state        const code =  || 200;
        // Get error message        const msg = errorCode[code] ||  || errorCode['default']
        if (code === 500) {
            ElMessage({message: msg, type: 'error'})
            return (new Error(msg))
        } else if (code === 401) {
            return refreshFun();
        } else if (code === 601) {
            ElMessage({message: msg, type: 'warning'})
            return (new Error(msg))
        } else if (code == 400) {
            // Whether to force login by user confirm            return ()
        } else if (code !== 200) {
            ({title: msg})
            return ('error')
        } else {
            return ( === 'blob' ? res : )
        }
    },
    error =&gt; {
        loadingInstance?.close()
        loadingInstance = null
        if ( == 401) {
            return refreshFun();
        }
        let {message} = error;
        if (message == "Network Error") {
            message = "Backend interface connection exception";
        } else if (("timeout")) {
            message = "System interface request timeout";
        } else {
            message =  ?  : 'message'
        }
        ElMessage({message: message, type: 'error', duration: 5 * 1000})
        return (error)
    }
)

// Refreshing the logo to avoid repeated refreshlet refreshing = false;
// Request waiting queuelet waitQueue = [];

function refreshFun(config) {
    if (refreshing == false) {
        refreshing = true;
        return useUserStore().refreshToken().then(() =&gt; {
            (callback =&gt; callback()); // The token has been successfully refreshed, all requests in the queue are retryed            waitQueue = [];
            refreshing = false;
            return service(config)
        }).catch((err) =&gt; {
            waitQueue = [];
            refreshing = false;
            if () {
                if ( === 403) {
                    ('Login status has expired (authentication failed), you can continue to stay on this page, or log in again', 'System prompt', {
                        confirmButtonText: 'Re-Login',
                        cancelButtonText: 'Cancel',
                        type: 'warning'
                    }).then(() =&gt; {
                        useUserStore().logoutClear();
                        (`/login`);
                    }).catch(() =&gt; {

                    });
                    return ()
                } else {
                    ('err:' + ( &amp;&amp; ) ?  : err)
                }
            } else {
                ElMessage({
                    message: ,
                    type: 'error',
                    duration: 5 * 1000
                })
            }
        })
    } else {
        // Refreshing the token, returning the promise that has not been resolved, refreshing the token to execute the callback        return new Promise((resolve =&gt; {
            (() =&gt; {
                resolve(service(config))
            })
        }))
    }
}

Summarize

The above is personal experience. I hope you can give you a reference and I hope you can support me more.