雙Token無(wú)感刷新機(jī)制實(shí)現(xiàn)方式
雙Token無(wú)感刷新機(jī)制實(shí)現(xiàn)
在現(xiàn)代 Web 應(yīng)用開(kāi)發(fā)中,前后端分離已經(jīng)成為一種趨勢(shì)。Vue.js 作為前端框架,Java作為后端語(yǔ)言的組合被廣泛應(yīng)用。在用戶認(rèn)證方面,JWT因?yàn)槠錈o(wú)狀態(tài)、易于擴(kuò)展等特點(diǎn)也備受青睞。
本文將詳細(xì)介紹如何在 Vue 前端和 Java后端實(shí)現(xiàn)雙 Token 的無(wú)感刷新機(jī)制。
后端依賴
<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過(guò)濾器,以及認(rèn)證失敗過(guò)濾器。
@Configuration @EnableWebSecurity public class SecurityConfig { /** * 認(rèn)證失敗處理類(lèi) */ @Autowired private AuthenticationEntryPointImpl unauthorizedHandler; /** * Jwt過(guò)濾器 */ @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public 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(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } // 使用BCryptPasswordEncoder作為security默認(rèn)的passwordEncoder @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
Jwt過(guò)濾器 *
這個(gè)過(guò)濾器是實(shí)現(xiàn)token刷新機(jī)制的核心,每次前端的請(qǐng)求攜帶accessToken與refreshToken過(guò)來(lái),此過(guò)濾器拿到之后,先對(duì)accessToken進(jìn)行解析,如果解析失?。?strong>過(guò)期),那么接下來(lái)會(huì)對(duì)refreshToken進(jìn)行解析,解析完成之后,如果沒(méi)有過(guò)期,就會(huì)生成新的accessToken與refreshToken返回給前端,并且設(shè)置一個(gè)新的請(qǐng)求頭Token-Refreshed,值可以隨便設(shè),前端能拿到就好。
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:繼承每個(gè)請(qǐng)求只會(huì)經(jīng)過(guò)一次的過(guò)濾器 */ @Component @Slf4j public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Value("${token.expiration}") private Long expiration; @Autowired private IUserMapper userMapper; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 獲取當(dāng)前請(qǐng)求路徑 String requestPath = request.getRequestURI(); // 排除不需要過(guò)濾的路徑 if (requestPath.equals("/user/login") || requestPath.equals("/user/register")) { filterChain.doFilter(request, response); return; } // 獲取token String 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; } // 解析token String 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); } // 存入SecurityContextHolder UsernamePasswordAuthenticationToken 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)); } }
前端的配置
前端對(duì)所有的axios請(qǐng)求進(jìn)行全局配置,先在每次請(qǐng)求的時(shí)候設(shè)置好請(qǐng)求頭accessToken與refreshToken,并且將每次請(qǐng)求都保存起來(lái),如果在請(qǐng)求時(shí)后端解析到accessToken失效,并且返回了新的accessToken與refreshToken,在請(qǐng)求頭拿到了后端設(shè)置好的Token-Refreshed,此時(shí)就可以重新將新的accessToken與refreshToken保存在瀏覽器本地,并且重新發(fā)送之前保存好的請(qǐng)求,就可以實(shí)現(xiàn)無(wú)感刷新。
request.js
import axios from 'axios' import {ref} from "vue"; // create an axios instance const service = axios.create({ baseURL: '/api', // url = base url + request url timeout: 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 = config return config } ) // response interceptor service.interceptors.response.use( response => { if (response.headers['token-refreshed']) { console.log('Token刷新成功'); // 如果有Token-Refreshed頭部,更新本地存儲(chǔ)中的Token localStorage.setItem('access_token', response.data.data.accessToken); localStorage.setItem('refresh_token', response.data.data.refreshToken); console.log("繼續(xù)") // 繼續(xù)發(fā)送原始請(qǐng)求 return axios(retryRequest.value) } return response; }, async error => { const originalRequest = error.config; // 如果是Token過(guò)期導(dǎo)致的401錯(cuò)誤,并且沒(méi)有retry標(biāo)記,嘗試刷新Token if (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); // 更新原始請(qǐng)求的Authorization頭部 originalRequest.headers['access_token'] = accessToken; // 重新發(fā)送原始請(qǐng)求 return instance(originalRequest); } catch (refreshError) { // 刷新Token失敗,跳轉(zhuǎn)到登錄頁(yè)或執(zhí)行其他處理 console.error('Token刷新失敗:', refreshError); // 這里可以跳轉(zhuǎn)到登錄頁(yè)或者執(zhí)行其他處理 } } } return Promise.reject(error); } ) export default service
通過(guò)以上步驟,我們就可以實(shí)現(xiàn)雙 Token 無(wú)感刷新機(jī)制。該機(jī)制通過(guò)短期有效的訪問(wèn) Token 和長(zhǎng)期有效的刷新 Token 相結(jié)合,在 Token 過(guò)期時(shí)自動(dòng)刷新。
本示例僅展示了基礎(chǔ)的實(shí)現(xiàn)方式,實(shí)際生產(chǎn)環(huán)境中還需要考慮更多安全性和健壯性的問(wèn)題。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- 如何實(shí)現(xiàn)無(wú)感刷新token
- VUE前端實(shí)現(xiàn)token的無(wú)感刷新3種方案(refresh_token)
- VUE前端實(shí)現(xiàn)token的無(wú)感刷新方式
- js項(xiàng)目中前端如何實(shí)現(xiàn)無(wú)感刷新token
- 前端雙token無(wú)感刷新圖文詳解
- 前端無(wú)感刷新token的實(shí)現(xiàn)步驟
- Vue項(xiàng)目實(shí)現(xiàn)token無(wú)感刷新的示例代碼
- Vue實(shí)現(xiàn)雙token無(wú)感刷新的示例代碼
- 雙token無(wú)感刷新nodejs+React詳細(xì)解釋(保姆級(jí)教程)
相關(guān)文章
Java中的Set集合不允許存儲(chǔ)重復(fù)元素的原理詳解
這篇文章主要介紹了Java中的Set集合不允許存儲(chǔ)重復(fù)元素的原理詳解,我們之前使用Set集合的時(shí)候發(fā)現(xiàn),Set集合的特點(diǎn)是不允許存儲(chǔ)重復(fù)元素,這是為什么呢,下面我們一起來(lái)研究一下,需要的朋友可以參考下2023-09-09spring boot整合redis實(shí)現(xiàn)RedisTemplate三分鐘快速入門(mén)
這篇文章主要介紹了spring boot整合redis實(shí)現(xiàn)RedisTemplate三分鐘快速入門(mén),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12Logback 使用TurboFilter實(shí)現(xiàn)日志級(jí)別等內(nèi)容的動(dòng)態(tài)修改操作
這篇文章主要介紹了Logback 使用TurboFilter實(shí)現(xiàn)日志級(jí)別等內(nèi)容的動(dòng)態(tài)修改操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08java類(lèi)的加載過(guò)程以及類(lèi)加載器的分析
這篇文章給大家詳細(xì)講述了java類(lèi)的加載過(guò)程以及類(lèi)加載器的相關(guān)知識(shí)點(diǎn)內(nèi)容,有需要的朋友可以學(xué)習(xí)下。2018-08-08Eclipse中導(dǎo)入Maven Web項(xiàng)目并配置其在Tomcat中運(yùn)行圖文詳解
這篇文章主要介紹了Eclipse中導(dǎo)入Maven Web項(xiàng)目并配置其在Tomcat中運(yùn)行圖文詳解,需要的朋友可以參考下2017-12-12Java中日期與時(shí)間的處理及工具類(lèi)封裝詳解
在項(xiàng)目開(kāi)發(fā)中免不了有對(duì)日期時(shí)間的處理,但Java中關(guān)于日期時(shí)間的類(lèi)太多了,本文就來(lái)介紹一下各種類(lèi)的使用及我們項(xiàng)目中應(yīng)該怎么選擇吧2023-07-07java中synchronized關(guān)鍵字的3種寫(xiě)法實(shí)例
synchronized是Java中的關(guān)鍵字,是一種同步鎖,下面這篇文章主要給大家介紹了關(guān)于java中synchronized關(guān)鍵字的3種寫(xiě)法,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2021-11-11