1. 双Token机制
json
{
"code": 0,
"data": {
"userId": 305,
"accessToken": "1c56f701051547bca956140f6c73a189",
"refreshToken": "3a9b5ac0eeed4d93ae67d3d0b0fb8d42",
"expiresTime": 1757122540160,
"openid": null
},
"msg": ""
}
作为一个初学者,我知道在发送网络请求中,我们需要在请求头中携带一个Token
身份令牌;当接触到真实的后端接口后,为什么会有两个Token
呢? 这种设计模式被称为 "访问令牌 + 刷新令牌"
机制。
java
1. Access Token (访问令牌)
是什么:就像一把一次性的门禁卡或电影票。
用途:客户端(Flutter App) 用它来访问受保护的资源(比如获取用户信息、发布内容、访问付费 API 等)。每次向服务器请求数据时,都需要在 HTTP Header(通常是 Authorization: Bearer <access_token>)中带上它,服务器会验证这个令牌是否有效且有权访问所求资源。
特点:
生命周期短:从响应数据中 "expiresTime":1757040344156 可以看出,这个令牌有一个明确的过期时间(可能几小时或几天后)。这是一个时间戳,对应北京时间 2025-09-05 10:45:44。
权限高:直接代表着用户的授权。
暴露风险高:因为它经常在网络请求中传输。
2. Refresh Token (刷新令牌)
是什么:就像一把用来补办门禁卡的钥匙或你的身份证。
用途:唯一的目的就是在 Access Token 过期后,用它去申请一个新的 Access Token。它本身不能用来访问任何业务 API。
特点:
生命周期长:它的有效期比 Access Token 长得多(可能是几周、几个月,或者直到用户主动注销)。
权限低:它只能用来换新的 Access Token,不能做别的事。
暴露风险低:它只会在一种非常特定的请求(刷新令牌)中发送,传输频率极低。
设计优点:
java
1. 安全性 (Security)
想象一下,如果只有一个长期有效的 Access Token:
一旦这个 Token 被黑客拦截或窃取(比如通过不安全的网络、日志泄露等),黑客就可以永久地冒充用户,为所欲为,因为令牌永远不会失效。
而使用双 Token 机制:
即使 Access Token 被窃取,因为它有效期很短,黑客能作恶的时间窗口也非常有限(可能只剩几个小时)。
Refresh Token 很少在网络上传送,所以被窃取的风险要低得多。即使 Access Token 泄露,攻击者也无法获取新的 Token,因为刷新需要 Refresh Token。
服务器可以有一个黑名单机制。如果发现异常行为(比如一个 Refresh Token 在短时间内从两个不同国家请求新 Access Token),服务器可以立即让这个 Refresh Token 失效,从而保护用户账户。这比撤销一个长期有效的 Access Token 要容易和高效得多。
2. 用户体验 (User Experience)
如果 Access Token 过期后,只让用户重新登录:
用户可能正在使用 App,突然就被踢了出去,需要重新输入用户名和密码,体验非常差。
而有了 Refresh Token:
应用可以在用户无感知的情况下(静默地)用 Refresh Token 去获取一个新的 Access Token,然后继续之前的操作。
用户只有在 Refresh Token 也过期(或者被服务器主动撤销)时,才需要重新登录。这大大延长了用户的"登录会话"时间,提升了体验。
然后将怎么使用呢?
我们把Token的过期时间也保存下来,每次打开应用后对比当前时间戳与过期的时间戳(当前时间戳 > 存储的时间戳)说明Token过期,请求刷新Token接口,传入刷新令牌
(当然在过期之间刷新)。
之后我们将要考虑存储的安全性,我来学习一下flutter_secure_storage
加密存储插件看看怎么使用。
2. flutter_secure_storage
加密存储
以下内容还未来得及尝试:
1. 添加依赖
在 pubspec.yaml
文件中添加依赖:
dart
dependencies:
flutter_secure_storage: ^8.0.0
运行 flutter pub get
安装包。
2. Android 配置
在 android/app/build.gradle
文件中,确保 minSdkVersion
至少为 18:
gradle
android {
defaultConfig {
minSdkVersion 18 // 或更高版本
// ... 其他配置
}
}
3. 基本使用方法
导入包
dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
创建存储实例(针对 Android 和 iOS 优化)
dart
// 创建适用于 Android 和 iOS 的存储实例
final storage = FlutterSecureStorage(
aOptions: const AndroidOptions(
encryptedSharedPreferences: true, // 在 Android 上使用更安全的EncryptedSharedPreferences
),
iOptions: const IOSOptions(
accessibility: KeychainAccessibility.first_unlock, // 设备首次解锁后可用
),
);
常用操作示例
dart
// 写入数据
await storage.write(key: 'access_token', value: 'your_token_here');
await storage.write(key: 'user_id', value: '12345');
// 读取数据
String? token = await storage.read(key: 'access_token');
String? userId = await storage.read(key: 'user_id');
// 读取所有值
Map<String, String> allValues = await storage.readAll();
// 删除单个值
await storage.delete(key: 'access_token');
// 删除所有值
await storage.deleteAll();
// 检查键是否存在
bool exists = await storage.containsKey(key: 'access_token');
4. 封装成工具类(推荐)
为了更好地组织代码,建议创建一个工具类:
dart
class SecureStorage {
static final SecureStorage _instance = SecureStorage._internal();
factory SecureStorage() => _instance;
SecureStorage._internal();
final FlutterSecureStorage _storage = const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
);
// 存储访问令牌
Future<void> saveAccessToken(String token) async {
await _storage.write(key: 'access_token', value: token);
}
// 获取访问令牌
Future<String?> getAccessToken() async {
return await _storage.read(key: 'access_token');
}
// 存储刷新令牌
Future<void> saveRefreshToken(String token) async {
await _storage.write(key: 'refresh_token', value: token);
}
// 获取刷新令牌
Future<String?> getRefreshToken() async {
return await _storage.read(key: 'refresh_token');
}
// 清除所有存储的数据
Future<void> clearAll() async {
await _storage.deleteAll();
}
// 检查用户是否已登录(是否有访问令牌)
Future<bool> isLoggedIn() async {
return await _storage.containsKey(key: 'access_token');
}
}
5. 在应用中使用
dart
// 在登录成功后保存令牌
void onLoginSuccess(String accessToken, String refreshToken) async {
final secureStorage = SecureStorage();
await secureStorage.saveAccessToken(accessToken);
await secureStorage.saveRefreshToken(refreshToken);
}
// 在应用启动时检查登录状态
void checkLoginStatus() async {
final secureStorage = SecureStorage();
bool isLoggedIn = await secureStorage.isLoggedIn();
if (isLoggedIn) {
String? token = await secureStorage.getAccessToken();
// 使用令牌进行API调用
} else {
// 导航到登录页面
}
}
// 登出时清除数据
void onLogout() async {
final secureStorage = SecureStorage();
await secureStorage.clearAll();
}
注意事项
- Android 备份 :默认情况下,Android 会备份数据到 Google Drive,这可能会导致安全问题和异常。建议在
AndroidManifest.xml
中禁用自动备份或排除安全存储数据:
xml
<application
android:allowBackup="false"
...>
<!-- 或者 -->
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
...>
创建 android/app/src/main/res/xml/backup_rules.xml
:
xml
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="sharedpref" path="FlutterSecureStorage.xml"/>
</full-backup-content>
- iOS 钥匙串访问组:如果您需要在多个应用间共享钥匙串数据,可能需要配置钥匙串访问组。但对于大多数单应用场景,不需要额外配置。
- 错误处理:在实际应用中,应该为所有存储操作添加适当的错误处理:
dart
try {
await storage.write(key: 'key', value: 'value');
} catch (e) {
print('存储数据时出错: $e');
// 处理错误,例如使用备用存储方案
}
这样,您就可以在 Android
和 iOS
应用中使用 flutter_secure_storage
安全地存储敏感数据了。这个插件会自动为每个平台使用适当的安全存储机制(Android
的 Keystore/EncryptedSharedPreferences
和 iOS
的 Keychain
)。
3. 真实测试
代码:
main.dart
dart
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage_example/secure_storage.dart';
void main() {
runApp(const MyApp());
}
///
class MyApp extends StatelessWidget {
///
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: StorageTestPage(),
);
}
}
///
class StorageTestPage extends StatefulWidget {
///
const StorageTestPage({super.key});
@override
State<StorageTestPage> createState() => _StorageTestPageState();
}
class _StorageTestPageState extends State<StorageTestPage> {
final SecureStorage storage = SecureStorage();
String _accessToken = '';
String _refreshToken = '';
bool _isLoggedIn = false;
final TextEditingController _accessController = TextEditingController();
final TextEditingController _refreshController = TextEditingController();
Future<void> _saveTokens() async {
await storage.saveAccessToken(_accessController.text);
await storage.saveRefreshToken(_refreshController.text);
await _readTokens();
}
Future<void> _readTokens() async {
final access = await storage.getAccessToken() ?? 'null';
final refresh = await storage.getRefreshToken() ?? 'null';
final loggedIn = await storage.isLoggedIn();
setState(() {
_accessToken = access;
_refreshToken = refresh;
_isLoggedIn = loggedIn;
});
}
Future<void> _clearTokens() async {
await storage.clearAll();
await _readTokens();
}
@override
void initState() {
super.initState();
_readTokens();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('SecureStorage 测试页面')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _accessController,
decoration: const InputDecoration(labelText: 'Access Token'),
),
TextField(
controller: _refreshController,
decoration: const InputDecoration(labelText: 'Refresh Token'),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: _saveTokens,
child: const Text('保存'),
),
ElevatedButton(
onPressed: _readTokens,
child: const Text('读取'),
),
ElevatedButton(
onPressed: _clearTokens,
child: const Text('清除'),
),
],
),
const Divider(height: 32),
Text('Access Token: $_accessToken'),
Text('Refresh Token: $_refreshToken'),
Text('是否已登录: $_isLoggedIn'),
],
),
),
);
}
}
secure_storage.dart
dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
/// 存储及工具类
class SecureStorage {
/// 单例模式
factory SecureStorage() => _instance;
/// 构造函数
SecureStorage._internal();
static final SecureStorage _instance = SecureStorage._internal();
final FlutterSecureStorage _storage = const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
);
/// 存储访问令牌
Future<void> saveAccessToken(String token) async {
await _storage.write(key: 'access_token', value: token);
}
/// 获取访问令牌
Future<String?> getAccessToken() async {
return _storage.read(key: 'access_token');
}
/// 存储刷新令牌
Future<void> saveRefreshToken(String token) async {
await _storage.write(key: 'refresh_token', value: token);
}
/// 获取刷新令牌
Future<String?> getRefreshToken() async {
return _storage.read(key: 'refresh_token');
}
/// 清除所有存储的数据
Future<void> clearAll() async {
await _storage.deleteAll();
}
/// 检查用户是否已登录(是否有访问令牌)
Future<bool> isLoggedIn() async {
return _storage.containsKey(key: 'access_token');
}
}
如何在模拟器上看加密存储后的数据,在项目终端依次输入以下命令:
bash
adb shell
run-as com.example.flutter_secure_storage_example
cd shared_prefs
cat FlutterSecureStorage.xml
然后得到

xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="__androidx_security_crypto_encrypted_prefs_key_keyset__">12a701124456bc5290290e354c5a1d61088990a67618ca5eded893b2cc5a61b2cce225445547fca6e3b2d3d3591f37acb9d85b568cab9a0c35ee2dab3caa5e6ce98863e62c074cb169beac7260bfc13c66191b5a509e1862d84ce2785df93771a3641728bba67fefb4089668b9d0f74695d5e748b44bf8aecb5fea5bec3be56a22377acd903e588a1171b81b70df615a769788c6dd6120b18ffd81d32329b6e6777dcd82e9c5620643ab1a4208fb86c627123b0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e4165735369764b6579100118fb86c6272001</string>
<string name="__androidx_security_crypto_encrypted_prefs_value_keyset__">128801af0f51731b0a553507d2cc8777a4386d301275860c4821386fbee7b5c31f6750e26655fb37d1ea4a9e1beb2248c705cd8184662ff4fe9b791749b5525a6359db351f36ee8db4b54fb6c598579078425e57aec4302ba11999fd131a72f1a3cf7627070bba73ad9e88f9219b2040b1d00d51c99c891bc11b4db8a300971f2d63b862488635a18cbfc91a4408ff8087f503123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e41657347636d4b6579100118ff8087f5032001</string>
<string name="AQTxg3tF/FbN5ojqZADBaV4DLVEt4sJW5tfzZgKCO9zeZFkQ3en8tTs/rYf7JDwDBM5brlmGzyNVjCvL/yUCacSG9Hy6q80Fmp1pyi01EFkwYrY/sl74Ew==">AT6hwH8xjjMZjux6626D8XCiAOfbcCKiE8EUGuBovF9uvHc/uAuh4D1RR3zNxQ==</string>
<string name="AQTxg3uwgoF/Qw+tcio/UNFY34OFIXBFBeg52C7qZ45TSSD4hgorX+gVN5mW5esTDFypJW9goK/IDxIaQbJXS9GC5+ceBFe/2spzvDW444tq4uVkA84NYMI=">AT6hwH/h+qRydKtr1IIZu0yf3L/WugEuYdod+FvMDRdarepExadPOwzsonjq9g==</string>
</map>
上边的数据构造如下:
dart
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="__androidx_security_crypto_encrypted_prefs_key_keyset__">...</string>
<string name="__androidx_security_crypto_encrypted_prefs_value_keyset__">...</string>
<string name="加密后的 key">加密后的 value</string>
<string name="加密后的 key">加密后的 value</string>
</map>
证明我们加密存储成功