Flutter生物特征存储插件biometric_storage_plus的使用

Flutter生物特征存储插件biometric_storage_plus的使用


生物特征存储插件biometric_storage_plus

Pub

该插件允许你以硬件加密的方式存储小数据,例如密码、密钥等。它支持Android、iOS、macOS,并且在Linux、Windows和Web平台上也有部分支持。

  • Android: 使用KeyStore。
  • iOS 和 macOS: 使用KeyChain。
  • Linux: 使用libsecret存储值到Keyring中(不支持生物特征认证)。
  • Windows: 使用wincreds.h存储到凭证存储中。
  • Web: 警告:使用未加密的localStorage存储。如果你有更好的安全存储方案,请在GitHub上提Issue。

可以参考AuthPass Password Manager应用来了解如何大量使用此插件。


入门指南

安装
Android

要求:

  • Android: API Level >= 23 (android/app/build.gradle中的minSdkVersion 23)

  • 确保使用最新的Kotlin版本:android/build.gradle中的ext.kotlin_version = '1.4.31'

  • MainActivity必须继承自FlutterFragmentActivity

  • 主Activity的主题必须使用Theme.AppCompat主题。否则,在Android < 29时会有崩溃问题。例如:

    android/app/src/main/AndroidManifest.xml

    <activity
        android:name=".MainActivity"
        android:launchMode="singleTop"
        android:theme="@style/LaunchTheme">
        [...]
        <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
    </activity>
    

    android/app/src/main/res/values/styles.xml

    <resources>
      <style name="LaunchTheme" parent="Theme.AppCompat.NoActionBar">
        ...
      </style>
      <style name="NormalTheme" parent="Theme.AppCompat.NoActionBar">
        ...
      </style>
    </resources>
    

资源:

iOS

要求:

  • 在app的Info.plist文件中包含NSFaceIDUsageDescription键
  • 支持所有被Flutter支持的iOS版本(如iOS 12)

已知问题:

  • 自iOS 15起,模拟器似乎不再支持本地认证。
macOS

要求:

  • 在app的Info.plist文件中包含NSFaceIDUsageDescription键
  • 启用秘钥链共享和签名
  • 支持所有被Flutter支持的macOS版本(如>= MacOS 10.14)

使用方法

你只需要四个主要的方法:

  1. 检查设备是否支持生物特征认证

    final response = await BiometricStorage().canAuthenticate();
    if (response != CanAuthenticateResponse.success) {
      // 处理错误
    }
    
  2. 创建访问对象

    final store = BiometricStorage().getStorage('mystorage');
    
  3. 读取数据

    final data = await store.read();
    
  4. 写入数据

    final myNewData = 'Hello World';
    await store.write(myNewData);
    

完整示例

以下是一个完整的示例,展示了如何使用biometric_storage_plus插件。

import 'dart:io';

import 'package:biometric_storage/biometric_storage_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart';

final MemoryAppender logMessages = MemoryAppender();

final _logger = Logger('main');

void main() {
  Logger.root.level = Level.ALL;
  PrintAppender().attachToLogger(Logger.root);
  logMessages.attachToLogger(Logger.root);
  _logger.fine('Application launched. (v2)');
  runApp(const MyApp());
}

class StringBufferWrapper with ChangeNotifier {
  final StringBuffer _buffer = StringBuffer();

  void writeln(String line) {
    _buffer.writeln(line);
    notifyListeners();
  }

  [@override](/user/override)
  String toString() => _buffer.toString();
}

class ShortFormatter extends LogRecordFormatter {
  [@override](/user/override)
  StringBuffer formatToStringBuffer(LogRecord rec, StringBuffer sb) {
    sb.write(
        '${rec.time.hour}:${rec.time.minute}:${rec.time.second} ${rec.level.name} '
        '${rec.message}');

    if (rec.error != null) {
      sb.write(rec.error);
    }
    // ignore: avoid_as
    final stackTrace = rec.stackTrace ??
        (rec.error is Error ? (rec.error as Error).stackTrace : null);
    if (stackTrace != null) {
      sb.write(stackTrace);
    }
    return sb;
  }
}

class MemoryAppender extends BaseLogAppender {
  MemoryAppender() : super(ShortFormatter());

  final StringBufferWrapper log = StringBufferWrapper();

  [@override](/user/override)
  void handle(LogRecord record) {
    log.writeln(formatter.format(record));
  }
}

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

  [@override](/user/override)
  MyAppState createState() => MyAppState();
}

class MyAppState extends State<MyApp> {
  final String baseName = 'default';
  BiometricStorageFile? _authStorage;
  BiometricStorageFile? _storage;
  BiometricStorageFile? _customPrompt;

