diff --git a/app/lib/data/remote/api_client.dart b/app/lib/data/remote/api_client.dart index b65eb5d..224a402 100644 --- a/app/lib/data/remote/api_client.dart +++ b/app/lib/data/remote/api_client.dart @@ -1,8 +1,9 @@ -// API 客户端 — Dio 封装 + JWT 注入 + 离线感知 +// API 客户端 — Dio 封装 + JWT 注入 + Token 自动刷新 + 离线感知 // // 核心职责: // - 封装 Dio HTTP 客户端,统一配置超时和头信息 // - JWT token 自动注入(请求拦截器) +// - 401 自动刷新 token + 重试原请求(审计 9a-AUTH-01) // - 离线状态感知(网络不可用时抛出明确异常) // - 为 SyncEngine 提供远程操作能力 @@ -26,6 +27,15 @@ class ApiClient { /// 基础 URL,默认指向本地开发服务器 final String baseUrl; + /// Token 刷新回调 — 由 AuthRepository 在构造后注册 + /// + /// 返回新的 access token,失败返回 null。 + /// 使用回调模式避免 ApiClient ↔ AuthRepository 循环依赖。 + Future Function()? onRefreshToken; + + /// 是否正在刷新 token(防止并发 401 触发多次刷新) + bool _isRefreshing = false; + ApiClient({this.baseUrl = 'http://localhost:3000/api/v1'}) { _dio = Dio(BaseOptions( baseUrl: baseUrl, @@ -48,11 +58,37 @@ class ApiClient { }, )); - // 响应拦截器:统一错误处理 + // 响应拦截器:401 自动刷新 token + 重试 _dio.interceptors.add(InterceptorsWrapper( - onError: (error, handler) { - // 401 时自动清除 token(需要重新登录) + onError: (error, handler) async { if (error.response?.statusCode == 401) { + // 不对刷新端点本身重试(避免无限循环) + final isRefreshRequest = + error.requestOptions.path.endsWith('/auth/refresh'); + + if (!isRefreshRequest && + onRefreshToken != null && + !_isRefreshing) { + _isRefreshing = true; + try { + final newToken = await onRefreshToken!(); + if (newToken != null) { + _token = newToken; + // 用新 token 重试原始请求 + error.requestOptions.headers['Authorization'] = + 'Bearer $newToken'; + _isRefreshing = false; + return handler.resolve( + await _dio.fetch(error.requestOptions), + ); + } + } catch (_) { + // 刷新失败,继续走 401 逻辑 + } + _isRefreshing = false; + } + + // 刷新失败或无刷新回调 → 清除 token _token = null; } handler.next(error); diff --git a/app/lib/data/repositories/auth_repository.dart b/app/lib/data/repositories/auth_repository.dart index 62bb97e..5d349b1 100644 --- a/app/lib/data/repositories/auth_repository.dart +++ b/app/lib/data/repositories/auth_repository.dart @@ -47,7 +47,10 @@ class AuthRepository { required ApiClient apiClient, required SecureTokenStore tokenStore, }) : _apiClient = apiClient, - _tokenStore = tokenStore; + _tokenStore = tokenStore { + // 注册 token 自动刷新回调 — 401 时 ApiClient 自动调用 + _apiClient.onRefreshToken = _handleAutoRefresh; + } /// 当前用户(可能为 null) User? get currentUser => _currentUser; @@ -215,6 +218,33 @@ class AuthRepository { } } + // ===== Token 自动刷新 ===== + + /// ApiClient 401 拦截器调用的自动刷新处理 + /// + /// 使用 refresh_token 获取新 access_token,更新 ApiClient 的 token, + /// 返回新 access_token(失败返回 null)。 + Future _handleAutoRefresh() async { + if (_currentToken == null) return null; + + _logger.d('自动刷新令牌(401 触发)'); + try { + final response = await _apiClient.post('/auth/refresh', data: { + 'refresh_token': _currentToken!.refreshToken, + }); + + final data = _extractData(response.data); + final token = AuthToken.fromJson(data); + + await _saveToken(token); + _logger.i('自动刷新令牌成功'); + return token.accessToken; + } catch (e) { + _logger.w('自动刷新令牌失败: $e'); + return null; + } + } + // ===== 私有方法 ===== /// 从 API 响应中提取 data 字段