/* * auth.js * Copyright (C) 2025 veypi * * 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); } toJSON() { return this.getToken() } toString() { return this.getToken() } 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 || typeof token !== 'string') return null; const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); return JSON.parse(window.atob(base64)); } catch (error) { console.warn('Token解析失败:', error); return null; } } __cache = null body() { if (!this.__cache) { this.__cache = this.parseToken(this.getToken()); } if (!this.__cache) { this.__cache = this.parseToken(this.getRefreshToken()); } return this.__cache } logout(to) { this.clearToken(); location.href = this.__root + '/login?redirect=' + (to || window.location.pathname); } async refresh() { 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()) this.__cache = null; // 清除缓存 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; // 检查错误响应状态码是否为 401 (未授权) // 并且确保这不是一个已经重试过的请求 (通过 originalRequest._retry 标记) if (error.response && error.response.status === 401) { // 统计该请求的重试次数 originalRequest.__retryCount = originalRequest.__retryCount || 0; originalRequest.__retryCount++; // 如果重试次数超过 3 次,则不再重试,直接跳转到登录页 if (originalRequest.__retryCount >= 3) { that.logout() // 拒绝原始请求的 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.refresh(); const newToken = that.getToken(); // 更新原始请求的 Authorization 头为新的令牌 originalRequest.headers.Authorization = `Bearer ${newToken}`; // 处理等待队列中的所有请求,用新的令牌重新发送 processQueue(null, newToken); // 重新发送最初导致 401 错误的请求 return instance(originalRequest); } catch (refreshError) { // 如果刷新令牌本身也失败了 (例如,refresh token 已过期) // 清除本地令牌 // 拒绝等待队列中的所有请求 processQueue(refreshError); // that.clearToken(); // that.logout(); // 拒绝原始请求的 Promise return Promise.reject(refreshError); } finally { // 无论成功或失败,最后都要将刷新标志重置为 false isRefreshing = false; } } // 对于其他类型的错误,或不是需要重试的 401 错误,直接拒绝 Promise return Promise.reject(error); }); } } export default new TokenService();