闽公网安备 35020302035485号
@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而另一个设备未刷新,可能会出现异常情况。