  final TextEditingController _writeController =
      TextEditingController(text: 'Lorem Ipsum');

  [@override](/user/override)
  void initState() {
    super.initState();
    logMessages.log.addListener(_logChanged);
    _checkAuthenticate();
  }

  [@override](/user/override)
  void dispose() {
    logMessages.log.removeListener(_logChanged);
    super.dispose();
  }

  Future<CanAuthenticateResponse> _checkAuthenticate() async {
    final response = await BiometricStorage().canAuthenticate();
    _logger.info('checked if authentication was possible: $response');
    return response;
  }

  void _logChanged() => setState(() {});

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Column(
          children: [
            const Text('Methods:'),
            ElevatedButton(
              child: const Text('init'),
              onPressed: () async {
                _logger.finer('Initializing $baseName');
                final authenticate = await _checkAuthenticate();
                if (authenticate == CanAuthenticateResponse.unsupported) {
                  _logger.severe(
                      'Unable to use authenticate. Unable to get storage.');
                  return;
                }
                final supportsAuthenticated =
                    authenticate == CanAuthenticateResponse.success ||
                        authenticate == CanAuthenticateResponse.statusUnknown;
                if (supportsAuthenticated) {
                  _authStorage = await BiometricStorage().getStorage(
                      '${baseName}_authenticated',
                      options: StorageFileInitOptions());
                }
                _storage = await BiometricStorage()
                    .getStorage('${baseName}_unauthenticated',
                        options: StorageFileInitOptions(
                          authenticationRequired: false,
                        ));
                if (supportsAuthenticated) {
                  _customPrompt = await BiometricStorage()
                      .getStorage('${baseName}_customPrompt',
                          options: StorageFileInitOptions(
                            androidAuthenticationValidityDuration:
                                const Duration(seconds: 5),
                            darwinTouchIDAuthenticationForceReuseContextDuration:
                                const Duration(seconds: 5),
                          ),
                          promptInfo: const PromptInfo(
                            iosPromptInfo: IosPromptInfo(
                              saveTitle: 'Custom save title',
                              accessTitle: 'Custom access title.',
                            ),
                            androidPromptInfo: AndroidPromptInfo(
                              title: 'Custom title',
                              subtitle: 'Custom subtitle',
                              description: 'Custom description',
                              negativeButton: 'Nope!',
                            ),
                          ));
                }
                setState(() {});
                _logger.info('initialized $baseName');
              },
            ),
            ...?_appArmorButton(),
            ...(_authStorage == null
                ? []
                : [
                    const Text('Biometric Authentication',
                        style: TextStyle(fontWeight: FontWeight.bold)),
                    StorageActions(
                        storageFile: _authStorage!,
                        writeController: _writeController),
                    const Divider(),
                  ]),
            ...?(_storage == null
                ? null
                : [
                    const Text('Unauthenticated',
                        style: TextStyle(fontWeight: FontWeight.bold)),
                    StorageActions(
                        storageFile: _storage!,
                        writeController: _writeController),
                    const Divider(),
                  ]),
            ...?(_customPrompt == null
                ? null
                : [
                    const Text('Custom Prompts w/ 5s auth validity',
                        style: TextStyle(fontWeight: FontWeight.bold)),
                    StorageActions(
                        storageFile: _customPrompt!,
                        writeController: _writeController),
                    const Divider(),
                  ]),
            const Divider(),
            TextField(
              decoration: const InputDecoration(
                labelText: 'Example text to write',
              ),
              controller: _writeController,
            ),
            Expanded(
              child: Container(
                color: Colors.white,
                constraints: const BoxConstraints.expand(),
                child: SingleChildScrollView(
                  reverse: true,
                  child: Container(
                    padding: const EdgeInsets.all(16),
                    child: Text(
                      logMessages.log.toString(),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  List<Widget>? _appArmorButton() => kIsWeb || !Platform.isLinux
      ? null
      : [
          ElevatedButton(
            child: const Text('Check App Armor'),
            onPressed: () async {
              if (await BiometricStorage().linuxCheckAppArmorError()) {
                _logger.info('Got an error! User has to authorize us to '
                    'use secret service.');
                _logger.info(
                    'Run: `snap connect biometric-storage-example:password-manager-service`');
              } else {
                _logger.info('all good.');
              }
            },
          )
        ];
}

class StorageActions extends StatelessWidget {
  const StorageActions({
    super.key,
    required this.storageFile,
    required this.writeController,
  });

  final BiometricStorageFile storageFile;
  final TextEditingController writeController;

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: <Widget>[
        ElevatedButton(
          child: const Text('read'),
          onPressed: () async {
            _logger.fine('reading from ${storageFile.name}');
            try {
              final result = await storageFile.read();
              _logger.fine('read: {$result}');
            } on AuthException catch (e) {
              if (e.code == AuthExceptionCode.userCanceled) {
                _logger.info('User canceled.');
                return;
              }
              rethrow;
            }
          },
        ),
        ElevatedButton(
          child: const Text('write'),
          onPressed: () async {
            _logger.fine('Going to write...');
            try {
              await storageFile
                  .write(' [${DateTime.now()}] ${writeController.text}');
              _logger.info('Written content.');
            } on AuthException catch (e) {
              if (e.code == AuthExceptionCode.userCanceled) {
                _logger.info('User canceled.');
                return;
              }
              rethrow;
            }
          },
        ),
        ElevatedButton(
          child: const Text('delete'),
          onPressed: () async {
            _logger.fine('deleting...');
            await storageFile.delete();
            _logger.info('Deleted.');
          },
        ),
      ],
    );
  }
}

更多关于Flutter生物特征存储插件biometric_storage_plus的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter生物特征存储插件biometric_storage_plus的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


biometric_storage_plus 是一个 Flutter 插件,它允许你使用生物特征认证(如指纹、面部识别等)来安全地存储和检索敏感数据。这个插件是对 biometric_storage 的增强版本,支持更多的平台和功能。

安装插件

首先,你需要在 pubspec.yaml 文件中添加 biometric_storage_plus 依赖:

dependencies:
  biometric_storage_plus: ^1.0.0

然后运行 flutter pub get 来安装插件。

使用插件

1. 导入插件

import 'package:biometric_storage_plus/biometric_storage_plus.dart';

2. 初始化存储

你可以使用 BiometricStorage 类来初始化存储。每个存储实例都需要一个唯一的名称,这个名称用于标识存储的数据。

final storage = await BiometricStorage().getStorage('my_secret_data');

3. 存储数据

你可以使用 write 方法来存储数据。数据将被加密并存储在设备的安全存储中。

await storage.write('This is my secret data');

4. 读取数据

使用 read 方法来读取存储的数据。系统会要求用户进行生物特征认证(如指纹或面部识别)来访问数据。

final secretData = await storage.read();
print('Secret Data: $secretData');

5. 删除数据

你可以使用 delete 方法来删除存储的数据。

await storage.delete();

处理异常

在使用 biometric_storage_plus 时,你可能会遇到一些异常情况,例如生物特征认证失败或设备不支持生物特征认证。你可以使用 try-catch 块来处理这些异常。

try {
  final secretData = await storage.read();
  print('Secret Data: $secretData');
} on BiometricStorageException catch (e) {
  print('Error: ${e.message}');
} catch (e) {
  print('Unexpected error: $e');
}

检查设备支持

你可以使用 BiometricStorage().canAuthenticate() 方法来检查设备是否支持生物特征认证。

final canAuthenticate = await BiometricStorage().canAuthenticate();
if (canAuthenticate) {
  print('Device supports biometric authentication');
} else {
  print('Device does not support biometric authentication');
}

完整示例

以下是一个完整的示例,展示了如何使用 biometric_storage_plus 插件来存储和读取敏感数据。

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

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BiometricStorageExample(),
    );
  }
}

class BiometricStorageExample extends StatefulWidget {
  [@override](/user/override)
  _BiometricStorageExampleState createState() => _BiometricStorageExampleState();
}

class _BiometricStorageExampleState extends State<BiometricStorageExample> {
  final _storageName = 'my_secret_data';
  String _secretData = '';

  Future<void> _storeData() async {
    final storage = await BiometricStorage().getStorage(_storageName);
    await storage.write('This is my secret data');
  }

  Future<void> _readData() async {
    try {
      final storage = await BiometricStorage().getStorage(_storageName);
      final data = await storage.read();
      setState(() {
        _secretData = data ?? 'No data found';
      });
    } on BiometricStorageException catch (e) {
      setState(() {
        _secretData = 'Error: ${e.message}';
      });
    } catch (e) {
      setState(() {
        _secretData = 'Unexpected error: $e';
      });
    }
  }

  Future<void> _deleteData() async {
    final storage = await BiometricStorage().getStorage(_storageName);
    await storage.delete();
    setState(() {
      _secretData = '';
    });
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Biometric Storage Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Secret Data: $_secretData'),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _storeData,
              child: Text('Store Data'),
            ),
            ElevatedButton(
              onPressed: _readData,
              child: Text('Read Data'),
            ),
            ElevatedButton(
              onPressed: _deleteData,
              child: Text('Delete Data'),
            ),
          ],
        ),
      ),
    );
  }
}
回到顶部