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-devlibjsoncpp-dev,并在运行应用程序时安装 libsecret-1-0libjsoncpp1。如果使用 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.entitlementsmacos/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

1 回复

更多关于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'),
            ),
          ],
        ),
      ),
    );
  }
}

解释

  1. 依赖项:在pubspec.yaml文件中添加flutter_secure_storage_x依赖项。
  2. 导入:在需要使用存储功能的Dart文件中导入flutter_secure_storage_x
  3. 初始化:在initState方法中初始化FlutterSecureStorage实例。
  4. 写数据:使用storage.write方法存储数据。
  5. 读数据:使用storage.read方法读取数据。
  6. 删除数据:使用storage.delete方法删除数据。

这个示例展示了如何使用flutter_secure_storage_x插件在Flutter应用中安全地存储、读取和删除数据。如果你需要存储更复杂的数据类型,你可能需要先将它们序列化为字符串(例如使用jsonEncode)。

回到顶部