@Component public class MyAccessFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String uri = request.getURI().getPath(); HttpMethod method = request.getMethod(); // 堆代码 duidaima.com // OPTION直接放行 if(method.matches(HttpMethod.OPTIONS.name())) return chain.filter(exchange); //登录请求直接放行 if(SecurityAccessConstant.REQUEST_LOGGING_URI.equals(uri) && method.matches(HttpMethod.POST.name())) return chain.filter(exchange); //获取token String token = JWTHelper.getToken(request.getHeaders().getFirst(SecurityAccessConstant.HEADER_NAME_TOKEN)); if(null != token){ //判断token是否过时 if(!JWTHelper.isOutDate(token)){ return chain.filter(exchange); }else{ if(!SecurityAccessConstant.REQUEST_REFRESH.equals(uri)) //当前不是刷新请求可以刷新返回的状态码就是511 return ResponseUtils.out(exchange , ResultData.fail(ResultCodeEnum.NEED_TO_REFRESH_TOKEN.getCode(), ResultCodeEnum.NEED_TO_REFRESH_TOKEN.getMessage())); //当前是刷新请求 但refreshToken都过期了,即刷新不支持 return ResponseUtils.out(exchange , ResultData.fail(ResultCodeEnum.RC401.getCode(), ResultCodeEnum.RC401.getMessage())); } } return ResponseUtils.out(exchange , ResultData.fail(ResultCodeEnum.RC401.getCode(), ResultCodeEnum.RC401.getMessage())); } @Override public int getOrder() { //数值越小 优先级越高 return Ordered.LOWEST_PRECEDENCE; } }2.1.1.1 问题Q2解决
//堆代码 duidaima.com //判断当前token是否过期 public static boolean isOutDate(String token){ try { Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token); Date expirationDate = claimsJws.getBody().getExpiration(); return expirationDate.before(new Date()); } catch (JwtException e) { // JWT token无效或已损坏 return true; } }2.1.2 axios拦截器
// 响应拦截器 service.interceptors.response.use( // 响应成功进入第1个函数 // 该函数的参数是响应对象 function(response) { console.log(response) return response.data.data; }, // 响应失败进入第2个函数,该函数的参数是错误对象 async function(error) { // 如果响应码是 401 ,则请求获取新的 token // 响应拦截器中的 error 就是那个响应的错误对象 if(error.response == undefined) return Promise.reject(error); const status = error.response.status const authStore = useAuthStore() let message = '' switch(status){ case 401: // 无权限 authStore.reset() // 清空store中的权限数据 window.sessionStorage.removeItem('isAuthenticated') window.sessionStorage.removeItem('token') window.sessionStorage.removeItem('refreshToken') message = 'token 失效,请重新登录' // 跳转到登录页 window.location.href = '/auth/login'; break; case 511: // 当前token需要刷新 try { const data = refresh() if(data !== null){ data.then((value) => { // Use the string value here if(value !== ''){ // 如果获取成功,则把新的 token 更新到容器中 console.log("刷新 token 成功", value); window.sessionStorage.setItem("token",value) // 把之前失败的用户请求继续发出去 // config 是一个对象,其中包含本次失败请求相关的那些配置信息,例如 url、method 都有 // return 把 request 的请求结果继续返回给发请求的具体位置 error.config.headers['Authorization'] = 'Bearer ' +value; return service(error.config); } console.log(value); }).catch((error) => { // Handle any errors that occurred while resolving the promise console.error(error); }); } } catch (err) { // 如果获取失败,直接跳转 登录页 console.log("请求刷线 token 失败", err); router.push("/login"); } break; case '403': message = '拒绝访问' break; case '404': message = '请求地址错误' break; case '500': message = '服务器故障' break; default: message = '网络连接故障' } Message.error(message) return Promise.reject(error); } );2.1.3 refresh刷新token方法实现
/** * 刷新token * 成功返回新token * 失败返回空字符串'' */ export async function refresh() : Promise<string>{ const refreshToken = window.sessionStorage.getItem("refreshToken") console.log("in >>> " ,refreshToken) if(refreshToken == undefined) return '' //本来就没有这个更新token则直接返回 try { const response = await axios({ method: 'GET', url: 'http://127.0.0.1:9001/api/simple/cloud/access/refresh',// 认证服务器地址 headers: { Authorization: `Bearer ${refreshToken}`, //header中放入的是refreshToken用于刷新请求 }, }); // 如果顺利返回会得到 data,由于后端使用统一结果返回ResultData,所以会多封装一层code、data if (response.data) { return response.data.data; //所以这里有两个data } else { return ''; } } catch (error) { console.log(error); return ''; } }2.1.4 正常和刷新情况下的console输出信息分析
import { refresh } from "@/api/system/auth/index" import { jwtDecode } from "jwt-decode"; export class MyTimer { private timerId: any | null = null; // delay为重复探查的间隔时间 , minCheck是判断token是否是快过期的 start(delay: number, minCheck : number): void { this.timerId = setInterval(async () => { const currentToken = window.sessionStorage.getItem('token'); console.log("timer++++") if (currentToken) { // 如果存在token,判断是否过期 let expirationTime = 0; expirationTime = getExpirationTime(currentToken) ; // 假设有一个函数用于获取token的过期时间 const timeRemaining = expirationTime - Date.now(); if (timeRemaining <= minCheck) { // 如果剩余时间小于等于5分钟,则异步发送刷新请求并更新token await refresh(); } } else { // 如果不存在token,则直接发送刷新请求并更新token await refresh(); } }, delay); } stop(): void { if (this.timerId !== null) { clearInterval(this.timerId); this.timerId = null; } } } // 获取过期时间 function getExpirationTime(rawToken:string) : number{ const res = jwtDecode(rawToken) return res.exp as number }2.2.2 修改Login点击事件
import { MyTimer } from "@/utils/tokenMonitor" const submit = () => { if (validate()) { login(formData) .then((data: UserInfoRes) => { if (data) { // 在这里添加需要执行的操作 const token = data.token; // 将token存储到authStore中 const authStore = useAuthStore() authStore.setToken(token) window.sessionStorage.setItem('token', token) window.sessionStorage.setItem('refreshToken', data.refreshToken) authStore.setIsAuthenticated(true) window.sessionStorage.setItem('isAuthenticated', 'true') authStore.setName(data.name) authStore.setButtons(data.buttons) authStore.setRoles(data.roles) authStore.setRouters(data.routers) //新增 引入计时器》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》 const clock = new MyTimer(); clock.start(1000*30,1000*30); init({ message: "logged in success", color: 'success' }); push({ name: 'dashboard' }) } }) .catch(() => { init({ message: "logged in fail , please check carefully!", color: '#FF0000' }); }); }else{ Message.error('error submit!!') return false } }2.2.3 测试
// 获取当前token过期时间 这里不判断是否过期因为是通过了过期判断才进来的 public static Date getExpirationDate(String token) { if(StringUtil.isBlank(token)) return null; Claims claims = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody(); return claims.getExpiration(); }2.3.1.2 发放token处携带过期时间
//存放token到请求头中 String[] tokenArray = JWTHelper.createToken(sysUser.getId(), sysUser.getEmail(), permsList); map.put("token",tokenArray[0]); // 新增设置过期时间 毫秒数 map.put("tokenExpire",JWTHelper.getExpirationDate(tokenArray[0]).getTime()); map.put("refreshToken",tokenArray[1]);同样在refreshToken处也就不是只返回token,也需要带上其过期时间,代码与上面相同就不重复写了
import { refresh } from "@/api/system/auth/index" class MyTimer { private timerId: any | null = null; private delay: number; //执行间隔时间 private minCheck: number; //判断token过期时间是否小于该值 private static instance: MyTimer; public static getInstance(): MyTimer { if (!MyTimer.instance) { MyTimer.instance = new MyTimer(); } return MyTimer.instance; } private constructor() { this.delay = 30000; // Default delay value in milliseconds this.minCheck = 60000; // Default minCheck value in milliseconds (1 minutes) } //启动监控器的方法 start(): void { this.timerId = setInterval(async () => { const currentToken = window.sessionStorage.getItem('token'); console.log("timer++++",currentToken) if (currentToken) { // 如果存在token,判断是否过期 const tokenExpireStr = window.sessionStorage.getItem('tokenExpire') as string// 假设有一个函数用于获取token的过期时间 const expirationTime = parseInt(tokenExpireStr, 10); //以10进制转换string字符串 const timeRemaining = expirationTime - Date.now(); console.log("ttime sub++++",timeRemaining) if (timeRemaining <= this.minCheck) { // 如果剩余时间小于等于minCheck分钟,则异步发送刷新请求并更新token try{ await refresh(); }catch (error) { console.error('刷新失败:', error); window.sessionStorage.removeItem('isAuthenticated') window.sessionStorage.removeItem('token') window.sessionStorage.removeItem('refreshToken') Message.error("token reflesh got some ploblem , please login") // 跳转到登录页的代码 window.location.href = '/auth/login'; } } } else { Message.error("token invalidate , please login") // token不存在 则跳转到登录页 window.location.href = '/auth/login'; } }, this.delay); console.log(this.timerId) } //关闭监控器的方法 stop(): void { if (this.timerId !== null) { clearInterval(this.timerId); this.timerId = null; } } //提供设置监控器的刷新间隔和需要刷新的阈值 setDelay(delay: number): void { this.delay = delay; } setMinCheck(minCheck: number): void { this.minCheck = minCheck; } } //导出全局唯一的实例方便管理 export const myFilterInstance = MyTimer.getInstance(); // 加到每一个页面上,当页面刷新时候则重启定时器,防止定时器刷掉 export function onPageRender(){ // Stop the current timer if it's running myFilterInstance.stop(); // Start the timer with the updated delay and minCheck values myFilterInstance.start(); }2.3.3 onPageRender 使用
import { onPageRender } from '@/utils/tokenMonitor' // 新增一个监听器,在页面渲染时候执行 window.addEventListener('load', () => { onPageRender(); });2.3.4 测试
// 向认证服务器发送请求,获取新的token Mono<ResultData> newTokenMono = WebClient.create().get() .uri(buildUri(SecurityAccessConstant.WEB_REQUEST_TO_AUTH_URL+SecurityAccessConstant.REQUEST_REFRESH , new String[]{"refreshToken", token})) .retrieve() .bodyToMono(ResultData.class); // 原子操作 AtomicBoolean isPass = new AtomicBoolean(false); //订阅数据 newTokenMono.subscribe(resultData -> { if(resultData.getCode() == "200"){ exchange.getRequest().getHeaders().set(SecurityAccessConstant.HEADER_NAME_TOKEN, SecurityAccessConstant.TOKEN_PREFIX + resultData.getData()); isPass.set(true); } }).dispose(); // 销毁资源 if(isPass.get()){ // 如果成功获取到资源(新token则发送新请求) return chain.filter(exchange.mutate().request().build()); }四. 怎么选择
解决一致性问题: 用户端刷新token可能导致不同客户端之间的状态不一致,比如一个设备刷新了token而另一个设备未刷新,可能会出现异常情况。