雙Token無感刷新機(jī)制實(shí)現(xiàn)方式
雙Token無感刷新機(jī)制實(shí)現(xiàn)
在現(xiàn)代 Web 應(yīng)用開發(fā)中,前后端分離已經(jīng)成為一種趨勢。Vue.js 作為前端框架,Java作為后端語言的組合被廣泛應(yīng)用。在用戶認(rèn)證方面,JWT因?yàn)槠錈o狀態(tài)、易于擴(kuò)展等特點(diǎn)也備受青睞。
本文將詳細(xì)介紹如何在 Vue 前端和 Java后端實(shí)現(xiàn)雙 Token 的無感刷新機(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過濾器,以及認(rèn)證失敗過濾器。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
/**
* 認(rèn)證失敗處理類
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* Jwt過濾器
*/
@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過濾器 *
這個過濾器是實(shí)現(xiàn)token刷新機(jī)制的核心,每次前端的請求攜帶accessToken與refreshToken過來,此過濾器拿到之后,先對accessToken進(jìn)行解析,如果解析失敗(過期),那么接下來會對refreshToken進(jìn)行解析,解析完成之后,如果沒有過期,就會生成新的accessToken與refreshToken返回給前端,并且設(shè)置一個新的請求頭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:繼承每個請求只會經(jīng)過一次的過濾器
*/
@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)前請求路徑
String requestPath = request.getRequestURI();
// 排除不需要過濾的路徑
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));
}
}
前端的配置
前端對所有的axios請求進(jìn)行全局配置,先在每次請求的時候設(shè)置好請求頭accessToken與refreshToken,并且將每次請求都保存起來,如果在請求時后端解析到accessToken失效,并且返回了新的accessToken與refreshToken,在請求頭拿到了后端設(shè)置好的Token-Refreshed,此時就可以重新將新的accessToken與refreshToken保存在瀏覽器本地,并且重新發(fā)送之前保存好的請求,就可以實(shí)現(xiàn)無感刷新。
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頭部,更新本地存儲中的Token
localStorage.setItem('access_token', response.data.data.accessToken);
localStorage.setItem('refresh_token', response.data.data.refreshToken);
console.log("繼續(xù)")
// 繼續(xù)發(fā)送原始請求
return axios(retryRequest.value)
}
return response;
},
async error => {
const originalRequest = error.config;
// 如果是Token過期導(dǎo)致的401錯誤,并且沒有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);
// 更新原始請求的Authorization頭部
originalRequest.headers['access_token'] = accessToken;
// 重新發(fā)送原始請求
return instance(originalRequest);
} catch (refreshError) {
// 刷新Token失敗,跳轉(zhuǎn)到登錄頁或執(zhí)行其他處理
console.error('Token刷新失敗:', refreshError);
// 這里可以跳轉(zhuǎn)到登錄頁或者執(zhí)行其他處理
}
}
}
return Promise.reject(error);
}
)
export default service
通過以上步驟,我們就可以實(shí)現(xiàn)雙 Token 無感刷新機(jī)制。該機(jī)制通過短期有效的訪問 Token 和長期有效的刷新 Token 相結(jié)合,在 Token 過期時自動刷新。
本示例僅展示了基礎(chǔ)的實(shí)現(xiàn)方式,實(shí)際生產(chǎn)環(huán)境中還需要考慮更多安全性和健壯性的問題。
總結(jié)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
- 前端登錄token失效實(shí)現(xiàn)雙Token無感刷新詳細(xì)步驟
- SpringBoot中雙token實(shí)現(xiàn)無感刷新
- 雙Token實(shí)現(xiàn)無感刷新的完整代碼示例
- 雙token無感刷新nodejs+React詳細(xì)解釋(保姆級教程)
- node.js實(shí)現(xiàn)雙Token+Cookie存儲+無感刷新機(jī)制的示例
- 前端雙token無感刷新圖文詳解
- vue中雙token和無感刷新token的區(qū)別
- Vue實(shí)現(xiàn)雙token無感刷新的示例代碼
- Vue3+Vite使用雙token實(shí)現(xiàn)無感刷新
- SpringBoot+React中雙token實(shí)現(xiàn)無感刷新
相關(guān)文章
spring Mvc配置xml使ResponseBody返回Json的方法示例
這篇文章主要給大家介紹了關(guān)于spring Mvc配置xml使ResponseBody返回Json的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-04-04
Java中struts2和spring MVC的區(qū)別_動力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java中struts2和spring MVC的區(qū)別,非常不錯,具有參考借鑒價(jià)值,需要的朋友參考下吧2017-09-09
java數(shù)據(jù)結(jié)構(gòu)基礎(chǔ):單鏈表與雙向鏈表
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)單鏈表、雙向鏈表的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
Java并發(fā)教程之Callable和Future接口詳解
Java從發(fā)布的第一個版本開始就可以很方便地編寫多線程的應(yīng)用程序,并在設(shè)計(jì)中引入異步處理,這篇文章主要給大家介紹了關(guān)于Java并發(fā)教程之Callable和Future接口的相關(guān)資料,需要的朋友可以參考下2021-07-07
Java Jackson之ObjectMapper常用用法總結(jié)
這篇文章主要給大家介紹了關(guān)于Java Jackson之ObjectMapper常用用法的相關(guān)資料,ObjectMapper是一個Java庫,用于將JSON字符串轉(zhuǎn)換為Java對象或?qū)ava對象轉(zhuǎn)換為JSON字符串,需要的朋友可以參考下2024-01-01

