Flutter Web环境下指纹登录问题:local_auth不支持Web的解决办法
Flutter Web移动端指纹登录解决方案
由于local_auth插件不支持Web端,这里推荐使用**Web Authentication API(WebAuthn)**来实现需求——这是W3C标准的浏览器原生API,支持移动端设备的指纹、面容ID等生物识别验证,完美适配Flutter Web场景。
核心逻辑
WebAuthn需要前后端配合完成:
- 用户首次通过账号密码登录后,前端调用WebAuthn API注册生物识别凭证,后端存储凭证的公钥等核心信息。
- 后续登录时,后端生成认证挑战(challenge),前端调用API触发生物识别验证,将验证结果传回后端校验,校验通过则完成登录。
具体实现步骤
1. 后端准备(必填)
WebAuthn的安全校验必须在后端完成,核心流程:
- 注册阶段:生成注册挑战,接收前端传回的凭证数据,验证后存储公钥、凭证ID等信息。
- 认证阶段:生成认证挑战,接收前端传回的断言数据,用存储的公钥校验签名有效性,返回登录结果。
2. Flutter Web前端实现
方式一:直接通过dart:js调用原生WebAuthn API
这种方式无需额外插件,直接对接浏览器API:
import 'dart:js' as js; import 'dart:convert'; import 'dart:typed_data'; // 注册生物识别凭证(首次登录账号密码后调用) Future<void> registerBiometric(String userId, String userEmail) async { // 从后端获取注册挑战(格式为base64url编码的字符串) final String challenge = await _fetchFromBackend('/api/webauthn/register/challenge'); try { // 调用浏览器的navigator.credentials.create final js.JsObject credential = await js.promiseToFuture( js.context['navigator']['credentials'].callMethod('create', [ js.JsObject.jsify({ 'publicKey': { 'challenge': _base64UrlToUint8List(challenge), 'rp': {'name': '你的应用名称'}, 'user': { 'id': _base64UrlToUint8List(base64UrlEncode(utf8.encode(userId))), 'name': userEmail, 'displayName': userEmail.split('@')[0] }, 'pubKeyCredParams': [{'type': 'public-key', 'alg': -7}], // 使用ES256算法 'authenticatorSelection': { 'authenticatorAttachment': 'platform', // 指定使用设备内置生物识别(指纹/面容) 'userVerification': 'required' // 强制验证用户身份 } } }) ]) ); // 将凭证数据转为可传输格式,传给后端存储 final Map<String, dynamic> credentialData = { 'id': credential['id'], 'rawId': base64UrlEncode(credential['rawId']), 'response': { 'attestationObject': base64UrlEncode(credential['response']['attestationObject']), 'clientDataJSON': base64UrlEncode(credential['response']['clientDataJSON']) } }; await _sendToBackend('/api/webauthn/register', credentialData); } catch (e) { print('生物识别注册失败: $e'); rethrow; } } // 生物识别登录(后续登录调用) Future<bool> authenticateWithBiometric(String userId) async { // 从后端获取认证挑战 final String challenge = await _fetchFromBackend('/api/webauthn/auth/challenge?userId=$userId'); // 从后端获取已存储的凭证ID final String credentialId = await _fetchFromBackend('/api/webauthn/credential?id=$userId'); try { // 调用浏览器的navigator.credentials.get final js.JsObject assertion = await js.promiseToFuture( js.context['navigator']['credentials'].callMethod('get', [ js.JsObject.jsify({ 'publicKey': { 'challenge': _base64UrlToUint8List(challenge), 'allowCredentials': [ { 'type': 'public-key', 'id': _base64UrlToUint8List(credentialId) } ], 'userVerification': 'required' } }) ]) ); // 将断言数据传给后端校验 final Map<String, dynamic> assertionData = { 'id': assertion['id'], 'rawId': base64UrlEncode(assertion['rawId']), 'response': { 'authenticatorData': base64UrlEncode(assertion['response']['authenticatorData']), 'clientDataJSON': base64UrlEncode(assertion['response']['clientDataJSON']), 'signature': base64UrlEncode(assertion['response']['signature']), 'userHandle': assertion['response']['userHandle'] != null ? base64UrlEncode(assertion['response']['userHandle']) : null } }; final bool isValid = await _sendToBackend('/api/webauthn/auth/verify', assertionData); return isValid; } catch (e) { print('生物识别验证失败: $e'); return false; } } // 工具方法:base64url转Uint8List Uint8List _base64UrlToUint8List(String base64Url) { String base64 = base64Url.replaceAll('-', '+').replaceAll('_', '/'); while (base64.length % 4 != 0) { base64 += '='; } return base64Decode(base64); } // 模拟后端请求方法,实际替换为你的API请求 Future<String> _fetchFromBackend(String url) async { // 这里替换为你的http请求逻辑 return ''; } Future<bool> _sendToBackend(String url, Map<String, dynamic> data) async { // 这里替换为你的http请求逻辑 return true; }
方式二:使用封装好的Flutter插件
如果不想直接写dart:js代码,可以使用支持Flutter Web的webauthn插件(仅需在pubspec.yaml中引入),简化调用流程:
import 'package:webauthn/webauthn.dart'; import 'dart:convert'; import 'dart:typed_data'; // 注册凭证 Future<void> registerBiometric(String userId, String userEmail) async { final String challenge = await _fetchFromBackend('/api/webauthn/register/challenge'); final options = PublicKeyCredentialCreationOptions( rp: RelyingParty(name: '你的应用名称'), user: UserEntity( id: Uint8List.fromList(utf8.encode(userId)), name: userEmail, displayName: userEmail.split('@')[0] ), challenge: _base64UrlToUint8List(challenge), pubKeyCredParams: [PublicKeyCredentialParameters(type: 'public-key', alg: -7)], authenticatorSelection: AuthenticatorSelectionCriteria( authenticatorAttachment: AuthenticatorAttachment.platform, userVerification: UserVerificationRequirement.required ) ); final credential = await WebAuthn.createCredential(options); // 将credential转为可传输格式传给后端 await _sendCredentialToBackend(credential); } // 认证登录 Future<bool> authenticateWithBiometric(String userId) async { final String challenge = await _fetchFromBackend('/api/webauthn/auth/challenge?userId=$userId'); final String credentialId = await _fetchFromBackend('/api/webauthn/credential?id=$userId'); final options = PublicKeyCredentialRequestOptions( challenge: _base64UrlToUint8List(challenge), allowCredentials: [PublicKeyCredentialDescriptor( type: 'public-key', id: _base64UrlToUint8List(credentialId) )], userVerification: UserVerificationRequirement.required ); final assertion = await WebAuthn.getCredential(options); // 将assertion传给后端校验 return await _verifyAssertionWithBackend(assertion); } // 工具方法同上,省略重复代码
关键注意事项
- 必须使用HTTPS:WebAuthn仅在HTTPS环境下生效(本地开发可使用
localhost)。 - 移动端浏览器支持:Chrome、Safari(iOS 13+)、Firefox等主流移动端浏览器均支持WebAuthn生物识别。
- 安全校验:所有生物识别的验证逻辑必须在后端完成,前端仅负责调用API获取凭证,禁止在前端做校验。
- 用户体验:首次登录需先通过账号密码验证,再引导用户注册生物识别凭证;后续登录可直接触发生物识别验证。
内容的提问来源于stack exchange,提问作者Farhad Rezvani
相关产品推荐
相关产品推荐

