Files
xianyan/lib/core/services/auth/token_service.dart
Developer 6119918185 release: bump version to 6.6.25+2606241
主要变更:
1. 新增桌面端托盘图标支持深色/浅色主题切换
2. 重构应用锁、动画配置、小组件导航服务职责
3. 修复Riverpod初始化断言、防重复点击、工作台模式残留选中态问题
4. 优化诗词服务、阅读进度、搜索结果空状态体验
5. 完善macOS打包配置与错误静默处理逻辑
6. 新增快速卡片多语言适配与动画退出队列管理
2026-06-24 04:26:50 +08:00

205 lines
6.8 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — Token管理服务
/// 创建时间: 2026-04-28
/// 更新时间: 2026-06-22
/// 作用: Token检测/刷新/自动续期/登录状态守护
/// 上次更新: 新增 silentRefreshIfExpiringSoon 静默刷新方法,
/// 缓存 expires_at 供拦截器在请求前判断是否即将过期
/// ============================================================
import 'dart:async';
import '../../network/api_client.dart';
import '../../storage/secure_storage.dart';
import '../../utils/logger.dart';
class TokenService {
TokenService._();
static final ApiClient _api = ApiClient.instance;
static Timer? _checkTimer;
static const _checkInterval = Duration(minutes: 15);
static const _refreshThresholdSeconds = 3600;
static bool _isRefreshing = false;
// ============================================================
// 静默刷新支持(供 ApiInterceptor 在请求前调用)
// ============================================================
/// 缓存的 Token 过期时间(由 checkToken/refreshToken 更新)
/// 首次启动时为 null等定时检测或首次 checkToken 后填充
static DateTime? _cachedExpiresAt;
/// 静默刷新阈值:提前 5 分钟300秒刷新
static const int _silentRefreshThresholdSeconds = 300;
static void startPeriodicCheck() {
_checkTimer?.cancel();
_checkTimer = Timer.periodic(_checkInterval, (_) async {
await checkAndRefresh();
});
Log.i('Token定时检测已启动间隔: ${_checkInterval.inMinutes}分钟');
}
static void stopPeriodicCheck() {
_checkTimer?.cancel();
_checkTimer = null;
Log.i('Token定时检测已停止');
}
static Future<TokenCheckResult> checkToken() async {
try {
final token = await SecureStorage.authToken;
if (token == null || token.isEmpty) {
return const TokenCheckResult(valid: false, reason: '无Token');
}
final response = await _api.post<Map<String, dynamic>>(
'/api/token/check',
);
final data = response.data as Map<String, dynamic>;
final code = (data['code'] as num?)?.toInt() ?? 0;
if (code == 1) {
final tokenData = data['data'] as Map<String, dynamic>? ?? {};
final expiresIn = (tokenData['expires_in'] as num?)?.toInt() ?? 0;
// 缓存过期时间,供拦截器静默刷新判断使用
if (expiresIn > 0) {
_cachedExpiresAt = DateTime.now().add(Duration(seconds: expiresIn));
}
return TokenCheckResult(
valid: true,
expiresIn: expiresIn,
reason: '有效',
);
}
final reason = data['msg'] as String? ?? '无效';
return TokenCheckResult(valid: false, reason: reason);
} catch (e) {
Log.e('Token检测失败', e);
return const TokenCheckResult(valid: false, reason: '检测异常');
}
}
static Future<bool> refreshToken() async {
if (_isRefreshing) return false;
_isRefreshing = true;
try {
final response = await _api.post<Map<String, dynamic>>(
'/api/token/refresh',
);
final data = response.data as Map<String, dynamic>;
final code = (data['code'] as num?)?.toInt() ?? 0;
if (code == 1) {
final tokenData = data['data'] as Map<String, dynamic>?;
final newToken = tokenData?['token'] as String?;
if (newToken != null && newToken.isNotEmpty) {
await SecureStorage.setAuthToken(newToken);
// 尝试读取并缓存过期时间(若响应中包含 expires_in
final expiresIn = (tokenData?['expires_in'] as num?)?.toInt() ?? 0;
if (expiresIn > 0) {
_cachedExpiresAt = DateTime.now().add(Duration(seconds: expiresIn));
}
Log.i('Token刷新成功');
return true;
}
}
Log.w('Token刷新失败: ${data['msg']}');
return false;
} catch (e) {
Log.e('Token刷新异常', e);
return false;
} finally {
_isRefreshing = false;
}
}
static Future<bool> checkAndRefresh() async {
final result = await checkToken();
if (!result.valid) {
Log.w('Token无效: ${result.reason}');
return false;
}
if (result.expiresIn > 0 && result.expiresIn < _refreshThresholdSeconds) {
Log.i('Token即将过期(${result.expiresIn}秒),自动续期');
return await refreshToken();
}
return true;
}
// ============================================================
// 静默刷新(供 ApiInterceptor 在请求前调用)
// ============================================================
/// 静默刷新即将过期的 Token
///
/// 由 [ApiInterceptor] 在每次请求前调用fire-and-forget不阻塞当前请求
/// 基于缓存的 [_cachedExpiresAt] 判断是否即将过期(提前 5 分钟),
/// 若即将过期则调用 [refreshToken] 静默刷新。
///
/// 并发安全:[refreshToken] 内部有 [_isRefreshing] 锁,
/// 多个请求同时触发时只会刷新一次,其余直接返回 false。
///
/// 刷新失败不阻塞:返回 false让原 Token 继续使用直到 401
/// 由拦截器的 401 处理逻辑引导用户重新登录。
///
/// 注意:首次启动时 [_cachedExpiresAt] 为 null等定时检测填充
/// 此方法会直接返回 false不会触发刷新。
static Future<bool> silentRefreshIfExpiringSoon() async {
// 没有缓存的过期时间,无法判断,跳过
if (_cachedExpiresAt == null) return false;
final now = DateTime.now();
final remaining = _cachedExpiresAt!.difference(now).inSeconds;
// 剩余时间大于阈值,无需刷新
if (remaining > _silentRefreshThresholdSeconds) return false;
Log.i(
'Token即将过期(剩余${remaining}秒),触发静默刷新',
null,
null,
LogCategory.auth,
);
final success = await refreshToken();
if (success) {
// 刷新成功后,如果缓存未更新(响应中没有 expires_in
// 设置一个默认过期时间2小时避免后续请求重复触发刷新。
// 定时检测checkAndRefresh会随后更新为准确值。
if (_cachedExpiresAt == null ||
_cachedExpiresAt!.isBefore(
now.add(const Duration(seconds: _silentRefreshThresholdSeconds)),
)) {
_cachedExpiresAt = now.add(const Duration(hours: 2));
Log.i(
'Token刷新成功使用默认过期时间(2小时)',
null,
null,
LogCategory.auth,
);
}
}
return success;
}
static bool get isRunning => _checkTimer != null;
}
class TokenCheckResult {
const TokenCheckResult({
required this.valid,
this.expiresIn = 0,
this.reason = '',
});
final bool valid;
final int expiresIn;
final String reason;
}