flutter——将token放到本地缓存SharedPreferences
'# Flutter——将Token放到本地缓存SharedPreferences
一、背景与问题
在移动应用开发中,Token是身份验证的关键凭证,通常在用户登录后由服务端返回。为了保持用户登录状态,需要将Token持久化存储。Flutter提供了shared_preferences插件作为本地持久化存储方案,但其背后涉及多层技术原理和安全考量。
本篇文章将深入解析shared_preferences的工作机制,探讨其在实际项目中的适用场景和潜在风险,并通过完整案例展示其使用方式。
二、基本原理
1. 底层实现机制
shared_preferences基于平台特定的存储机制:
- Android:使用
SharedPreferencesAPI,基于Xml文件存储键值对 - iOS:使用
NSUserDefaults,基于plist文件存储键值对 - 通用机制:通过
PlatformData类封装平台差异,提供统一的get/set接口
其核心特点包括:
- 同步/异步操作支持
- 自动持久化机制
- 简单的键值对存储模型
2. 数据存储结构
在Android中,shared_preferences会将数据存储为/data/data/<package_name>/shared_prefs/目录下的.xml文件。每个应用对应一个SharedPreferences文件,通过MODE_PRIVATE模式确保私有访问。
3. 读写流程
读写操作遵循以下流程:
- 调用
get/set方法 - 通过
PlatformData类进行平台适配 - 触发平台特定的存储操作
- 自动处理数据序列化/反序列化
三、环境准备
1. 依赖配置
在pubspec.yaml中添加:
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.0.62. 初始化配置
在main.dart中初始化:
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initSharedPreferences(); // 自定义初始化方法
runApp(MyApp());
}四、核心实现
1. 基础存储与读取
Future<void> saveToken(String token) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', token);
}
Future<String?> getToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('auth_token');
}关键代码解释:
SharedPreferences.getInstance()获取单例实例setString方法会自动处理字符串序列化getString方法返回null表示键不存在
2. 异步操作处理
Future<void> saveTokenWithDelay(String token) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', token);
await Future.delayed(Duration(seconds: 2)); // 模拟耗时操作
}注意事项:
- 长时间阻塞主线程会导致ANR
- 应该使用
Future.microtask或SchedulerBinding进行异步处理
3. 安全存储方案
import 'package:encrypt/encrypt.dart' as encrypt;
Future<void> saveSecureToken(String token) async {
final prefs = await SharedPreferences.getInstance();
final key = 'secure_key';
final encrypted = encrypt.Encrypted(token, encrypt.Encrypter(encrypt.Keys(key)));
await prefs.setString('secure_token', encrypted.toString());
}安全风险分析:
shared_preferences本身不提供加密功能- 若直接存储明文Token,可能被反编译获取
- 建议配合
encrypt库进行加密存储
五、完整案例
1. 登录流程实现
class LoginScreen extends StatefulWidget {
@override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
Future<void> _login() async {
final username = _usernameController.text;
final password = _passwordController.text;
// 模拟网络请求
final response = await NetworkService.login(username, password);
if (response.isSuccess) {
await saveToken(response.token);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => HomeScreen()));
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('登录失败: ${response.message}')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('登录')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(controller: _usernameController, decoration: InputDecoration(labelText: '用户名')),
TextField(controller: _passwordController, decoration: InputDecoration(labelText: '密码')),
SizedBox(height: 16),
ElevatedButton(
onPressed: _login,
child: Text('登录'),
),
],
),
),
);
}
}2. 会话管理实现
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('首页')),
body: Center(
child: FutureBuilder<String?>(
future: getToken(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('当前Token: ${snapshot.data}');
} else if (snapshot.hasError) {
return Text('获取Token失败');
}
return CircularProgressIndicator();
},
),
),
);
}
}3. Token过期检测
Future<void> checkTokenExpiry() async {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('auth_token');
if (token == null) {
// Token不存在,跳转登录页
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => LoginScreen()));
return;
}
// 检查Token有效期(假设有效期为1小时)
final expiry = prefs.getString('token_expiry');
if (expiry == null || DateTime.now().millisecondsSinceEpoch > int.parse(expiry)) {
// Token过期,刷新或跳转
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Token已过期,请重新登录')),
);
}
}六、源码解析
1. SharedPreferences类源码
class SharedPreferences {
static Future<SharedPreferences> getInstance() async {
if (_instance == null) {
_instance = SharedPreferences._internal();
}
return _instance!;
}
Future<void> setString(String key, String value) async {
await _write(key, value);
}
Future<String?> getString(String key) async {
return _read(key);
}
Future<void> _write(String key, String value) async {
// 平台特定的写入逻辑
await _platformData.write(key, value);
}
Future<String?> _read(String key) async {
return _platformData.read(key);
}
}关键点分析:
- 使用单例模式保证全局唯一实例
- 通过
_platformData进行平台适配 - 写入操作会触发持久化存储
2. 平台适配实现
class PlatformData {
Future<void> write(String key, String value) async {
if (Platform.isAndroid) {
await _androidWrite(key, value);
} else if (Platform.isIOS) {
await _iosWrite(key, value);
}
}
Future<String?> read(String key) async {
if (Platform.isAndroid) {
return await _androidRead(key);
} else if (Platform.isIOS) {
return await _iosRead(key);
}
return null;
}
}注意事项:
- 不同平台的存储格式不同(XML vs. Plist)
- 需要处理平台特定的异常情况
七、进阶使用
1. 增量更新策略
Future<void> updateToken(String newToken) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', newToken);
await prefs.setString('token_expiry', DateTime.now().millisecondsSinceEpoch + 3600000); // 1小时
}2. 多环境存储策略
Future<void> saveEnvironmentToken(String token, String environment) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token_$environment', token);
}3. 热重载支持
Future<void> saveTokenWithHotReload(String token) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', token);
}热重载注意事项:
- 热重载不会清除存储数据
- 需要手动刷新数据以生效
八、性能与工程实践
1. 性能优化方案
| 问题 | 解决方案 |
|---|---|
| 频繁读写影响性能 | 使用缓存机制 |
| 大数据量存储 | 使用SQLite替代 |
| 重复键值对 | 使用Map缓存 |
2. 异常处理策略
try {
await saveToken(token);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('保存Token失败: $e')),
);
}3. 安全加固措施
- 使用
encrypt库进行加密存储 - 增加密钥保护机制
- 使用
SecureStorage替代原始存储
4. 异步处理模式
void checkTokenExpiry() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await checkTokenExpiry();
});
}九、常见问题与踩坑
1. 常见错误示例
// 错误:未处理异步操作
Future<void> saveToken(String token) async {
final prefs = await SharedPreferences.getInstance();
prefs.setString('auth_token', token); // 未await
}问题分析: 异步写入未等待可能导致数据丢失
2. 正确写法
Future<void> saveToken(String token) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', token);
}3. 常见陷阱
| 陷阱 | 解决方案 |
|---|---|
| 未处理null值 | 使用??操作符 |
| 类型转换错误 | 使用getString代替直接强转 |
| 平台差异问题 | 使用Platform.isAndroid进行条件判断 |
十、最佳实践
1. 推荐方案
- 存储策略:使用
setString存储Token - 安全策略:结合
encrypt库进行加密 - 生命周期管理:在
initState中读取Token - 异常处理:添加全面的异常捕获
2. 推荐目录结构
lib/
├── data/
│ └── storage.dart # 存储逻辑
├── services/
│ └── auth_service.dart # 认证服务
├── widgets/
│ └── login_screen.dart # 登录界面
└── main.dart # 入口文件3. 推荐开发模式
- 使用
Provider管理Token状态 - 采用
Stream进行实时更新 - 使用
RxDart进行异步处理
十一、总结
在Flutter开发中,shared_preferences是实现本地持久化存储的常用方案,但其背后涉及复杂的平台适配机制和安全考量。通过本文的深入分析,我们可以理解其工作原理、适用场景和潜在风险。
在实际开发中,建议:
- 对于非敏感数据使用
shared_preferences - 对于敏感数据采用加密存储方案
- 对于大数据量使用SQLite等数据库
- 在关键业务逻辑中加入异常处理机制
通过合理的设计和实践,我们可以充分利用shared_preferences的特性,构建稳定、安全的移动应用。
评论已关闭