雙 Token 無感刷新機制實現

?作者簡介:熱愛Java后端開發的一名學習者,大家可以跟我一起討論各種問題喔。
🍎個人主頁:Hhzzy99
🍊個人信條:堅持就是勝利!
💞當前專欄:項目實踐
🥭本文內容:實現雙Token的無感刷新。

雙 Token 無感刷新機制實現

      • 后端依賴
      • 安全配置
      • Jwt過濾器 *
      • 前端的配置

在現代 Web 應用開發中,前后端分離已經成為一種趨勢。Vue.js 作為前端框架,Java
作為后端語言的組合被廣泛應用。在用戶認證方面,JWT因為其無狀態、易于擴展等特點也備受青睞。本文將詳細介紹如何在 Vue 前端和 Java
后端實現雙 Token 的無感刷新機制。

后端依賴

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><!-- 其他依賴 -->
</dependencies>

安全配置

配置Jwt過濾器,以及認證失敗過濾器。

@Configuration
@EnableWebSecurity
public class SecurityConfig {/*** 認證失敗處理類*/@Autowiredprivate AuthenticationEntryPointImpl unauthorizedHandler;/*** Jwt過濾器*/@Autowiredprivate JwtAuthenticationFilter jwtAuthenticationFilter;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.csrf().disable().cors().and().addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class).headers().cacheControl().disable().and().exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().antMatchers("/user/login", "/user/forgetPassword/**", "/user/sendUpdatePasswordEmailCode/**", "/user/register", "/swagger-ui.html", "/user/sendEmailLoginCode", "/user/verifyEmailLoginCode/**").permitAll().antMatchers("/admin/**").hasRole("ADMIN").anyRequest().authenticated().and().headers().frameOptions().disable();return http.build();}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}// 使用BCryptPasswordEncoder作為security默認的passwordEncoder@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}

Jwt過濾器 *

這個過濾器是實現token刷新機制的核心,每次前端的請求攜帶accessToken與refreshToken過來,此過濾器拿到之后,先對accessToken進行解析,如果解析失敗(過期),那么接下來會對refreshToken進行解析,解析完成之后,如果沒有過期,就會生成新的accessToken與refreshToken返回給前端,并且設置一個新的請求頭Token-Refreshed,值可以隨便設,前端能拿到就好。

package com.hblog.backend.config;import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hblog.backend.entity.LoginUser;
import com.hblog.backend.entity.User;
import com.hblog.backend.exception.BusinessException;
import com.hblog.backend.exception.EnumException;
import com.hblog.backend.mapper.IUserMapper;
import com.hblog.backend.response.CommonResponse;
import com.hblog.backend.utli.*;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;/*** @ClassName: JwtAuthenticationFilter* @author: Hhzzy99* @date: 2024/3/17 16:09* description:繼承每個請求只會經過一次的過濾器*/
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;@Value("${token.expiration}")private Long expiration;@Autowiredprivate IUserMapper userMapper;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 獲取當前請求路徑String requestPath = request.getRequestURI();// 排除不需要過濾的路徑if (requestPath.equals("/user/login") || requestPath.equals("/user/register")) {filterChain.doFilter(request, response);return;}// 獲取tokenString accessToken = request.getHeader("access_token");String refreshToken = request.getHeader("refresh_token");if ("null".equals(accessToken) || "".equals(accessToken) || "undefined".equals(accessToken) || null == accessToken) {// 放行filterChain.doFilter(request, response);return;}// 解析tokenString userId = "";boolean isRefresh = false;try {userId = JwtUtils.parseJWT(accessToken).getSubject();} catch (Exception e) {isRefresh = true;e.printStackTrace();}if (isRefresh) {try {userId = JwtUtils.parseJWT(refreshToken).getSubject();accessToken = JwtUtils.createJWT(userId);refreshToken = JwtUtils.createRefreshToken(userId);User loginUser = userMapper.getUserById(Long.valueOf(userId));Integer ttl = expiration.intValue() / 1000;log.warn("@@@@@@@@@@@@@@@@@@刷新token@@@@@@@@@@@@@@@@@@@@@");redisCache.setCacheObject("userInfo:" + userId, loginUser, ttl, TimeUnit.SECONDS);writeTokenResponse(response, accessToken, refreshToken);return;} catch (Exception e1) {throw new BusinessException(EnumException.THE_LOGIN_HAS_EXPIRED);}}// 從redis里面獲取用戶信息User loginUser = redisCache.getCacheObject("userInfo:" + userId);if (Objects.isNull(loginUser)) {throw new BusinessException(EnumException.THE_LOGIN_HAS_EXPIRED);}// 存入SecurityContextHolderUsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);// 放行filterChain.doFilter(request, response);}private void writeTokenResponse(HttpServletResponse response, String accessToken, String refreshToken) throws IOException {Map<String, String> tokenMap = new HashMap<>();tokenMap.put("accessToken", accessToken);tokenMap.put("refreshToken", refreshToken);Map<String, String> headers = new HashMap<>();headers.put("Token-Refreshed", "true");CommonResponse<Map<String, String>> commonResponse = new CommonResponse<>(200, "Token refreshed successfully", tokenMap);WebUtil.renderString(response, headers, JSON.toJSONString(commonResponse));}
}

