You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Web环境下指纹登录问题:local_auth不支持Web的解决办法

Flutter Web移动端指纹登录解决方案

由于local_auth插件不支持Web端,这里推荐使用**Web Authentication API(WebAuthn)**来实现需求——这是W3C标准的浏览器原生API,支持移动端设备的指纹、面容ID等生物识别验证,完美适配Flutter Web场景。

核心逻辑

WebAuthn需要前后端配合完成:

  1. 用户首次通过账号密码登录后,前端调用WebAuthn API注册生物识别凭证,后端存储凭证的公钥等核心信息。
  2. 后续登录时,后端生成认证挑战(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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 10:35:17