springboot+vue3無感知刷新token實戰

目錄

一、java后端

1、token構造實現類

①驗證碼方式實現類

②刷新token方式實現類

?2、token相關操作:setCookie

①createToken

②refreshToken

二、前端(vue3+axios)


????????web網站中,前后端交互時,通常使用token機制來做認證,token一般會設置有效期,當token過了有效期后,用戶需要重新登錄授權獲取新的token,但是某些業務場景下,用戶不希望頻繁的進行登錄授權,但是安全考慮,token的有效期不能設置太長時間,所以有了刷新token的設計,無感知刷新token的機制更進一步優化了用戶體驗,本文是博主實際業務項目中基于springboot和vue3無感知刷新token的代碼實戰。

首先介紹無感知刷新token的實現思路:

①首次授權頒發token時,我們通過后端給前端請求response中寫入兩種cookie

? ? ? ? - access_token

? ? ? ? - refresh_token(超時時間比access_token長一些)

需要注意:

????????-后端setCookie時httpOnly=true(限制cookie只能被http請求攜帶使用,不能被js操作)

? ? ? ? -前端axios請求參數withCredentials=true(http請求時,自動攜帶token)

②access_token失效時,拋出特殊異常,前后端約定http響應碼(401),此時觸發刷新token邏輯

③前段http請求鉤子中,如果出現http響應碼為401時,立即觸發刷新token邏輯,同時緩存后續請求,刷新token結束后,依次續發緩存中的請求

一、java后端

后端java框架使用springboot,spring-security

登錄接口:?

/*** @author lichenhao* @date 2023/2/8 17:41*/
@RestController
public class AuthController {/*** 登錄方法** @param loginBody 登錄信息* @return 結果*/@PostMapping("/oauth")public AjaxResult login(@RequestBody LoginBody loginBody) {ITokenGranter granter = TokenGranterBuilder.getGranter(loginBody.getGrantType());return granter.grant(loginBody);}
}import lombok.Data;/*** 用戶登錄對象** @author lichenhao*/
@Data
public class LoginBody {/*** 用戶名*/private String username;/*** 用戶密碼*/private String password;/*** 驗證碼*/private String code;/*** 唯一標識*/private String uuid;/** grantType 授權類型* */private String grantType;/** 是否直接強退該賬號登陸的其他客戶端* */private Boolean forceLogoutFlag;
}

token構造接口類和token實現類構造器如下:

/*** @author lichenhao* @date 2023/2/8 17:29* <p>* 獲取token*/
public interface ITokenGranter {AjaxResult grant(LoginBody loginBody);
}/*** @author lichenhao* @date 2023/2/8 17:29*/
@AllArgsConstructor
public class TokenGranterBuilder {/*** TokenGranter緩存池*/private static final Map<String, ITokenGranter> GRANTER_POOL = new ConcurrentHashMap<>();static {GRANTER_POOL.put(CaptchaTokenGranter.GRANT_TYPE, SpringUtils.getBean(CaptchaTokenGranter.class));GRANTER_POOL.put(RefreshTokenGranter.GRANT_TYPE, SpringUtils.getBean(RefreshTokenGranter.class));}/*** 獲取TokenGranter** @param grantType 授權類型* @return ITokenGranter*/public static ITokenGranter getGranter(String grantType) {ITokenGranter tokenGranter = GRANTER_POOL.get(StringUtils.toStr(grantType, PasswordTokenGranter.GRANT_TYPE));if (tokenGranter == null) {throw new ServiceException("no grantType was found");} else {return tokenGranter;}}}

這里通過LoginBody的grantType屬性,指定實際的token構造實現類;同時,需要有token

本文我們用到了驗證碼方式和刷新token方式,如下

1、token構造實現類

①驗證碼方式實現類

