You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/ui/token.js

216 lines
6.8 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* auth.js
* Copyright (C) 2025 veypi <i@veypi.com>
*
* Distributed under terms of the MIT license.
*/
class TokenService {
__root = ''
constructor() {
this.tokenKey = 'access';
this.refreshTokenKey = 'refresh';
}
setRoot(root) {
this.__root = root;
}
setToken(token) {
localStorage.setItem(this.tokenKey, token);
}
getToken() {
return localStorage.getItem(this.tokenKey);
}
setRefreshToken(refreshToken) {
localStorage.setItem(this.refreshTokenKey, refreshToken);
}
getRefreshToken() {
return localStorage.getItem(this.refreshTokenKey);
}
clearToken() {
localStorage.removeItem(this.tokenKey);
localStorage.removeItem(this.refreshTokenKey);
}
hasToken() {
return !!this.getToken();
}
parseToken(token) {
try {
if (!token) return null;
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(window.atob(base64));
} catch (error) {
console.error('Token解析失败:', error);
return null;
}
}
__cache = null
body() {
if (!this.__cache) {
this.__cache = this.parseToken(this.getToken());
}
return this.__cache
}
logout() {
this.clearToken();
location.href = this.__root + '/login?redirect=' + window.location.pathname;
}
async refreshToken() {
const refreshToken = this.getRefreshToken();
if (!refreshToken) {
this.logout()
return;
}
try {
let data = await fetch(this.__root + '/api/token', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh: refreshToken })
}).then(res => res.json())
if (data.code === 0) {
this.setToken(data.data);
} else {
this.clearToken()
}
} catch (e) {
console.error('Token刷新失败:', e);
this.clearToken()
// logout();
}
}
isExpired() {
const decoded = this.body();
if (!decoded) return true;
const currentTime = Date.now() / 1000;
return decoded.exp < currentTime;
}
wrapAxios(instance) {
// 定义一个标志,用于防止在刷新令牌时发送多个刷新请求
let isRefreshing = false;
// 定义一个队列,用于存储在刷新令牌期间失败的请求
let failedQueue = [];
/**
* 处理等待队列中的请求。
* 刷新令牌成功后,将队列中的请求重新发送;刷新失败则拒绝这些请求。
* @param {Error|null} error - 如果刷新令牌失败,则为错误对象。
* @param {string|null} token - 刷新成功后获取的新令牌。
*/
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
// 刷新失败,拒绝队列中的所有请求
prom.reject(error);
} else {
// 刷新成功,使用新令牌解决队列中的所有请求
prom.resolve(token);
}
});
// 清空队列
failedQueue = [];
};
// 请求拦截器:在发送请求前添加认证令牌
instance.interceptors.request.use(config => {
const token = this.getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, error => {
// 对请求错误做些什么
return Promise.reject(error);
});
let that = this
// 响应拦截器:处理响应数据,特别是针对 401 状态码进行令牌刷新和重试
instance.interceptors.response.use((response) => {
// 任何 2xx 范围内的状态码都会触发此函数
// 这里可以添加其他全局的成功响应处理逻辑
return response;
}, async function(error) {
// 任何超出 2xx 范围的状态码都会触发此函数
const originalRequest = error.config;
console.log(error)
// 检查错误响应状态码是否为 401 (未授权)
// 并且确保这不是一个已经重试过的请求 (通过 originalRequest._retry 标记)
if (error.response && error.response.status === 401 && !originalRequest._retry) {
// 标记此请求为已重试,避免无限循环
originalRequest._retry = true;
// 统计该请求的重试次数
originalRequest.__retryCount = originalRequest.__retryCount || 0;
originalRequest.__retryCount++;
// 如果重试次数超过 3 次,则不再重试,直接跳转到登录页
if (originalRequest.__retryCount >= 3) {
that.clearToken();
// 跳转到登录页,并带上当前页面的路径作为重定向参数
window.location.href = that.__root + '/login?redirect=' + window.location.pathname;
// 拒绝原始请求的 Promise停止后续处理
return Promise.reject(error);
}
// 如果当前正在进行令牌刷新
if (isRefreshing) {
// 将当前失败的请求添加到队列中,等待新令牌
return new Promise(resolve => {
failedQueue.push({ resolve, reject: (err) => { throw err; } });
}).then(token => {
// 刷新成功后,使用新令牌更新请求头
originalRequest.headers.Authorization = `Bearer ${token}`;
// 重新发送原始请求
return instance(originalRequest);
}).catch(err => {
// 如果队列中的请求被拒绝,则抛出错误
return Promise.reject(err);
});
}
// 如果没有正在进行令牌刷新,则设置标志为 true开始刷新
isRefreshing = true;
try {
// 发送请求来刷新令牌
await that.refreshToken();
const newToken = that.getToken();
// 更新原始请求的 Authorization 头为新的令牌
originalRequest.headers.Authorization = `Bearer ${newToken}`;
// 处理等待队列中的所有请求,用新的令牌重新发送
processQueue(null, newToken);
// 重新发送最初导致 401 错误的请求
return instance(originalRequest);
} catch (refreshError) {
// 如果刷新令牌本身也失败了 (例如refresh token 已过期)
// 清除本地令牌
that.clearToken();
// 拒绝等待队列中的所有请求
processQueue(refreshError);
that.logout();
// 拒绝原始请求的 Promise
return Promise.reject(refreshError);
} finally {
// 无论成功或失败,最后都要将刷新标志重置为 false
isRefreshing = false;
}
}
// 对于其他类型的错误,或不是需要重试的 401 错误,直接拒绝 Promise
return Promise.reject(error);
});
}
}
export default new TokenService();