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<String, ITokenGranter> GRANTER_POOL = new ConcurrentHashMap<>(); 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<SysUserOnline> userOnlineList = (null, username); if((userOnlineList)){ if(forceLogoutFlag != null && 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<String, Object> claimsMap = new HashMap<>(); 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<String, Object> 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<String, Object> 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 => { // do something return config }, error => { }) // Response Interceptor(res => { 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 => { 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(() => { (callback => callback()); // The token has been successfully refreshed, all requests in the queue are retryed waitQueue = []; refreshing = false; return service(config) }).catch((err) => { 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(() => { useUserStore().logoutClear(); (`/login`); }).catch(() => { }); return () } else { ('err:' + ( && ) ? : 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 => { (() => { 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.