Flutter安全存储插件flutter_secure_storage_x的使用
Flutter安全存储插件flutter_secure_storage_x的使用
flutter_secure_storage_x
是一个用于在 Flutter 应用中安全存储数据的插件。它使用不同的机制在不同平台上存储数据:
- iOS: 使用 Keychain
- Android: 使用 AES 加密,AES 密钥使用 RSA 加密并存储在 KeyStore 中
- Linux: 使用
libsecret
- macOS: 使用 Keychain
- Web: 使用 WebCrypto API
重要通知
Android 重要通知
从 v5.0.0 开始,可以使用 EncryptedSharedPreferences
来存储数据。但需要注意的是,Jetpack 安全加密库已弃用,推荐使用默认实现或 DataStore。
AndroidOptions _getAndroidOptions() => const AndroidOptions(
dataStore: true,
);
如果要从 EncryptedSharedPreferences
迁移到 DataStore,可以使用以下选项:
AndroidOptions _getAndroidOptions() => const AndroidOptions(
encryptedSharedPreferences: true,
dataStore: true,
);
注意:对 EncryptedSharedPreferences
的支持计划在 v11 中移除。
Web 重要通知
flutter_secure_storage
只能在 HTTPS 或 localhost 环境下工作。请参阅 此问题 获取更多信息。
平台实现
平台 | read | write | delete | containsKey | readAll | deleteAll | isCupertinoProtectedDataAvailable | onCupertinoProtectedDataAvailabilityChanged |
---|---|---|---|---|---|---|---|---|
Android | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ||
iOS | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (macOS 12 及以上) |
Windows | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ||
Linux | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ||
macOS | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (macOS 12 及以上) |
Web | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
入门指南
在使用 MethodChannel
之前,请确保调用了 WidgetsFlutterBinding.ensureInitialized()
。详细信息请参阅 此问题。
import 'package:flutter_secure_storage_x/flutter_secure_storage_x.dart';
// 创建存储实例
final storage = FlutterSecureStorage();
// 读取值
String value = await storage.read(key: key);
// 读取所有值
Map<String, String> allValues = await storage.readAll();
// 删除值
await storage.delete(key: key);
// 删除所有值
await storage.deleteAll();
// 写入值
await storage.write(key: key, value: value);
配置
配置 Android 版本
在 [project]/android/app/build.gradle
中设置 minSdkVersion
为 >= 23。
android {
...
defaultConfig {
...
minSdkVersion 23
...
}
}
注意:默认情况下,Android 会将数据备份到 Google Drive,这可能导致 java.security.InvalidKeyException: Failed to unwrap key
异常。需要禁用自动备份并排除 FlutterSecureStorage
使用的共享偏好设置。详情请参阅 禁用自动备份 和 排除共享偏好设置。
配置 Web 版本
flutter_secure_storage
使用实验性的 WebCrypto 实现。请确保启用了 HTTP 严格前向安全性,并在响应中应用了适当的头部。详情请参阅 MDN 文档 和 NetSparker 文档。
配置 Linux 版本
需要在机器上安装 libsecret-1-dev
和 libjsoncpp-dev
,并在运行应用程序时安装 libsecret-1-0
和 libjsoncpp1
。如果使用 snapcraft 构建项目,可以使用以下配置:
parts:
uet-lms:
source: .
plugin: flutter
flutter-target: lib/main.dart
build-packages:
- libsecret-1-dev
- libjsoncpp-dev
stage-packages:
- libsecret-1-0
- libjsoncpp-dev
除了 libsecret
,还需要一个密钥环服务,例如 gnome-keyring
(Gnome 用户)或 ksecretsservice
(KDE 用户),或其他轻量级提供者如 secret-service。
配置 macOS 版本
需要在 macOS 跑步器中添加 Keychain 共享功能。请在 macos/Runner/DebugProfile.entitlements
和 macos/Runner/Release.entitlements
中添加以下内容:
<key>keychain-access-groups</key>
<array/>
如果您的应用程序使用了 App Groups,则需要在 keychain-access-groups
参数中添加 App Group 的名称。例如,如果您的 App Group 名称为 “aoeu”,则值应为:
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)aoeu</string>
</array>
配置 Windows 版本
需要安装 C++ ATL 库以及 Visual Studio 构建工具。请从 这里 下载并确保安装了 C++ ATL。
集成测试
在 example
目录下运行以下命令:
flutter drive --target=test_driver/app.dart
贡献
如果您想贡献代码,请克隆仓库并使用 melos
初始化工作区:
flutter pub get
melos bootstrap
示例代码
以下是一个完整的示例代码,展示了如何使用 flutter_secure_storage_x
插件:
import 'dart:math' show Random;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage_x/flutter_secure_storage_x.dart';
import 'package:platform/platform.dart';
void main() {
runApp(
const MaterialApp(
home: ItemsWidget(),
),
);
}
enum _Actions {
deleteAll,
isProtectedDataAvailable,
}
enum _ItemActions {
delete,
edit,
containsKey,
read,
}
class ItemsWidget extends StatefulWidget {
const ItemsWidget({super.key});
@override
State<ItemsWidget> createState() => _ItemsWidgetState();
}
class _ItemsWidgetState extends State<ItemsWidget> {
final _storage = const FlutterSecureStorage();
final _accountNameController = TextEditingController(
text: 'flutter_secure_storage_service',
);
final _items = <_SecItem>[];
@override
void initState() {
super.initState();
_accountNameController.addListener(_readAll);
Future(() async {
await _readAll();
});
}
@override
void dispose() {
_accountNameController.removeListener(_readAll);
_accountNameController.dispose();
super.dispose();
}
Future<void> _readAll() async {
final all = await _storage.readAll(
iOptions: _getIOSOptions(),
aOptions: _getAndroidOptions(),
);
setState(() {
_items
..clear()
..addAll(all.entries.map((e) => (key: e.key, value: e.value)))
..sort((a, b) => int.parse(a.key).compareTo(int.parse(b.key)));
});
}
Future<void> _deleteAll() async {
await _storage.deleteAll(
iOptions: _getIOSOptions(),
aOptions: _getAndroidOptions(),
);
await _readAll();
}
Future<void> _isProtectedDataAvailable() async {
final scaffoldMessenger = ScaffoldMessenger.of(context);
final result = await _storage.isCupertinoProtectedDataAvailable();
scaffoldMessenger.showSnackBar(
SnackBar(
content: Text('Protected data available: $result'),
backgroundColor: result != null && result ? Colors.green : Colors.red,
),
);
}
Future<void> _addNewItem() async {
await _storage.write(
key: DateTime.timestamp().microsecondsSinceEpoch.toString(),
value: _randomValue(),
iOptions: _getIOSOptions(),
aOptions: _getAndroidOptions(),
);
await _readAll();
}
IOSOptions _getIOSOptions() => IOSOptions(
accountName: _getAccountName(),
);
AndroidOptions _getAndroidOptions() => const AndroidOptions(
dataStore: true,
);
String? _getAccountName() =>
_accountNameController.text.isEmpty ? null : _accountNameController.text;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
actions: [
IconButton(
key: const Key('add_random'),
onPressed: () async {
await _addNewItem();
},
icon: const Icon(Icons.add),
),
PopupMenuButton<_Actions>(
key: const Key('popup_menu'),
onSelected: (action) async {
switch (action) {
case _Actions.deleteAll:
await _deleteAll();
case _Actions.isProtectedDataAvailable:
await _isProtectedDataAvailable();
}
},
itemBuilder: (context) => [
const PopupMenuItem(
key: Key('delete_all'),
value: _Actions.deleteAll,
child: Text('Delete all'),
),
const PopupMenuItem(
key: Key('is_protected_data_available'),
value: _Actions.isProtectedDataAvailable,
child: Text('IsProtectedDataAvailable'),
),
],
),
],
),
body: CustomScrollView(
slivers: [
if (!kIsWeb && const LocalPlatform().isIOS)
SliverToBoxAdapter(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: TextFormField(
controller: _accountNameController,
decoration: const InputDecoration(
labelText: 'kSecAttrService',
),
),
),
),
),
SliverList.builder(
itemCount: _items.length,
itemBuilder: (context, index) => ListTile(
trailing: PopupMenuButton(
key: Key('popup_row_$index'),
onSelected: (action) async {
await _performAction(
context: context,
action: action,
item: _items[index],
);
},
itemBuilder: (context) => [
PopupMenuItem(
value: _ItemActions.delete,
child: Text(
'Delete',
key: Key('delete_row_$index'),
),
),
PopupMenuItem(
value: _ItemActions.edit,
child: Text(
'Edit',
key: Key('edit_row_$index'),
),
),
PopupMenuItem(
value: _ItemActions.containsKey,
child: Text(
'Contains Key',
key: Key('contains_row_$index'),
),
),
PopupMenuItem(
value: _ItemActions.read,
child: Text(
'Read',
key: Key('contains_row_$index'),
),
),
],
),
title: Text(
_items[index].value,
key: Key('title_row_$index'),
),
subtitle: Text(
_items[index].key,
key: Key('subtitle_row_$index'),
),
),
),
],
),
);
}
Future<void> _performAction({
required BuildContext context,
required _ItemActions action,
required _SecItem item,
}) async {
switch (action) {
case _ItemActions.delete:
await _storage.delete(
key: item.key,
iOptions: _getIOSOptions(),
aOptions: _getAndroidOptions(),
);
await _readAll();
case _ItemActions.edit:
final result = await showDialog<String>(
context: context,
builder: (context) => _EditTextInputDialog(item.value),
);
if (result == null) {
return;
}
await _storage.write(
key: item.key,
value: result,
iOptions: _getIOSOptions(),
aOptions: _getAndroidOptions(),
);
await _readAll();
case _ItemActions.containsKey:
final scaffoldMessenger = ScaffoldMessenger.of(context);
final key = await showDialog<String>(
context: context,
builder: (context) => _DisplayTextInputDialog(item.key),
);
if (key == null) {
return;
}
final result = await _storage.containsKey(
key: key,
iOptions: _getIOSOptions(),
aOptions: _getAndroidOptions(),
);
scaffoldMessenger.showSnackBar(
SnackBar(
content: Text('Contains Key: $result, key checked: $key'),
backgroundColor: result ? Colors.green : Colors.red,
),
);
case _ItemActions.read:
final scaffoldMessenger = ScaffoldMessenger.of(context);
final key = await showDialog<String>(
context: context,
builder: (context) => _DisplayTextInputDialog(item.key),
);
if (key == null) {
return;
}
final result = await _storage.read(
key: key,
iOptions: _getIOSOptions(),
aOptions: _getAndroidOptions(),
);
scaffoldMessenger.showSnackBar(
SnackBar(
content: Text('value: $result'),
),
);
}
}
String _randomValue() {
final rand = Random();
final codeUnits = List<int>.generate(
20,
(_) => rand.nextInt(26) + 65,
growable: false,
);
return String.fromCharCodes(codeUnits);
}
}
class _EditTextInputDialog extends StatefulWidget {
const _EditTextInputDialog(this.value);
final String value;
@override
State<_EditTextInputDialog> createState() => _EditTextInputDialogState();
}
class _EditTextInputDialogState extends State<_EditTextInputDialog> {
late final TextEditingController _editTextDialogController;
@override
void initState() {
super.initState();
_editTextDialogController = TextEditingController(
text: widget.value,
);
}
@override
void dispose() {
_editTextDialogController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Edit item'),
content: TextField(
key: const Key('title_field'),
controller: _editTextDialogController,
autofocus: true,
),
actions: [
TextButton(
key: const Key('cancel'),
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
TextButton(
key: const Key('save'),
onPressed: () {
final value = _editTextDialogController.text;
Navigator.of(context).pop(value);
},
child: const Text('Save'),
),
],
);
}
}
class _DisplayTextInputDialog extends StatefulWidget {
const _DisplayTextInputDialog(this.value);
final String value;
@override
State<_DisplayTextInputDialog> createState() => _DisplayTextInputDialogState();
}
class _DisplayTextInputDialogState extends State<_DisplayTextInputDialog> {
late final TextEditingController _displayTextDialogController;
@override
void initState() {
super.initState();
_displayTextDialogController = TextEditingController(
text: widget.value,
);
}
@override
void dispose() {
_displayTextDialogController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Check if key exists'),
actions: [
TextButton(
onPressed: () {
final value = _displayTextDialogController.text;
Navigator.of(context).pop(value);
},
child: const Text('OK'),
),
],
content: TextField(
controller: _displayTextDialogController,
),
);
}
}
typedef _SecItem = ({
String key,
String value,
});
希望这些信息对您有所帮助!如果有任何问题或需要进一步的帮助,请随时提问。
更多关于Flutter安全存储插件flutter_secure_storage_x的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter安全存储插件flutter_secure_storage_x的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是如何在Flutter应用中使用flutter_secure_storage_x
插件的一个示例。这个插件允许你安全地存储和检索敏感数据,比如API密钥、用户令牌等。
首先,确保你已经在pubspec.yaml
文件中添加了依赖项:
dependencies:
flutter:
sdk: flutter
flutter_secure_storage_x: ^5.0.0 # 请检查最新版本号
然后,运行flutter pub get
来安装依赖。
接下来,在你的Flutter应用中导入flutter_secure_storage_x
,并使用它来存储和检索数据。以下是一个完整的示例:
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage_x/flutter_secure_storage_x.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Secure Storage Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SecureStorageExample(),
);
}
}
class SecureStorageExample extends StatefulWidget {
@override
_SecureStorageExampleState createState() => _SecureStorageExampleState();
}
class _SecureStorageExampleState extends State<SecureStorageExample> {
late FlutterSecureStorage storage;
@override
void initState() {
super.initState();
storage = FlutterSecureStorage();
}
Future<void> _writeData() async {
String key = 'my_secure_key';
String value = 'my_secure_value';
await storage.write(key: key, value: value);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Data written successfully')),
);
}
Future<void> _readData() async {
String key = 'my_secure_key';
String? value = await storage.read(key: key);
if (value != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Data read: $value')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('No data found for key: $key')),
);
}
}
Future<void> _deleteData() async {
String key = 'my_secure_key';
await storage.delete(key: key);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Data deleted successfully')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Secure Storage Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: _writeData,
child: Text('Write Data'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _readData,
child: Text('Read Data'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _deleteData,
child: Text('Delete Data'),
),
],
),
),
);
}
}
解释
- 依赖项:在
pubspec.yaml
文件中添加flutter_secure_storage_x
依赖项。 - 导入:在需要使用存储功能的Dart文件中导入
flutter_secure_storage_x
。 - 初始化:在
initState
方法中初始化FlutterSecureStorage
实例。 - 写数据:使用
storage.write
方法存储数据。 - 读数据:使用
storage.read
方法读取数据。 - 删除数据:使用
storage.delete
方法删除数据。
这个示例展示了如何使用flutter_secure_storage_x
插件在Flutter应用中安全地存储、读取和删除数据。如果你需要存储更复杂的数据类型,你可能需要先将它们序列化为字符串(例如使用jsonEncode
)。