前端的配置

前端對所有的axios請求進行全局配置,先在每次請求的時候設置好請求頭accessToken與refreshToken,并且將每次請求都保存起來,如果在請求時后端解析到accessToken失效,并且返回了新的accessToken與refreshToken,在請求頭拿到了后端設置好的Token-Refreshed,此時就可以重新將新的accessToken與refreshToken保存在瀏覽器本地,并且重新發送之前保存好的請求,就可以實現無感刷新。

request.js

import axios from 'axios'
import {ref} from "vue";// create an axios instance
const service = axios.create({baseURL: '/api', // url = base url + request urltimeout: 20000 // request timeout
})const retryRequest = ref(null)// request interceptor
service.interceptors.request.use(config => {// 加入頭信息配置if (localStorage.getItem("access_token") !== null && localStorage.getItem("access_token") !== undefined){config.headers['access_token'] = localStorage.getItem("access_token")}if (localStorage.getItem("refresh_token") !== null && localStorage.getItem("refresh_token") !== undefined){config.headers['refresh_token'] = localStorage.getItem("refresh_token")}retryRequest.value = configreturn config}
)// response interceptor
service.interceptors.response.use(response => {if (response.headers['token-refreshed']) {console.log('Token刷新成功');// 如果有Token-Refreshed頭部,更新本地存儲中的TokenlocalStorage.setItem('access_token', response.data.data.accessToken);localStorage.setItem('refresh_token', response.data.data.refreshToken);console.log("繼續")// 繼續發送原始請求return axios(retryRequest.value)}return response;},async error => {const originalRequest = error.config;// 如果是Token過期導致的401錯誤,并且沒有retry標記,嘗試刷新Tokenif (error.response.status === 401 && !originalRequest._retry) {originalRequest._retry = true;const refreshToken = localStorage.getItem('refresh_token');if (refreshToken) {try {const response = await axios.post('/refresh-token', { refreshToken });const { accessToken, refreshToken: newRefreshToken } = response.data.data;localStorage.setItem('access_token', accessToken);localStorage.setItem('refresh_token', newRefreshToken);// 更新原始請求的Authorization頭部originalRequest.headers['access_token'] = accessToken;// 重新發送原始請求return instance(originalRequest);} catch (refreshError) {// 刷新Token失敗,跳轉到登錄頁或執行其他處理console.error('Token刷新失敗:', refreshError);// 這里可以跳轉到登錄頁或者執行其他處理}}}return Promise.reject(error);}
)export default service

通過以上步驟,我們就可以實現雙 Token 無感刷新機制。該機制通過短期有效的訪問 Token 和長期有效的刷新 Token 相結合,在 Token 過期時自動刷新。

本示例僅展示了基礎的實現方式,實際生產環境中還需要考慮更多安全性和健壯性的問題。

希望這篇文章能幫助你更好地理解和實現 JWT 雙 Token 無感刷新機制。如有任何問題或建議,歡迎在評論區留言討論。

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

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

相關文章

微信小程序性能與體驗優化

1. 合理的設置可點擊元素的響應區域大小&#xff1b; 比較常見的是頁面的點擊按鈕太小&#xff0c;用戶點擊不到按鈕&#xff0c;這樣用戶體驗很不好。 2. 避免渲染頁面耗時過長&#xff1b; 當頁面渲染時間過長的話&#xff0c;會讓用戶感覺非常卡頓&#xff0c;當出現這種…

密室逃脫——收集版修改測試

一、原版修改 1、導入資源 Unity Learn | 3D Beginner: Complete Project | URP 2、設置Scene 刪除SampleScene&#xff0c;打開UnityTechnologies-3DBeginnerComplete下的MainScene 3、降低音量 (1) 打開Hierarchy面板上的Audio降低音量 (2) 打開Prefabs文件夾&#xf…

lnmp php7 安裝ssh2擴展

安裝ssh2擴展前必須安裝libssh2包 下載地址: wget http://www.libssh2.org/download/libssh2-1.11.0.tar.gzwget http://pecl.php.net/get/ssh2-1.4.tgz &#xff08;這里要換成最新的版本&#xff09; 先安裝 libssh2 再安裝 SSH2: tar -zxvf libssh2-1.11.0.tar.gzcd libss…

若依框架(RuoYi)中實現部門及子部門用戶查詢的SQL邏輯解析

前言 在基于若依框架&#xff08;RuoYi&#xff09;的項目開發中&#xff0c;經常會遇到需要根據部門ID查詢其下屬所有用戶的需求&#xff0c;包括直接隸屬于該部門的用戶以及屬于其子部門的所有用戶。這一需求在組織架構管理、權限分配等場景中尤為常見。本文將深入解析一段典…

【深入理解計算機系統——2信息的表示和處理】

計算機的本質就是二進制&#xff0c;0/1&#xff0c;稱之為bit&#xff08;位&#xff09;&#xff0c;一個位沒有什么意義&#xff0c;當同時擁有多個位&#xff0c;并且加上某種解釋&#xff0c;就可以表示任何有限集合的元素。&#xff08;為什么是有限&#xff1f;因為用bi…

【日志信息管理】管理日志信息的類

日志用于記錄程序的執行記錄包括程序的出錯記錄&#xff0c;程序致命退出原因&#xff0c;程序的正常執行記錄。這樣我們就可以很快的察覺程序的錯誤原因、執行狀況等等&#xff0c;因此管理日志信息是非常重要的。 日志一般由以下部分組合&#xff1a; 日志時間、日志等級、…

Java 基礎--File - IO流(2)

I/O流 定義 數據從硬盤流向內存為輸入流&#xff0c;數據從內存流向硬盤為輸出流。輸入也叫讀取數據&#xff0c;輸出也叫寫出數據。 IO分類 1.按照數據的流向分為&#xff1a;輸入流和輸出流 ①輸入流&#xff1a;把數據從其他設備上讀取到內存中的流 ②輸出流&#xff1…

Qt 基礎組件速學 事件過濾器

學習目標&#xff1a;理解事件過濾器 前置環境 運行環境:qt creator 4.12 學習內容和效果演示&#xff1a; Qt 提供了事件過濾器的機制,允許我們在事件到達目標對象之前對事件進行攔截和處理。這在以下情況下非常有用: 全局事件處理: 我們可以在應用程序級別安裝一個事件過…

工控人最愛的PLC觸摸屏一體機,有多香

PLC觸摸屏一體機是什么 PLC觸摸屏一體機&#xff0c;聽起來可能有點技術化&#xff0c;但簡單來說&#xff0c;它就是一個集成了可編程邏輯控制器&#xff08;PLC&#xff09;和觸摸屏的智能設備。這種設備不僅能夠執行自動化控制任務&#xff0c;還能實時顯示和操作設備狀態&a…

JVM原理(十九):JVM虛擬機內存模型

1. 硬件的效率與一致性 數據不安全的原因&#xff1a;緩存一致性的問題 共享內存多核系統&#xff1a;在多路處理器系統中&#xff0c;每個處理器都有自己的高速緩存&#xff0c;而他們又共享同一主內存。 線程先后執行結果不一致問題&#xff1a;除了增加高速緩存之外&#…

【Python】已解決:nltk.download(‘stopwords‘) 報錯問題

文章目錄 一、分析問題背景二、可能出錯的原因三、錯誤代碼示例四、正確代碼示例五、注意事項 已解決&#xff1a;nltk.download(‘stopwords’) 報錯問題 一、分析問題背景 在使用Python的自然語言處理庫NLTK&#xff08;Natural Language Toolkit&#xff09;時&#xff0c…

后端開發常見錯誤

1、解析json字符串要考慮格式不正確&#xff0c;空值情況 2、解析時間字符串要考虎格式和空值 3、使用mybatis的foreach的時候要考慮拼接sql的耗時&#xff0c;尤其是超過10條數據 4、表字段長度&#xff0c;在接口層校驗字段長度&#xff0c; 調用三方系統的報錯要截取報錯…

CentOS 7安裝Elasticsearch7.7.0和Kibana

一. 準備安裝包 elasticsearch和kibana&#xff1a;官網歷史版本找到并下載&#xff08;https://www.elastic.co/cn/downloads/past-releases#elasticsearch&#xff09;ik分詞器&#xff1a;GitHub下載&#xff08;https://github.com/infinilabs/analysis-ik/releases/tag/v…

【大模型】衡量巨獸:解讀評估LLM性能的關鍵技術指標

衡量巨獸&#xff1a;解讀評估LLM性能的關鍵技術指標 引言一、困惑度&#xff1a;語言模型的試金石1.1 定義與原理1.2 計算公式1.3 應用與意義 二、BLEU 分數&#xff1a;翻譯質量的標尺2.1 定義與原理2.2 計算方法2.3 應用與意義 三、其他評估指標&#xff1a;綜合考量下的多元…

設計模式之狀態機模式

一、狀態機模式介紹 狀態機模式&#xff08;State Machine Pattern&#xff09;是一種用于描述對象行為的軟件設計模式&#xff0c;屬于行為型設計模式。在狀態機模式中&#xff0c;對象的行為取決于其內部狀態&#xff0c;并且在不同的狀態下&#xff0c;對象可能會有不同的行…

STM32F103C8T6核心板原理圖和PCB分享

PCB圖 原理圖 資料下載地址&#xff1a; 原理圖PCB庫: https://545c.com/d/45573183-61875742-29897c?p7526 (訪問密碼: 7526)

[go-zero] 簡單微服務調用

文章目錄 1.注意事項2.服務劃分及創建2.1 用戶微服務2.2 訂單微服務 3.啟動服務3.1 etcd 服務啟動3.2 微服務啟動3.3 測試訪問 1.注意事項 go-zero微服務的注冊中心默認使用的是Etcd。 本小節將以一個訂單服務調用用戶服務來簡單演示一下&#xff0c;其實訂單服務是api服務&a…

Java 使用sql查詢mongodb

在現代應用開發中&#xff0c;關系型數據庫和NoSQL數據庫各有千秋。MongoDB作為一種流行的NoSQL數據庫&#xff0c;以其靈活的文檔模型和強大的擴展能力&#xff0c;受到廣泛歡迎。然而&#xff0c;有時開發者可能更熟悉SQL查詢語法&#xff0c;或者需要在現有系統中復用SQL查詢…

【ARMv8/v9 GIC 系列 5.6 -- GIC 超優先級中斷詳細介紹】

請閱讀【ARM GICv3/v4 實戰學習 】 文章目錄 Interrupt superpriority超優先級中斷的特性和應用Physical interface interrupt signalsPhysical Group 1 Non-NMI for Current Security StatePhysical Group 1 for Other Security State, or a Group 0 Non-NMIPhysical Group 1 …

進程控制-wait和waitpid進程回收

wait 阻塞函數 函數作用&#xff1a; 1. 阻塞并等待子進程退出 2. 回收子進程殘留資源 3. 獲取子進程結束狀態&#xff08;退出原因&#xff09; pid_t wait(int *wstatus); 返回值&#xff1a; ‐1 : 回收失敗&#xff0c;已經沒有子進程了 >0 : 回收子進程對應的…