Flutter密钥存储管理插件keychain_access的使用

Flutter密钥存储管理插件keychain_access的使用

Flutter 插件用于访问 MacOS 和 iOS 上的 Keychain Access APIs。

功能

以下操作是可用的:

  • 存储安全数据
  • 检索安全数据
  • 删除安全数据

介绍

为什么:

在我看来,对于 Android 平台,有许多稳定的库或插件可用。然而,可靠的 MacOS 和 iOS 插件似乎并不存在。

该插件目前仅支持 MacOS 和 iOS,并且可能根据需求扩展到其他平台。

它利用了 Keychain Services API 来添加安全数据、查询安全数据和删除安全数据。

使用

要将安全数据添加到钥匙串中:

import 'package:keychain_access/keychain_access.dart';

await keychainAccess.addSecureData(
    "uniqueSecureDataIdentifier", # 示例:用户名,用户ID等。
    "<secure-password>", # -> 您的安全数据在这里。
    application: "com.company.exampleApp" # -> 这是可选的。
);
// *注意:如果键已存在,这将抛出 PlatformException。
// 使用 addOrUpdateSecureData 替换已有记录。

要更新钥匙串中的安全数据:

import 'package:keychain_access/keychain_access.dart';

await keychainAccess.updateSecureData(
    "uniqueSecureDataIdentifier", # 示例:用户名,用户ID等。
    "<secure-password>", # -> 您的安全数据在这里。
    application: "com.company.exampleApp" # -> 这是可选的。
);
// *注意:如果键不存在,这将抛出 PlatformException。
// 使用 addOrUpdateSecureData 添加新记录。
// 但是,这不能保证线程安全性。

要向钥匙串添加或更新安全数据:

import 'package:keychain_access/keychain_access.dart';

await keychainAccess.addOrUpdateSecureData(
    "uniqueSecureDataIdentifier", # 示例:用户名,用户ID等。
    "<secure-password>", # -> 您的安全数据在这里。
    application: "com.company.exampleApp" # -> 这是可选的。
);

要从钥匙串中删除安全数据:

import 'package:keychain_access/keychain_access.dart';

await keychainAccess.deleteSecureData(
    "uniqueSecureDataIdentifier", # 示例:用户名,用户ID等。
    "<secure-password>", # -> 您的安全数据在这里。
    application: "com.company.exampleApp" # -> 这是可选的。
);
// *注意:如果键不存在,这将抛出 PlatformException。

完整示例

以下是一个完整的示例代码,展示了如何使用 keychain_access 插件来管理钥匙串中的安全数据。

