fix(client): 切换账号显示上一账号数据——登录/登出按会话纪元重建 ProviderScope
张三登录后看到汤宇飞的订单列表(含已删行):业务 provider 的 notifier 实例 与 _cache 兜底缓存跨账号存活,拉取失败时把上一账号数据端给新账号(10 个 列表 provider 同款模式)。根治:会话纪元 sessionEpoch(Riverpod 体系外 ValueNotifier)作 ProviderScope 的 key,login/logout 后自增重建整棵状态树。 logout 的撤销会话 POST 改为不阻塞,确保清盘先于 scope 重建,防止新 scope restore 读到旧 token 把上一账号登回来。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../config/app_constants.dart';
|
||||
import 'session_epoch.dart';
|
||||
|
||||
const _kAccessToken = 'access_token';
|
||||
const _kRefreshToken = 'refresh_token';
|
||||
@@ -118,6 +121,9 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
});
|
||||
state = AuthState(initialized: true, user: user);
|
||||
debugPrint('[Auth] login() state set, isLoggedIn=${state.isLoggedIn}');
|
||||
// 持久化与状态就绪后重建 ProviderScope(防跨账号内存残留)。
|
||||
// 新 scope 的 restore() 从刚写入的 SharedPreferences 恢复本会话。
|
||||
sessionEpoch.value++;
|
||||
}
|
||||
|
||||
/// 续期后写回令牌。后端会**轮换 refresh token**(jti 轮换 + 盗用检测),
|
||||
@@ -157,18 +163,19 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
// 先作废在途的令牌持久化并立即本地登出,使任何并发的续期写入在落盘前被丢弃。
|
||||
_sessionGen++;
|
||||
state = const AuthState(initialized: true);
|
||||
// 尽力通知后端撤销当前会话(离线/失败均忽略,不阻塞本地登出)
|
||||
// 尽力通知后端撤销当前会话(离线/失败均忽略)。不 await:
|
||||
// ① 不阻塞本地登出;② 不推迟下方的清盘与 scope 重建——若重建先于清盘,
|
||||
// 新 scope 的 restore() 会读到旧 token 把上一账号自动登回来。
|
||||
if (token != null && token.isNotEmpty) {
|
||||
try {
|
||||
await Dio(BaseOptions(
|
||||
baseUrl: AppConfig.apiBaseUrl,
|
||||
connectTimeout: AppConstants.authProbeTimeout,
|
||||
receiveTimeout: AppConstants.authProbeTimeout,
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
)).post('/auth/logout');
|
||||
} catch (_) {
|
||||
unawaited(Dio(BaseOptions(
|
||||
baseUrl: AppConfig.apiBaseUrl,
|
||||
connectTimeout: AppConstants.authProbeTimeout,
|
||||
receiveTimeout: AppConstants.authProbeTimeout,
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
)).post('/auth/logout').catchError((Object _) {
|
||||
// ignore: 后端不可达或会话已失效都无所谓
|
||||
}
|
||||
return Response<dynamic>(requestOptions: RequestOptions());
|
||||
}));
|
||||
}
|
||||
// 经队列串行清空 token,排在任何先前入队的续期写入之后,确保最终为已登出状态。
|
||||
await _runStorage(() async {
|
||||
@@ -182,6 +189,8 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
await prefs.remove(_kShopId);
|
||||
await prefs.remove(_kRole);
|
||||
});
|
||||
// 清盘完成后重建 ProviderScope,抹掉上一账号的全部内存态。
|
||||
sessionEpoch.value++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// 会话纪元:每次显式登录 / 登出成功后自增(见 AuthNotifier.login/logout)。
|
||||
///
|
||||
/// main.dart 以其为 ProviderScope 的 key —— 纪元变化即整棵 Riverpod 状态树销毁重建,
|
||||
/// 所有业务 provider(含 notifier 实例字段里的分页 / 筛选 / _cache 兜底缓存)随账号
|
||||
/// 切换彻底清零,杜绝跨账号内存残留(如订单列表显示上一账号数据的泄漏)。
|
||||
///
|
||||
/// 必须在 Riverpod 体系外(scope 重建时它自身要存活),故用裸 ValueNotifier。
|
||||
/// 只在 login()/logout() 主动 bump;启动恢复持久化会话(restore())不 bump,
|
||||
/// 否则「重建 → restore → 又 bump」死循环。
|
||||
final ValueNotifier<int> sessionEpoch = ValueNotifier(0);
|
||||
+11
-1
@@ -6,6 +6,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart';
|
||||
import 'core/auth/auth_state.dart';
|
||||
import 'core/auth/session_epoch.dart';
|
||||
import 'core/config/app_info.dart';
|
||||
import 'core/errors/error_reporter.dart';
|
||||
import 'core/platform/single_instance.dart' as single_instance;
|
||||
@@ -40,7 +41,16 @@ void main() {
|
||||
}
|
||||
|
||||
await AppInfo.load(); // 从配置文件加载「关于我们」品牌信息
|
||||
runApp(const ProviderScope(child: JiuApp()));
|
||||
// 会话纪元变化(登录/登出)→ 换 key 重建整个 ProviderScope:
|
||||
// 所有业务 provider 状态(分页/筛选/notifier 内缓存)随账号切换彻底清零,
|
||||
// 杜绝跨账号内存残留。auth 会话由新 scope 的 restore() 从持久化恢复。
|
||||
runApp(ValueListenableBuilder<int>(
|
||||
valueListenable: sessionEpoch,
|
||||
builder: (_, epoch, __) => ProviderScope(
|
||||
key: ValueKey(epoch),
|
||||
child: const JiuApp(),
|
||||
),
|
||||
));
|
||||
},
|
||||
(error, stack) {
|
||||
debugPrint('═══ Zone Error ═══════════════════════════════');
|
||||
|
||||
Reference in New Issue
Block a user