/*** @author lichenhao* @date 2023/2/8 17:32*/
@Component
public class CaptchaTokenGranter implements ITokenGranter {public static final String GRANT_TYPE = "captcha";@Autowiredprivate SysLoginService loginService;@Overridepublic AjaxResult grant(LoginBody loginBody) {String username = loginBody.getUsername();String code = loginBody.getCode();String password = loginBody.getPassword();String uuid = loginBody.getUuid();Boolean forceLogoutFlag = loginBody.getForceLogoutFlag();AjaxResult ajaxResult = validateLoginBody(username, password, code, uuid);// 驗證碼loginService.validateCaptcha(username, code, uuid);// 登錄loginService.login(username, password, uuid, forceLogoutFlag);// 刪除驗證碼loginService.deleteCaptcha(uuid);return ajaxResult;}private AjaxResult validateLoginBody(String username, String password, String code, String uuid) {if (StringUtils.isBlank(username)) {return AjaxResult.error("用戶名必填");}if (StringUtils.isBlank(password)) {return AjaxResult.error("密碼必填");}if (StringUtils.isBlank(code)) {return AjaxResult.error("驗證碼必填");}if (StringUtils.isBlank(uuid)) {return AjaxResult.error("uuid必填");}return AjaxResult.success();}
}/*** 登錄驗證** @param username 用戶名* @param password 密碼* @return 結果*/public void login(String username, String password, String uuid, Boolean forceLogoutFlag) {// 校驗basic authIClientDetails iClientDetails = tokenService.validBasicAuth();// 用戶驗證Authentication authentication = null;try {UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);AuthenticationContextHolder.setContext(authenticationToken);// 該方法會去調用UserDetailsServiceImpl.loadUserByUsernameauthentication = authenticationManager.authenticate(authenticationToken);} catch (Exception e) {if (e instanceof BadCredentialsException) {AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();} else {AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));throw new ServiceException(e.getMessage());}} finally {AuthenticationContextHolder.clearContext();}LoginUser loginUser = (LoginUser) authentication.getPrincipal();tokenService.setUserAgent(loginUser);Long customerId = loginUser.getUser().getCustomerId();Boolean singleClientFlag = SystemConfig.isSingleClientFlag();if(customerId != null){Customer customer = customerService.selectCustomerById(customerId);singleClientFlag = customer.getSingleClientFlag();log.info(String.format("客戶【%s】單賬號登錄限制開關:%s", customer.getCode(), singleClientFlag));}if(singleClientFlag){List<SysUserOnline> userOnlineList = userOnlineService.getUserOnlineList(null, username);if(CollectionUtils.isNotEmpty(userOnlineList)){if(forceLogoutFlag != null && forceLogoutFlag){// 踢掉其他使用該賬號登陸的客戶端userOnlineService.forceLogoutBySysUserOnlineList(userOnlineList);}else{throw new ServiceException("【" + username + "】已登錄,是否仍然登陸", 400);}}}// 生成tokentokenService.createToken(iClientDetails, loginUser, uuid);AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));recordLoginInfo(loginUser.getUserId());}

②刷新token方式實現類

/*** @author lichenhao* @date 2023/2/8 17:35*/
@Component
public class RefreshTokenGranter implements ITokenGranter {public static final String GRANT_TYPE = "refresh_token";@Autowiredprivate TokenService tokenService;@Overridepublic AjaxResult grant(LoginBody loginBody) {tokenService.refreshToken();return AjaxResult.success();}
}

?2、token相關操作:setCookie

①createToken

    /*** 創建令牌* 注意:access_token和refresh_token 使用同一個tokenId*/public void createToken(IClientDetails clientDetails, LoginUser loginUser, String tokenId) {if(loginUser == null){throw new ForbiddenException("用戶信息無效,請重新登陸!");}loginUser.setTokenId(tokenId);String username = loginUser.getUsername();String clientId = clientDetails.getClientId();// 設置jwt要攜帶的用戶信息Map<String, Object> claimsMap = new HashMap<>();initClaimsMap(claimsMap, loginUser);long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);int accessTokenValidity = clientDetails.getAccessTokenValidity();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 = clientDetails.getRefreshTokenValidity();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);// 寫入cookie中HttpServletResponse response = ServletUtils.getResponse();WebUtil.setCookie(response, SecureConstant.ACCESS_TOKEN, accessToken, accessTokenValidity);WebUtil.setCookie(response, SecureConstant.REFRESH_TOKEN, refreshToken, refreshTokenValidity);//插入緩存(過期時間為最長過期時間=refresh_token的過期時間 理論上,保持操作的情況下,一直會被刷新)loginUser.setLoginTime(nowMillis);loginUser.setExpireTime(refreshTokenExpMillis);updateUserCache(loginUser);}private void initClaimsMap(Map<String, Object> claims, LoginUser loginUser) {// 添加jwt自定義參數}/*** 生成jwt token** @param jwtTokenType token類型:access_token、refresh_token* @param expDate      token過期日期* @param now          當前日期* @param signKey      簽名key* @param claimsMap    jwt自定義信息(可攜帶額外的用戶信息)* @param clientId     應用id* @param tokenId      token的唯一標識(建議同一組 access_token、refresh_token 使用一個)* @param subject      jwt下發的用戶標識* @return token字符串*/private String createJwtToken(String jwtTokenType, Date expDate, Date now, String signKey, Map<String, Object> claimsMap, String clientId, String tokenId, String subject) {JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("typ", "JWT").setId(tokenId).setSubject(subject).signWith(SignatureAlgorithm.HS512, signKey);//設置JWT參數(user維度)claimsMap.forEach(jwtBuilder::claim);//設置應用idjwtBuilder.claim(SecureConstant.CLAIMS_CLIENT_ID, clientId);//設置token typejwtBuilder.claim(SecureConstant.CLAIMS_TOKEN_TYPE, jwtTokenType);//添加Token過期時間jwtBuilder.setExpiration(expDate).setNotBefore(now);return jwtBuilder.compact();}/** 更新緩存中的用戶信息* */public void updateUserCache(LoginUser loginUser) {// 根據tokenId將loginUser緩存String userKey = getTokenKey(loginUser.getTokenId());redisService.setCacheObject(userKey, loginUser, parseIntByLong(loginUser.getExpireTime() - loginUser.getLoginTime()), TimeUnit.MILLISECONDS);}private String getTokenKey(String uuid) {return "login_tokens:" + uuid;}

②refreshToken

    /*** 刷新令牌有效期*/public void refreshToken() {// 從cookie中拿到refreshTokenString refreshToken = WebUtil.getCookieVal(ServletUtils.getRequest(), SecureConstant.REFRESH_TOKEN);if (StringUtils.isBlank(refreshToken)) {throw new ForbiddenException("認證失敗!");}// 驗證 refreshToken 是否有效Claims claims = parseToken(refreshToken, JWT_REFRESH_TOKEN_SECRET);if (claims == null) {throw new ForbiddenException("認證失敗!");}String clientId = StringUtils.toStr(claims.get(SecureConstant.CLAIMS_CLIENT_ID));String tokenId = claims.getId();LoginUser loginUser = getLoginUserByTokenId(tokenId);if(loginUser == null){throw new ForbiddenException("用戶信息無效,請重新登陸!");}IClientDetails clientDetails = getClientDetailsService().loadClientByClientId(clientId);// 刪除原token緩存delLoginUserCache(tokenId);// 重新生成tokencreateToken(clientDetails, loginUser, IdUtils.simpleUUID());}/*** 根據tokenId獲取用戶信息** @return 用戶信息*/public LoginUser getLoginUserByTokenId(String tokenId) {String userKey = getTokenKey(tokenId);LoginUser user = redisService.getCacheObject(userKey);return user;}/*** 刪除用戶緩存*/public void delLoginUserCache(String tokenId) {if (StringUtils.isNotEmpty(tokenId)) {String userKey = getTokenKey(tokenId);redisService.deleteObject(userKey);}}

③異常碼

?401:access_token無效,開始刷新token邏輯

403:refresh_token無效,或者其他需要跳轉登錄頁面的場景

二、前端(vue3+axios)

// 創建axios實例
const service = axios.create({// axios中請求配置有baseURL選項,表示請求URL公共部分baseURL: import.meta.env.VITE_APP_BASE_API,// 超時timeout: 120000,withCredentials: true
})// request攔截器
service.interceptors.request.use(config => {// do somethingreturn config
}, error => {})// 響應攔截器
service.interceptors.response.use(res => {loadingInstance?.close()loadingInstance = null// 未設置狀態碼則默認成功狀態const code = res.data.code || 200;// 獲取錯誤信息const msg = errorCode[code] || res.data.msg || errorCode['default']if (code === 500) {ElMessage({message: msg, type: 'error'})return Promise.reject(new Error(msg))} else if (code === 401) {return refreshFun(res.config);} else if (code === 601) {ElMessage({message: msg, type: 'warning'})return Promise.reject(new Error(msg))} else if (code == 400) {// 需要用戶confirm是否強制登陸return Promise.resolve(res.data)} else if (code !== 200) {ElNotification.error({title: msg})return Promise.reject('error')} else {return Promise.resolve(res.request.responseType === 'blob' ? res : res.data)}},error => {loadingInstance?.close()loadingInstance = nullif (error.response.status == 401) {return refreshFun(error.config);}let {message} = error;if (message == "Network Error") {message = "后端接口連接異常";} else if (message.includes("timeout")) {message = "系統接口請求超時";} else {message = error.response.data ? error.response.data.msg : 'message'}ElMessage({message: message, type: 'error', duration: 5 * 1000})return Promise.reject(error)}
)// 正在刷新標識,避免重復刷新
let refreshing = false;
// 請求等待隊列
let waitQueue = [];function refreshFun(config) {if (refreshing == false) {refreshing = true;return useUserStore().refreshToken().then(() => {waitQueue.forEach(callback => callback()); // 已成功刷新token,隊列中的所有請求重試waitQueue = [];refreshing = false;return service(config)}).catch((err) => {waitQueue = [];refreshing = false;if (err.response) {if (err.response.status === 403) {ElMessageBox.confirm('登錄狀態已過期(認證失敗),您可以繼續留在該頁面,或者重新登錄', '系統提示', {confirmButtonText: '重新登錄',cancelButtonText: '取消',type: 'warning'}).then(() => {useUserStore().logoutClear();router.push(`/login`);}).catch(() => {});return Promise.reject()} else {console.log('err:' + (err.response && err.response.data.msg) ? err.response.data.msg : err)}} else {ElMessage({message: err.message,type: 'error',duration: 5 * 1000})}})} else {// 正在刷新token,返回未執行resolve的Promise,刷新token執行回調return new Promise((resolve => {waitQueue.push(() => {resolve(service(config))})}))}
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/35531.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/35531.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/35531.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

全球最快的 JSON 文件查詢工具

本文字數&#xff1a;1684&#xff1b;估計閱讀時間&#xff1a;5分鐘 審校&#xff1a;莊曉東&#xff08;魏莊&#xff09; 本文在公眾號【ClickHouseInc】首發 介紹 在 ClickHouse&#xff0c;我們熱衷于基準測試和性能優化。所以當我第一次看到 Hacker News 上那篇“查詢大…

代碼隨想錄算法訓練營day31|134.加油站、135. 分發糖果、406.根據身高重建隊列

134.加油站 如下圖所示&#xff1a; 當索引一道2的時候&#xff0c;剩余油量的總量13-6 < 0&#xff0c;這個時候說明以索引0為起點不合適&#xff0c;將起點更新為索引3. 兩點證明&#xff1a; 1.如果我們從藍色段中間選一個點開始&#xff0c;是不是最后sumGas就不小于0…

從靈感到成品:使用AI生成博客文章的完整指南

在信息爆炸的時代&#xff0c;每個人都有講述自己故事的權利和需求。博客作為一種表達方式&#xff0c;不僅能記錄個人經歷&#xff0c;還能分享知識和觀點。然而&#xff0c;許多人在寫博客文章時&#xff0c;常常會遇到靈感枯竭、時間不夠用或者不知道如何開始等問題。幸運的…

光伏儲能系統/安科瑞DTSD1352-CF雙向計量表-安科瑞 蔣靜

1 長期以來&#xff0c;我國施行居民用電低價政策&#xff0c;居民電價大幅低于供電成本&#xff0c;雖然實施了全天分三時段的階梯電價政策&#xff0c;但過去近10年中高峰節電的效果卻不夠明顯。從分時用電運作機制來看&#xff0c;居民用電價格的波動幅度不大&#xff0c;但…

華為云鯤鵬架構docker部署2048小游戲

華為云鯤鵬架構docker部署2048小游戲 1. 鯤鵬架構ESC2. 配置docker3. 上傳2048鏡像4. 刪除容器,鏡像 1. 鯤鵬架構ESC 2. 配置docker 安裝dockeryum -y install docker開機啟動 systemctl enable docker啟動docker服務 systemctl start docker查詢docker的運行版本 docker -v3…

時序分析基本概念介紹——min pulse width 最小脈沖寬度

文章目錄 前言一、什么是 min pulse width&#xff1f;二、為什么檢查 min pulse width&#xff1f;三、如何設置 min pulse width約束&#xff1f;1. 在sdc里面定義2. library里面定義 四、如何檢查 min pulse width&#xff1f;五、如何修復 min pulse width&#xff1f;總結…

docker啟動ws-scrcpy和redroid記錄

git克隆最新的ws-scrcpy代碼 git clone gitgithub.com:NetrisTV/ws-scrcpy.git進入ws-scrcpy目錄新建Dockerfile文件&#xff0c;內容如下 FROM node:16-alpine WORKDIR /appRUN npm config set registry http://mirrors.cloud.tencent.com/npm/ RUN npm install -g node-gyp…

攻防世界-Web題目1

目錄 cookie 1、題目 2、知識點 3、思路 get_post 1、題目 2、知識點 3、思路 disabled_button 1、題目 2、知識點 3、思路 backup 1、題目 2、知識點 3、思路 cookie 1、題目 2、知識點 cookie&#xff0c;數據包 3、思路 題目提示我們cookie&#xff0c;抓…

Markdown中如何插入空行和空格

Markdown 是一種輕量級的標記語言&#xff0c;它的主要目標是以易讀易寫為優先&#xff0c;并兼容 HTML。雖然 Markdown 本身對于排版的要求比較寬松&#xff0c;但在某些情況下&#xff0c;我們可能需要在文檔中插入空行或空格來達到特定的排版效果。 插入空行 在Markdown中…

【ai】trition:tritonclient.utils.shared_memory 僅支持linux

Can’t find tritonclient.utils.shared_memory on WIN10 #4149yolov4的python客戶端 導入以后,windows 的pycharm 就是看不到折騰了很久:SaviorEnv 環境下安裝tritonclient[all]也會失敗 (base) C:\Users\zhangbin>conda create -n SaviorEnv python=3.8 Collecting pack…

ubuntu 18 虛擬機安裝(1)

ubuntu 18 虛擬機安裝 ubuntu 18.04.6 Ubuntu 18.04.6 LTS (Bionic Beaver) https://releases.ubuntu.com/bionic/ 參考&#xff1a; 設置固定IP地址 https://blog.csdn.net/wowocpp/article/details/126160428 https://www.jianshu.com/p/1d133c0dec9d ubuntu-18.04.6-l…

元數據管理的發展歷程你了解嗎?元數據管理要克服哪些挑戰?

在當今的信息化時代&#xff0c;數據的價值已被廣泛認可&#xff0c;而元數據作為描述數據的數據&#xff0c;其作用日益凸顯。元數據管理&#xff0c;作為確保數據質量、促進數據共享和提高數據透明度的關鍵環節&#xff0c;對企業的數據戰略至關重要。隨著技術的發展&#xf…

程序設計中對內存分配管理的思考,進程內存、線程內存、共享池、棧、堆

設計一個程序&#xff0c;要考慮如何分配和管理內存&#xff0c;以下是對所有內存分配和管理類型的總結。 第一、進程級的內存資源&#xff0c;也叫全局靜態內存&#xff0c;其生命周期是伴隨整個進程的運行期間&#xff0c;可以用作在進程范圍內共享數據的方法。對應于C語言的…

C#1.0-11.0所有歷史版本主要特性總結

文章目錄 前言名詞解釋主要版本一覽表各版本主要特性一句話總結 C# 1.0 (Visual Studio 2002, .Net Framework 1.0)C# 2.0 (Visual Studio 2005, .Net Framework 2.0)C# 3.0 (Visual Studio 2008, .Net Framework 3.0)C# 4.0 (Visual Studio 2010, .Net Framework 4)C# 5.0 (V…

Bigram 分詞學習

Bigram 分詞&#xff1a;概念、應用與中文實踐 Bigram 分詞是一種基礎而有效的文本處理技術&#xff0c;特別是在自然語言處理中有著廣泛的應用。本文將詳細解釋 Bigram 分詞的概念、它在各個領域的應用&#xff0c;以及在中文處理中的獨特優勢和實踐。 什么是 Bigram 分詞&a…

DataWhale - 吃瓜教程學習筆記(三)

學習視頻&#xff1a;第3章-對數幾率回歸_嗶哩嗶哩_bilibili 西瓜書對應章節&#xff1a; 3.3 對數幾率回歸 sigmoid函數 極大似然估計建模 信息論 以概率論、隨機過程為基本研究工具&#xff0c;研究廣義通信系統的整個過程 - 信息熵 &#xff08;信息期望&#xff09; 度…

Windows bat 提取多個目錄下的文件,到一個目錄

批處理命令 echo off setlocalrem 設置源目錄和目標目錄 set "sourceDirE:\motrix" set "targetDirE:\新建文件夾"rem 創建目標目錄&#xff0c;如果不存在 if not exist "%targetDir%" mkdir "%targetDir%"rem 循環遍歷源目錄中的所…

TCP:TCP連接的建立與終止

TCP連接的建立與終止 建立連接第一次握手第二次握手第三次握手 終止連接第一次揮手第二次揮手第三次揮手第四次揮手 T C P是一個面向連接的協議。無論哪一方向另一方發送數據之前&#xff0c;都必須先在雙方之間建立一條連接。本文將詳細討論一個T C P連接是如何建立的以及通信…

2024年能源電力行業CRM研究報告

中國能源電力行業屬于大制造業的重要組成部分&#xff0c;在國民經濟中的地位舉足輕重。據統計&#xff0c;近十年來能源電力行業的整體投資呈現出增長趨勢&#xff0c;尤其是“十四五”期間增長顯著&#xff0c;2022年全國主要電力企業共完成投資12470億元&#xff0c;同比增長…

STM32中掛在APB1(低速)和APB2(高速)上的外設

在STM32中&#xff0c; 連接在APB1(低速外設)上的設備有&#xff1a; 電源接口、備份接口、CAN、USB、I2C1、I2C2、UART2、UART3、SPI2、窗口看門狗、Timer2、Timer3、Timer4 。 連接在APB2(高速外設)上的設備有&#xff1a; GPIO_A-E、USART1、ADC1、ADC2、ADC3、TIM1、TIM…