import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:keychain_access/keychain_access.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  [@override](/user/override)
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _simpleResults = _PluginFunctionResults();
  final _resultsForApplicationName = _PluginFunctionResults();
  final _keychainAccessPlugin = KeychainAccess();

  final applicationName = 'org.gps.flutterKeychainAccessPlugin';
  final exampleUsername = 'username123';
  final examplePassword = 'password123';
  final exampleUpdatedPassword = 'updatedPassword123';
  final exampleAddOrUpdatePassword = 'addOrUpdatePassword123';

  [@override](/user/override)
  void initState() {
    super.initState();
    initPlatformState();
  }

  Future<void> _triggerAddPassword(_PluginFunctionResults results,
      {String? application}) async {
    String passwordAddSuccessful;
    try {
      passwordAddSuccessful = await _keychainAccessPlugin.addSecureData(
              exampleUsername, examplePassword,
              application: application)
          ? "Success"
          : "FAILED";
    } on PlatformException catch (e) {
      passwordAddSuccessful = 'Failed to addPassword $e';
    }

    // 如果小部件在异步平台消息传输期间被从树中移除,则我们希望丢弃回复而不是调用 setState 更新我们的非存在的外观。
    if (!mounted) return;

    setState(() {
      results.addPasswordStatus.message = passwordAddSuccessful;
      results.addPasswordStatus.status = passwordAddSuccessful == "Success";
    });
  }

  Future<void> _triggerUpdatePassword(_PluginFunctionResults results,
      {String? application}) async {
    String passwordUpdateSuccessful;
    try {
      passwordUpdateSuccessful = await _keychainAccessPlugin.updateSecureData(
              exampleUsername, exampleUpdatedPassword,
              application: application)
          ? "Success"
          : "FAILED";
    } on PlatformException catch (e) {
      passwordUpdateSuccessful = 'Failed to updatePassword $e';
    }

    // 如果小部件在异步平台消息传输期间被从树中移除,则我们希望丢弃回复而不是调用 setState 更新我们的非存在的外观。
    if (!mounted) return;

    setState(() {
      results.updatePasswordStatus.message = passwordUpdateSuccessful;
      results.updatePasswordStatus.status =
          passwordUpdateSuccessful == 'Success';
    });
  }

  Future<void> _triggerAddOrUpdatePassword(_PluginFunctionResults results,
      {String? application}) async {
    String addOrUpdatePasswordSuccessful;
    try {
      addOrUpdatePasswordSuccessful = await _keychainAccessPlugin
              .updateSecureData(exampleUsername, exampleAddOrUpdatePassword,
                  application: application)
          ? "Success"
          : "FAILED";
    } on PlatformException catch (e) {
      addOrUpdatePasswordSuccessful = 'Failed to addOrUpdatePassword $e';
    }

    // 如果小部件在异步平台消息传输期间被从树中移除,则我们希望丢弃回复而不是调用 setState 更新我们的非存在的外观。
    if (!mounted) return;

    setState(() {
      results.addOrUpdatePasswordStatus.message = addOrUpdatePasswordSuccessful;
      results.addOrUpdatePasswordStatus.status =
          addOrUpdatePasswordSuccessful == 'Success';
    });
  }

  Future<void> _triggerFindPassword(
      _PluginFunctionResults results, String? expected,
      {String? application, String? key}) async {
    String findPasswordSuccessful;
    bool isSuccess = false;
    try {
      key ??= exampleUsername;
      final passwordValue = await _keychainAccessPlugin.findSecureData(key,
          application: application);
      isSuccess = passwordValue == expected;
      findPasswordSuccessful =
          isSuccess ? "$passwordValue" : "Failed";
    } on PlatformException catch (e) {
      findPasswordSuccessful = 'Failed to findPassword $e';
    }

    // 如果小部件在异步平台消息传输期间被从树中移除,则我们希望丢弃回复而不是调用 setState 更新我们的非存在的外观。
    if (!mounted) return;

    setState(() {
      final result = _PluginFunctionResult();
      result.message = findPasswordSuccessful;
      result.status = isSuccess;
      results.findPasswordStatuses.add(result);
    });
  }

  Future<void> _triggerDeletePassword(_PluginFunctionResults results,
      {String? application}) async {
    String deletePasswordSuccessful;
    try {
      deletePasswordSuccessful = await _keychainAccessPlugin
              .deleteSecureData(exampleUsername, application: application)
          ? "Success"
          : "Failed";
    } on PlatformException catch (e) {
      deletePasswordSuccessful = 'Failed to deletePassword $e';
    }

    // 如果小部件在异步平台消息传输期间被从树中移除,则我们希望丢弃回复而不是调用 setState 更新我们的非存在的外观。
    if (!mounted) return;

    setState(() {
      results.deletePasswordStatus.message = deletePasswordSuccessful;
      results.deletePasswordStatus.status =
          deletePasswordSuccessful == 'Success';
    });
  }

  // 平台消息是异步的,所以我们初始化在一个异步方法中。
  Future<void> initPlatformState() async {
    // 简单结果。
    await _triggerAddPassword(_simpleResults);
    await _triggerFindPassword(_simpleResults, examplePassword);

    await _triggerUpdatePassword(_simpleResults);
    await _triggerFindPassword(_simpleResults, exampleUpdatedPassword);

    await _triggerAddOrUpdatePassword(_simpleResults);
    await _triggerFindPassword(_simpleResults, exampleAddOrUpdatePassword);

    await _triggerDeletePassword(_simpleResults);
    await _triggerFindPassword(_simpleResults, null, key: "DOES_NOT_EXIST");

    // 应用名称结果。
    await _triggerAddPassword(_resultsForApplicationName,
        application: applicationName);
    await _triggerFindPassword(_resultsForApplicationName, examplePassword,
        application: applicationName);

    await _triggerUpdatePassword(_resultsForApplicationName,
        application: applicationName);
    await _triggerFindPassword(
        _resultsForApplicationName, exampleUpdatedPassword,
        application: applicationName);

    await _triggerAddOrUpdatePassword(_resultsForApplicationName,
        application: applicationName);
    await _triggerFindPassword(
        _resultsForApplicationName, exampleAddOrUpdatePassword,
        application: applicationName);

    await _triggerDeletePassword(_resultsForApplicationName,
        application: applicationName);
    await _triggerFindPassword(_resultsForApplicationName, null,
        application: applicationName, key: "DOES_NOT_EXIST");
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      appBar: AppBar(
        title: const Text('Keychain Access Plugin Example'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Table(
            border: TableBorder.all(),
            defaultVerticalAlignment: TableCellVerticalAlignment.middle,
            children: [
              TableRow(
                children: [
                  TableCell(child: columnField('Function Name', heading: true)),
                  TableCell(
                    child: columnField('No Application field', heading: true),
                  ),
                  TableCell(
                    child: columnField('W/ Application', heading: true),
                  ),
                ],
              ),
              TableRow(
                children: [
                  TableCell(
                    child: columnField('addSecureData'),
                  ),
                  TableCell(
                    child: columnField(_buildStatusMessage(
                        [_simpleResults.addPasswordStatus])),
                  ),
                  TableCell(
                    child: columnField(_buildStatusMessage(
                        [_resultsForApplicationName.addPasswordStatus])),
                  ),
                ],
              ),
              TableRow(
                children: [
                  TableCell(
                    child: columnField('updateSecureData'),
                  ),
                  TableCell(
                    child: columnField(_buildStatusMessage(
                        [_simpleResults.updatePasswordStatus])),
                  ),
                  TableCell(
                    child: columnField(_buildStatusMessage(
                        [_resultsForApplicationName.updatePasswordStatus])),
                  ),
                ],
              ),
              TableRow(
                children: [
                  TableCell(
                    child: columnField('addOrUpdateSecureData'),
                  ),
                  TableCell(
                    child: columnField(_buildStatusMessage(
                        [_simpleResults.addOrUpdatePasswordStatus])),
                  ),
                  TableCell(
                    child: columnField(_buildStatusMessage([
                      _resultsForApplicationName.addOrUpdatePasswordStatus
                    ])),
                  ),
                ],
              ),
              TableRow(
                children: [
                  TableCell(
                    child: columnField('findSecureData'),
                  ),
                  TableCell(
                    child: columnField(_buildStatusMessage(
                        _simpleResults.findPasswordStatuses)),
                  ),
                  TableCell(
                    child: columnField(_buildStatusMessage(
                        _resultsForApplicationName.findPasswordStatuses)),
                  ),
                ],
              ),
              TableRow(
                children: [
                  TableCell(
                    child: columnField('deleteSecureData'),
                  ),
                  TableCell(
                    child: columnField(_buildStatusMessage(
                        [_simpleResults.deletePasswordStatus])),
                  ),
                  TableCell(
                    child: columnField(_buildStatusMessage(
                        [_resultsForApplicationName.deletePasswordStatus])),
                  ),
                ],
              ),
            ]),
      ),
    ));
  }

  Widget columnField(String text, {bool heading = false}) {
    TextStyle style = const TextStyle();
    if (heading) {
      style = const TextStyle(fontWeight: FontWeight.bold);
    }
    return Padding(
      padding: const EdgeInsets.all(2.0),
      child: Text(
        text,
        style: style,
      ),
    );
  }

  String _buildStatusMessage(List<_PluginFunctionResult> statuses) {
    if (statuses.isEmpty) {
      return '';
    }
    bool allSuccess = statuses
        .map((e) => e.status)
        .reduce((value, element) => value && element);
    final message = statuses.map((e) => e.message).join('\n');
    if (allSuccess) {
      return '✅$message';
    }
    return '❌$message';
  }
}

