Files
CloudHome-Ops/lib/utils/credential_storage.dart

135 lines
4.7 KiB
Dart
Raw 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.

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
/// 本地凭据持久化(基于 SharedPreferences,跨平台可靠持久化)。
///
/// 存储内容:
/// - 账号 / 密码(仅当用户勾选「记住密码」时保存)
/// - token / refreshToken / 用户信息(登录成功后保存,用于免登录恢复)
///
/// 历史坑:早期版本使用 dart:io 文件存储 + `Directory.current`,但 Android 上
/// `Directory.current` 指向不可写路径,会静默退化为「仅内存」存储,导致 App 重启后
/// 凭据与登录态全部丢失(表现即「记住账号密码不生效 / 每次都要重新登录」)。
/// 现统一改用 SharedPreferences(应用私有、沙箱隔离、必定可写、跨重启持久)。
class CredentialStorage {
CredentialStorage._();
/// 单例。
static final CredentialStorage instance = CredentialStorage._();
/// SharedPreferences 实例;[init] 完成后非 null(除非极端初始化失败)。
SharedPreferences? _prefs;
/// 是否已尝试初始化(无论成功失败都置 true,避免重复 await 同一 Future)。
bool _initialized = false;
/// 异步预初始化:必须在 runApp 前 await,使下方同步 getter 可读到持久化数据。
static Future<void> init() async {
await instance._init();
}
Future<void> _init() async {
if (_initialized) return;
try {
_prefs = await SharedPreferences.getInstance();
} catch (_) {
// 极端情况下 SharedPreferences 不可用:退化为内存态,不影响启动与登录。
_prefs = null;
} finally {
_initialized = true;
}
}
// ── 账号密码 ──────────────────────────────────────────
/// 是否记住密码。
bool get rememberPassword => _prefs?.getBool(_kRemember) ?? false;
/// 保存的账号(rememberPassword=true 时有值)。
String? get savedAccount => _prefs?.getString(_kAccount);
/// 保存的密码(rememberPassword=true 时有值)。
String? get savedPassword => _prefs?.getString(_kPassword);
/// 保存账号密码(用户勾选「记住密码」并登录成功后调用)。
Future<void> saveCredentials({
required String account,
required String password,
}) async {
final prefs = _prefs;
if (prefs == null) return;
await prefs.setString(_kAccount, account);
await prefs.setString(_kPassword, password);
await prefs.setBool(_kRemember, true);
}
/// 清除保存的账号密码(用户未勾选「记住密码」时调用)。
Future<void> clearCredentials() async {
final prefs = _prefs;
if (prefs == null) return;
await prefs.remove(_kAccount);
await prefs.remove(_kPassword);
await prefs.setBool(_kRemember, false);
}
// ── Token & 用户信息(用于免登录恢复) ──────────────────
/// 保存的 token。
String? get token => _prefs?.getString(_kToken);
/// 保存的 refreshToken。
String? get refreshToken => _prefs?.getString(_kRefreshToken);
/// 保存的用户信息 JSON 字符串。
String? get userJson => _prefs?.getString(_kUserJson);
/// 保存登录态(token + refreshToken + 用户信息)。
Future<void> saveSession({
required String token,
String? refreshToken,
String? userJson,
}) async {
final prefs = _prefs;
if (prefs == null) return;
await prefs.setString(_kToken, token);
if (refreshToken != null && refreshToken.isNotEmpty) {
await prefs.setString(_kRefreshToken, refreshToken);
}
if (userJson != null && userJson.isNotEmpty) {
await prefs.setString(_kUserJson, userJson);
}
}
/// 清除登录态(登出或会话失效时调用)。
Future<void> clearSession() async {
final prefs = _prefs;
if (prefs == null) return;
await prefs.remove(_kToken);
await prefs.remove(_kRefreshToken);
await prefs.remove(_kUserJson);
}
/// 是否有已保存的登录态。
bool get hasSession => (token ?? '').isNotEmpty;
/// 从 JSON 恢复用户信息 Map。
Map<String, dynamic>? get savedUserMap {
final json = userJson;
if (json == null || json.isEmpty) return null;
try {
return jsonDecode(json) as Map<String, dynamic>;
} catch (_) {
return null;
}
}
// key 常量(加前缀避免与 StoreLocalCache 等业务 key 冲突)。
static const String _kRemember = 'cred_remember';
static const String _kAccount = 'cred_account';
static const String _kPassword = 'cred_password';
static const String _kToken = 'cred_token';
static const String _kRefreshToken = 'cred_refreshToken';
static const String _kUserJson = 'cred_userJson';
}