This commit is contained in:
“xHuPo” 2025-06-17 14:44:48 +08:00
parent 17a02ea47e
commit 70e7a113e6
22 changed files with 967 additions and 285 deletions

View file

@ -4,46 +4,110 @@
// 导入统一配置
const config = require('./config');
const auth = require('./auth');
/**
* 检查是否有有效的认证令牌
* @returns {boolean} 是否有有效的访问令牌和刷新令牌
*/
const hasValidTokens = () => {
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
// 检查token是否存在且未过期
if (!accessToken || !refreshToken) {
return false;
}
// 使用shouldRefreshToken验证token有效性
const { needsRefresh } = shouldRefreshToken();
return !needsRefresh;
};
/**
* 检查JWT token是否需要刷新
* @returns {boolean}
* @returns {Object} 包含needsRefresh和hasRefreshToken两个布尔值
*/
const shouldRefreshToken = () => {
try {
const token = wx.getStorageSync(config.JWT_CONFIG.storage.access);
if (!token) return true;
// 解析JWT token不验证签名
const [, payload] = token.split('.');
const { exp } = JSON.parse(atob(payload));
const expirationTime = exp * 1000; // 转换为毫秒
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
// 如果token将在5分钟内过期则刷新
return Date.now() + config.JWT_CONFIG.refreshThreshold > expirationTime;
// 如果没有访问令牌或刷新令牌,则不需要刷新
if (!accessToken) {
return { needsRefresh: false, hasRefreshToken: !!refreshToken };
}
// 使用auth模块解析JWT token
const decoded = auth.parseToken(accessToken);
// 定义expirationTime变量
let expirationTime;
if (decoded && decoded.exp) {
expirationTime = decoded.exp * 1000; // 转换为毫秒
} else {
console.error('解析JWT token失败: 无法获取过期时间');
// 如果解析失败假设token将在1小时后过期
expirationTime = Date.now() + 3600 * 1000;
}
// 如果token将在5分钟内过期则需要刷新
const needsRefresh = Date.now() + config.JWT_CONFIG.refreshThreshold > expirationTime;
return { needsRefresh, hasRefreshToken: !!refreshToken };
} catch (error) {
console.error('Token解析失败:', error);
return true;
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
return { needsRefresh: false, hasRefreshToken: !!refreshToken };
}
};
/**
* 刷新JWT token
* @returns {Promise<void>}
* @returns {Promise<boolean>} 刷新是否成功
*/
const refreshToken = async () => {
try {
const refreshToken = wx.getStorageSync(config.JWT_CONFIG.storage.refresh);
if (!refreshToken) {
throw new Error('No refresh token available');
console.warn('没有可用的刷新令牌,无法刷新访问令牌');
return false;
}
const response = await wx.request({
url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.REFRESH}`,
method: 'POST',
header: {
'Authorization': `Bearer ${refreshToken}`
}
// 使用Promise包装wx.request以便使用await
const response = await new Promise((resolve, reject) => {
wx.request({
url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.REFRESH}`,
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': `${config.JWT_CONFIG.tokenPrefix}${refreshToken}`
},
success: (res) => {
// 确保response对象包含完整的信息
resolve({
...res,
requestOptions: {
url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.REFRESH}`,
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': `${config.JWT_CONFIG.tokenPrefix}${refreshToken}`
}
}
});
},
fail: (error) => {
// 增强错误信息
const enhancedError = new Error(error.errMsg || '刷新令牌请求失败');
enhancedError.originalError = error;
enhancedError.requestOptions = {
url: `${config.API_BASE_URL}${config.API_ENDPOINTS.AUTH.REFRESH}`,
method: 'POST'
};
reject(enhancedError);
}
});
});
if (response.statusCode === 200 && response.data.access_token) {
@ -51,15 +115,21 @@ const refreshToken = async () => {
if (response.data.refresh_token) {
wx.setStorageSync(config.JWT_CONFIG.storage.refresh, response.data.refresh_token);
}
console.log('Token刷新成功');
return true;
} else {
throw new Error('Token refresh failed');
console.error(`Token刷新失败: ${response.statusCode}`);
// 清除所有token强制用户重新登录
wx.removeStorageSync(config.JWT_CONFIG.storage.access);
wx.removeStorageSync(config.JWT_CONFIG.storage.refresh);
return false;
}
} catch (error) {
console.error('Token刷新失败:', error);
// 清除所有token强制用户重新登录
wx.removeStorageSync(config.JWT_CONFIG.storage.access);
wx.removeStorageSync(config.JWT_CONFIG.storage.refresh);
throw new Error('认证已过期,请重新登录');
return false;
}
};
@ -70,20 +140,33 @@ const refreshToken = async () => {
* @returns {Promise<any>} 响应数据
*/
const request = async (url, options = {}) => {
// 检查并刷新token
if (shouldRefreshToken()) {
await refreshToken();
// 对于登录请求跳过token刷新检查
const isLoginRequest = url === config.API_ENDPOINTS.AUTH.LOGIN;
if (!isLoginRequest) {
// 检查是否需要刷新token
const tokenStatus = shouldRefreshToken();
if (tokenStatus.needsRefresh && tokenStatus.hasRefreshToken) {
const refreshSuccess = await refreshToken();
if (!refreshSuccess) {
console.warn('Token刷新失败继续使用当前token');
}
}
}
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
const defaultOptions = {
method: 'GET',
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`
'Content-Type': 'application/json'
}
};
// 只有在有访问令牌且不是登录请求时才添加Authorization头
if (accessToken && !isLoginRequest) {
defaultOptions.header['Authorization'] = `Bearer ${accessToken}`;
}
const requestOptions = {
...defaultOptions,
...options,
@ -98,33 +181,81 @@ const request = async (url, options = {}) => {
try {
const response = await new Promise((resolve, reject) => {
wx.request({
const requestTask = wx.request({
...requestOptions,
success: resolve,
fail: reject
success: (res) => {
// 对于404错误静默处理不要让wx.request打印错误日志
if (res.statusCode === 404 && url === config.API_ENDPOINTS.OTP.RECOVER) {
// 静默处理404错误
res.silent404 = true;
}
// 确保response对象包含完整的信息
resolve({
...res,
requestOptions: {
url: requestOptions.url,
method: requestOptions.method,
header: requestOptions.header
}
});
},
fail: (error) => {
// 增强错误信息
const enhancedError = new Error(error.errMsg || '网络请求失败');
enhancedError.originalError = error;
enhancedError.requestOptions = {
url: requestOptions.url,
method: requestOptions.method,
header: requestOptions.header
};
reject(enhancedError);
}
});
});
// 处理401错误token无效
if (response.statusCode === 401) {
// 尝试刷新token并重试请求
await refreshToken();
return request(url, options); // 递归调用使用新token重试
if (response.statusCode === 401 && !isLoginRequest) {
// 只有在有刷新令牌的情况下才尝试刷新token
const tokenStatus = shouldRefreshToken();
if (tokenStatus.hasRefreshToken) {
const refreshSuccess = await refreshToken();
if (refreshSuccess) {
return request(url, options); // 递归调用使用新token重试
}
}
// 如果没有刷新令牌或刷新失败,抛出认证错误
throw new Error('认证失败,请重新登录');
}
if (response.statusCode >= 200 && response.statusCode < 300) {
return response.data;
}
throw new Error(response.data?.message || '请求失败');
} catch (error) {
console.error('API请求失败:', {
endpoint: url,
method: requestOptions.method,
status: error.statusCode || 'N/A',
error: error.message
});
// 增强错误信息
const error = new Error(response.data?.message || '请求失败');
error.response = response;
error.statusCode = response.statusCode;
throw error;
} catch (error) {
// 只有非404错误才打印错误日志
if (error.statusCode !== 404) {
console.error('API请求失败:', {
endpoint: url,
method: requestOptions.method,
status: error.statusCode || 'N/A',
error: error.message,
requestOptions: error.requestOptions || requestOptions,
originalError: error.originalError || error
});
}
// 重新抛出增强的错误
const enhancedError = new Error(error.message || '网络请求失败');
enhancedError.originalError = error;
enhancedError.requestOptions = error.requestOptions || requestOptions;
throw enhancedError;
}
};
@ -139,11 +270,51 @@ const uploadTokens = async (tokens) => {
}
try {
// 从access_token中获取用户ID
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
if (!accessToken) {
throw new Error('未登录');
}
// 使用auth模块解析JWT token获取用户ID
const decoded = auth.parseToken(accessToken);
if (!decoded) {
throw new Error('无法解析访问令牌');
}
const userId = decoded.sub || decoded.user_id || decoded.userId; // 尝试不同的可能字段名
if (!userId) {
throw new Error('用户未登录');
}
// 处理令牌数据,确保符合后端要求
const processedTokens = tokens.map(token => {
// 创建一个新对象,只包含后端需要的字段
const processedToken = {
id: token.id,
issuer: token.issuer,
account: token.account,
secret: token.secret,
type: token.type,
period: token.period,
digits: token.digits,
algorithm: token.algorithm
};
// 只有HOTP类型的令牌才设置counter字段且必须大于等于0
if (token.type && token.type.toUpperCase() === 'HOTP') {
processedToken.counter = token.counter || 0;
}
// TOTP类型的令牌不设置counter字段
return processedToken;
});
const response = await request(config.API_ENDPOINTS.OTP.SAVE, {
method: 'POST',
data: {
tokens,
timestamp: Date.now()
tokens: processedTokens,
userId
}
});
@ -164,24 +335,86 @@ const uploadTokens = async (tokens) => {
*/
const fetchLatestTokens = async () => {
try {
// 从access_token中获取用户ID
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
if (!accessToken) {
throw new Error('未登录');
}
// 使用auth模块解析JWT token获取用户ID
const decoded = auth.parseToken(accessToken);
if (!decoded) {
throw new Error('无法解析访问令牌');
}
const userId = decoded.sub || decoded.user_id || decoded.userId; // 尝试不同的可能字段名
if (!userId) {
throw new Error('无法获取用户ID');
}
const response = await request(config.API_ENDPOINTS.OTP.RECOVER, {
method: 'POST',
data: {
timestamp: Date.now()
userId
}
});
if (response.success && response.data?.tokens) {
if (response.success) {
// 当云端无数据时,返回空数组
if (!response.data?.tokens) {
return {
tokens: [],
timestamp: new Date().toISOString(),
num: 0
};
}
// 处理从后端获取的令牌数据,确保符合前端要求
const processedTokens = response.data.tokens.map(token => {
// 创建一个新对象,包含前端需要的字段
const processedToken = {
...token,
createTime: token.createTime || new Date().toISOString(),
lastUpdate: token.lastUpdate || new Date().toISOString(),
code: token.code || ''
};
// 确保HOTP类型的令牌有counter字段
if (token.type && token.type.toUpperCase() === 'HOTP') {
processedToken.counter = token.counter || 0;
}
// TOTP类型的令牌不需要counter字段
return processedToken;
});
return {
tokens: response.data.tokens,
tokens: processedTokens,
timestamp: response.data.timestamp,
num: response.data.tokens.length
num: processedTokens.length
};
}
throw new Error(response.message || '未找到云端数据');
// 当云端无数据时后端应返回404状态码
if (response.statusCode === 404) {
return {
tokens: [],
timestamp: new Date().toISOString(),
num: 0
};
}
throw new Error(response.message || '获取云端数据失败');
} catch (error) {
console.error('获取云端数据失败:', error);
// 当状态码为404时说明云端无数据返回空数组
if (error.statusCode === 404) {
return {
tokens: [],
timestamp: new Date().toISOString(),
num: 0
};
}
throw error;
}
};
@ -193,16 +426,20 @@ const fetchLatestTokens = async () => {
*/
const initCloud = async () => {
try {
// 检查是否有有效的访问令牌
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
if (!accessToken) {
console.log('未找到访问令牌,需要登录');
// 检查是否有有效的令牌
if (!hasValidTokens()) {
console.log('未找到有效令牌,需要登录');
return false;
}
// 验证令牌有效性
if (shouldRefreshToken()) {
await refreshToken();
const tokenStatus = shouldRefreshToken();
if (tokenStatus.needsRefresh) {
const refreshSuccess = await refreshToken();
if (!refreshSuccess) {
console.log('令牌刷新失败,需要重新登录');
return false;
}
}
return true;
} catch (error) {
@ -287,6 +524,47 @@ const mergeTokens = (localTokens, cloudTokens, options = { preferCloud: true })
return result;
};
/**
* 清空云端所有令牌数据
* @returns {Promise<boolean>} 是否成功
*/
const clearAllTokens = async () => {
try {
// 从access_token中获取用户ID
const accessToken = wx.getStorageSync(config.JWT_CONFIG.storage.access);
if (!accessToken) {
throw new Error('未登录');
}
// 使用auth模块解析JWT token获取用户ID
const decoded = auth.parseToken(accessToken);
if (!decoded) {
throw new Error('无法解析访问令牌');
}
const userId = decoded.sub || decoded.user_id || decoded.userId;
if (!userId) {
throw new Error('无法获取用户ID');
}
const response = await request(config.API_ENDPOINTS.OTP.CLEAR_ALL, {
method: 'POST',
data: {
userId
}
});
if (response.success) {
return true;
}
throw new Error(response.message || '清空失败');
} catch (error) {
console.error('清空云端数据失败:', error);
throw error;
}
};
module.exports = {
uploadTokens,
fetchLatestTokens,
@ -294,5 +572,8 @@ module.exports = {
mergeTokens,
initCloud,
shouldRefreshToken,
refreshToken
refreshToken,
request,
hasValidTokens,
clearAllTokens
};