class _PluginFunctionResults {
  _PluginFunctionResult addPasswordStatus = _PluginFunctionResult();
  _PluginFunctionResult updatePasswordStatus = _PluginFunctionResult();
  _PluginFunctionResult addOrUpdatePasswordStatus = _PluginFunctionResult();
  final List<_PluginFunctionResult> findPasswordStatuses = [];
  _PluginFunctionResult deletePasswordStatus = _PluginFunctionResult();
}

class _PluginFunctionResult {
  bool status = false;
  String message = 'Unknown';
}

更多关于Flutter密钥存储管理插件keychain_access的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter密钥存储管理插件keychain_access的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何在Flutter项目中使用keychain_access插件进行密钥存储管理的代码示例。keychain_access插件主要用于iOS平台上的密钥存储,而Android平台则使用其他机制(如keystore),但本示例将专注于iOS平台。

首先,确保你已经在pubspec.yaml文件中添加了keychain_access依赖:

dependencies:
  flutter:
    sdk: flutter
  keychain_access: ^0.6.0  # 请检查最新版本号

然后,运行flutter pub get来获取依赖。

接下来,我们编写Flutter代码来演示如何使用keychain_access插件。以下是一个简单的示例,展示如何存储和检索密钥。

main.dart

import 'package:flutter/material.dart';
import 'package:keychain_access/keychain_access.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Keychain Access Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: KeychainDemoPage(),
    );
  }
}

