Flutter生物特征存储插件biometric_storage_plus的使用
Flutter生物特征存储插件biometric_storage_plus的使用
生物特征存储插件biometric_storage_plus
该插件允许你以硬件加密的方式存储小数据,例如密码、密钥等。它支持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)
使用方法
你只需要四个主要的方法:
-
检查设备是否支持生物特征认证
final response = await BiometricStorage().canAuthenticate(); if (response != CanAuthenticateResponse.success) { // 处理错误 }
-
创建访问对象
final store = BiometricStorage().getStorage('mystorage');
-
读取数据
final data = await store.read();
-
写入数据
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
更多关于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'),
),
],
),
),
);
}
}