diff --git a/src/services/api.ts b/src/services/api.ts index 96f28e1..98e5fff 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -2,6 +2,7 @@ * API 客户端 * 使用 fetch API 进行 HTTP 请求 * 支持请求/响应拦截器 + * 支持主动刷新 token(快过期时自动刷新) */ import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -24,6 +25,9 @@ const SSE_URL = `${BASE_URL.replace(/\/+$/, '')}/realtime/sse`; const TOKEN_KEY = 'auth_token'; const REFRESH_TOKEN_KEY = 'refresh_token'; +// Token 提前刷新阈值(秒):提前 5 分钟刷新 +const TOKEN_REFRESH_THRESHOLD_SECONDS = 5 * 60; + // API 响应格式 export interface ApiResponse { code: number; @@ -53,10 +57,21 @@ export class ApiError extends Error { } } +// JWT Payload 结构 +interface JwtPayload { + user_id: string; + username: string; + exp?: number; + iat?: number; + iss?: string; +} + // API 客户端类 class ApiClient { private baseUrl: string; private navigation: any = null; + // 刷新锁:防止并发刷新 + private refreshLock: Promise | null = null; constructor(baseUrl: string) { this.baseUrl = baseUrl; @@ -67,6 +82,33 @@ class ApiClient { this.navigation = navigation; } + // 解析 JWT token 获取 payload + private parseJwt(token: string): JwtPayload | null { + try { + const parts = token.split('.'); + if (parts.length !== 3) { + return null; + } + // React Native/Expo 环境使用 atob + const payload = JSON.parse(atob(parts[1])); + return payload as JwtPayload; + } catch { + return null; + } + } + + // 检查 token 是否快过期(返回 true 表示需要刷新) + private isTokenExpiringSoon(token: string): boolean { + const payload = this.parseJwt(token); + if (!payload || !payload.exp) { + return false; + } + const now = Math.floor(Date.now() / 1000); + const remaining = payload.exp - now; + // 剩余时间小于阈值时需要刷新 + return remaining < TOKEN_REFRESH_THRESHOLD_SECONDS && remaining > 0; + } + // 获取 token async getToken(): Promise { try { @@ -137,10 +179,33 @@ class ApiClient { 'Content-Type': 'application/json', }; - // 添加认证 token + // 添加认证 token(主动刷新快过期的 token) const token = await this.getToken(); if (token) { - headers['Authorization'] = `Bearer ${token}`; + // 检查 token 是否快过期,如果是则主动刷新 + if (this.isTokenExpiringSoon(token)) { + const refreshed = await this.refreshToken(); + if (!refreshed) { + // 刷新失败,清除 token 并跳转登录 + await this.clearToken(); + if (this.navigation) { + this.navigation.dispatch( + CommonActions.reset({ + index: 0, + routes: [{ name: 'Login' }], + }) + ); + } + throw new ApiError(401, '登录已过期,请重新登录'); + } + // 刷新成功,使用新 token + const newToken = await this.getToken(); + if (newToken) { + headers['Authorization'] = `Bearer ${newToken}`; + } + } else { + headers['Authorization'] = `Bearer ${token}`; + } } // 构建请求配置 @@ -230,8 +295,27 @@ class ApiClient { } } - // 刷新 token + // 刷新 token(带锁,防止并发刷新) private async refreshToken(): Promise { + // 如果已有刷新操作正在进行,等待其完成 + if (this.refreshLock) { + return this.refreshLock; + } + + // 创建刷新锁 + this.refreshLock = this.doRefreshToken(); + + try { + const result = await this.refreshLock; + return result; + } finally { + // 释放刷新锁 + this.refreshLock = null; + } + } + + // 实际执行刷新 token + private async doRefreshToken(): Promise { try { const refreshToken = await this.getRefreshToken(); if (!refreshToken) { @@ -242,8 +326,10 @@ class ApiClient { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${refreshToken}`, }, + body: JSON.stringify({ + refresh_token: refreshToken, + }), }); if (!response.ok) { @@ -309,10 +395,22 @@ class ApiClient { }); } + // 获取 token 并检查是否需要刷新 const token = await this.getToken(); const headers: HeadersInit = {}; if (token) { - headers['Authorization'] = `Bearer ${token}`; + // 检查 token 是否快过期 + if (this.isTokenExpiringSoon(token)) { + const refreshed = await this.refreshToken(); + if (refreshed) { + const newToken = await this.getToken(); + if (newToken) { + headers['Authorization'] = `Bearer ${newToken}`; + } + } + } else { + headers['Authorization'] = `Bearer ${token}`; + } } const response = await fetch(`${this.baseUrl}${path}`, { diff --git a/src/services/authService.ts b/src/services/authService.ts index c32f953..0311b8b 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -237,7 +237,14 @@ class AuthService { // 刷新Token async refreshToken(): Promise { try { - const response = await api.post('/auth/refresh'); + const refreshToken = await api.getRefreshToken(); + if (!refreshToken) { + return null; + } + + const response = await api.post('/auth/refresh', { + refresh_token: refreshToken, + }); if (!response.data) { return null; @@ -245,8 +252,9 @@ class AuthService { if (response.data.token) { await api.setToken(response.data.token); } - if (response.data.refreshToken) { - await api.setRefreshToken(response.data.refreshToken); + const newRefreshToken = response.data.refresh_token || response.data.refreshToken; + if (newRefreshToken) { + await api.setRefreshToken(newRefreshToken); } return response.data;