class KeychainDemoPage extends StatefulWidget {
  @override
  _KeychainDemoPageState createState() => _KeychainDemoPageState();
}

class _KeychainDemoPageState extends State<KeychainDemoPage> {
  final Keychain _keychain = Keychain();
  String _retrievedValue = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Keychain Access Demo'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextField(
              decoration: InputDecoration(labelText: 'Value to Store'),
              onChanged: (value) async {
                await _storeValue(value);
              },
            ),
            SizedBox(height: 20),
            Text('Retrieved Value: $_retrievedValue'),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () async {
                _retrievedValue = await _retrieveValue();
                setState(() {});
              },
              child: Text('Retrieve Value'),
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _storeValue(String value) async {
    try {
      await _keychain.setInternetCredentials(
        server: 'example.com',  // 可以是任何你选择的标识符
        account: 'user_account',
        password: value,
      );
      print('Value stored successfully');
    } catch (e) {
      print('Error storing value: $e');
    }
  }

  Future<String?> _retrieveValue() async {
    try {
      final credentials = await _keychain.getInternetCredentials(
        server: 'example.com',
        account: 'user_account',
      );
      return credentials?.password;
    } catch (e) {
      print('Error retrieving value: $e');
      return null;
    }
  }
}

注意事项

  1. 平台特定性keychain_access主要用于iOS。对于Android,你需要使用其他插件或原生代码来访问keystore
  2. 权限:确保你的iOS项目已正确配置以使用Keychain服务。通常,这不需要额外的配置,但在某些情况下,你可能需要处理App Transport Security (ATS) 设置。
  3. 错误处理:示例代码中包含了基本的错误处理,但在实际应用中,你可能需要更详细的错误日志记录和用户反馈。
  4. 敏感数据:确保你存储的数据是加密的,并且只存储必要的敏感信息。

这个示例展示了如何在Flutter应用中存储和检索密钥,但请根据你的具体需求和安全要求进行调整。

回到顶部