背景
MAUI 的出现,赋予了广大 Net 开发者开发多平台应用的能力,MAUI 是 Xamarin.Forms 演变而来,但是相比 Xamarin 性能更好,可扩展性更强,结构更简单。但是 MAUI 对于平台相关的实现并不完整。所以 MASA 团队开展了一个实验性项目,意在对微软 MAUI 的补充和扩展,
项目地址https://github.com/BlazorComponent/MASA.Blazor/tree/main/src/Masa.Blazor.Maui.Plugin
每个功能都有单独的 demo 演示项目,考虑到 app 安装文件体积(虽然 MAUI 已经集成裁剪功能,但是该功能对于代码本身有影响),届时每一个功能都会以单独的 nuget 包的形式提供,方便测试,现在项目才刚刚开始,但是相信很快就会有可以交付的内容啦。
前言
本系列文章面向移动开发小白,从零开始进行平台相关功能开发,演示如何参考平台的官方文档使用 MAUI 技术来开发相应功能。
介绍
在 API 级别 23 (Android 6.0) 设备上引入指纹扫描仪为应用程序提供了传统的用户名/密码用户身份验证的替代方法。 相较于用户名和密码,采用指纹对用户进行身份验证使应用程序安全性的实现更具隐私性,之后 API-28(Android9.0) 中添加了生物识别身份验证 Biometric,增加了人脸认证相关功能。我们今天讨论的只涉及指纹认证,考虑到兼容性问题采用 API - 23 (Android 6.0) 版本提供的 FingerprintManager API,经过测试可以在 Android 6.0 -11.0 中正常工作,如果您需要人脸验证相关功能请参考链接: androidx.biometric,实现细节与本文类似。
思路
我们先看一下 Android 的指纹验证方法核心的指纹管理类 FingerprintManagerCompat ,fingerprintManager 是通过 FingerprintManagerCompat.from(Context context)来创建的。
JAVA代码
FingerprintManagerCompat fingerprintManager= FingerprintManagerCompat.from(Context context);
复制代码
1、检查资格:
1、需要检查设备是否支持指纹。2、需要检查设备是否受保护 - 用户必须使用屏幕锁保护设备。 如果用户未使用屏幕锁保护设备,但是当前应用程序对于安全性要求很高,则应通知用户必须配置屏幕锁。3、需要检查用户是否已经注册指纹 - 用户必须至少有一个指纹已注册到操作系统。 此权限检查应在每次尝试进行身份验证之前进行,因为用户有可能随时取消指纹在 MAUI blazor 项目的 Platforms->Android 文件夹添加 MasaMauiFingerprintService.cs 类,添加如下两个方法
public static class MasaMauiFingerprintService
{
private static FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.From(Android.App.Application.Context);
/// <summary>
/// Check eligibility
/// </summary>
/// <returns>error message</returns>
public static async Task<string> CheckingEligibility()
{
// 1、Check if your hardware supports it
if (!fingerprintManager.IsHardwareDetected)
{
return "IsHardwareDetected";
}
// 2、Check if the user is using a screen lock
// KeyguardManager: Lock screen management class
var keyguardManager = Android.App.Application.Context.GetSystemService(Context.KeyguardService) as KeyguardManager;
if (!keyguardManager.IsKeyguardSecure)
{
return "The device does not have a screen lock set";
}
// 3、Check if at least one fingerprint is registered
if (!fingerprintManager.HasEnrolledFingerprints)
{
return "The device does not have a fingerprint set, please set at least one fingerprint";
}
var granted = await CheckAndRequestFingerprintPermission();
if (!granted)
{
return "Permissions not granted";
}
return string.Empty;
}
/// <summary>
/// Permission check
/// </summary>
/// <returns></returns>
private static async Task<bool> CheckAndRequestFingerprintPermission()
{
var status = await Permissions.CheckStatusAsync<AndroidFingerprintPermissions>();
if (status == PermissionStatus.Granted)
return true;
status = await Permissions.RequestAsync<AndroidFingerprintPermissions>();
if (status == PermissionStatus.Granted)
return true;
return false;
}
/// <summary>
/// Permissions required for fingerprints
/// </summary>
private class AndroidFingerprintPermissions : Permissions.BasePlatformPermission
{
public override (string androidPermission, bool isRuntime)[] RequiredPermissions =>
new List<(string androidPermission, bool isRuntime)>
{
(global::Android.Manifest.Permission.UseFingerprint, true),
}.ToArray();
}
}
复制代码
CheckingEligibility 依次检查设备是否支持蓝牙(IsHardwareDetected)、设备是否有屏幕锁(IsKeyguardSecure)这里需要一个 KeyguardManager 的类帮助检查、是否注册了至少一个指纹(HasEnrolledFingerprints)、是否统一了使用指纹相关权限。
2、扫描指纹实现
现在,我们使用 FingerprintManager 的 Authenticate 方法进行指纹验证,
JAVA代码
public void authenticate (FingerprintManager.CryptoObject crypto,
CancellationSignal cancel,
int flags,
FingerprintManager.AuthenticationCallback callback,
Handler handler)
复制代码
参数:crypto FingerprintManager.CryptoObject: 这是一个加密类的对象,指纹扫描器会使用这个对象来判断认证结果的合法性。这个对象可以是 null,但是这样的话,就意味这 app 无条件信任认证结果,所以这个过程可能被攻击,数据可以被篡改。因此,建议这个参数不要置为 null。
cancel CancellationSignal:这个对象用来在指纹识别器扫描用户指纹的是时候取消当前的扫描操作,如果不取消的话,那么指纹扫描器会移植扫描直到超时(一般为 30s,取决于具体的厂商实现),这样的话就会比较耗电。建议这个参数不要置为 null。识别过程中可以手动取消指纹识别
flags int:没用,传 0callback
FingerprintManager.AuthenticationCallback:要接收身份验证事件的回调方法, 此值不能为 null
handler Handler:FingerprintManagerCompat 将会使用这个 handler 中的 looper 来处理来自指纹识别硬件的消息。一般来说,我们开发的时候可以直接传 null,因为 FingerprintManagerCompat 会默认使用 app 的 main looper 来处理
我们继续在当前目录下添加 CryptoObjectHelper.cs 类
public class CryptoObjectHelper
{
// 键值名称,应用中需要保持唯一
static readonly string KEY_NAME = "com.masa-maui-blazor.android.sample.fingerprint_authentication_key";
// 写死不用改
static readonly string KEYSTORE_NAME = "AndroidKeyStore";
// 加密算法参数 不用改
static readonly string KEY_ALGORITHM = KeyProperties.KeyAlgorithmAes;
static readonly string BLOCK_MODE = KeyProperties.BlockModeCbc;
static readonly string ENCRYPTION_PADDING = KeyProperties.EncryptionPaddingPkcs7;
static readonly string TRANSFORMATION = KEY_ALGORITHM + "/" +
BLOCK_MODE + "/" +
ENCRYPTION_PADDING;
readonly KeyStore _keystore;
public CryptoObjectHelper()
{
_keystore = KeyStore.GetInstance(KEYSTORE_NAME);
_keystore.Load(null);
}
public FingerprintManagerCompat.CryptoObject BuildCryptoObject()
{
var cipher = CreateCipher();
return new FingerprintManagerCompat.CryptoObject(cipher);
}
Cipher CreateCipher(bool retry = true)
{
var key = GetKey();
var cipher = Cipher.GetInstance(TRANSFORMATION);
try
{
cipher.Init(CipherMode.EncryptMode, key);
}
catch (KeyPermanentlyInvalidatedException e)
{
_keystore.DeleteEntry(KEY_NAME);
if (retry)
{
CreateCipher(false);
}
else
{
throw new Exception("Could not create the cipher for fingerprint authentication.", e);
}
}
return cipher;
}
IKey GetKey()
{
IKey secretKey;
if (!_keystore.IsKeyEntry(KEY_NAME))
{
CreateKey();
}
secretKey = _keystore.GetKey(KEY_NAME, null);
return secretKey;
}
void CreateKey()
{
var keyGen = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, KEYSTORE_NAME);
var keyGenSpec =
new KeyGenParameterSpec.Builder(KEY_NAME, KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt)
.SetBlockModes(BLOCK_MODE)
.SetEncryptionPaddings(ENCRYPTION_PADDING)
.SetUserAuthenticationRequired(true)
.Build();
keyGen.Init(keyGenSpec);
keyGen.GenerateKey();
}
}
复制代码
CryptoObjectHelper 类使用 Android KeyGenerator 生成密钥并安全地将其存储在设备上。创建的键类型的元数据由类的 KeyGenParameterSpec 实例提供。使用 GetInstance 工厂方法实例化 AKeyGenerator。上述代码使用 (AES) 作为加密算法。 KeyGenParameterSpec.Builde 包装具体的 AES 加密配置信息,SetUserAuthenticationRequired(true) 表示在使用密钥之前需要用户身份验证。
我们在 MasaMauiFingerprintService 添加一个验证的方法,其中自定义的 MasaMauiAuthCallback,在下面一节介绍。
public static void FingerPrintAuthentication()
{
fingerprintManager.Authenticate(new CryptoObjectHelper().BuildCryptoObject(), 0, new CancellationSignal(), new MasaMauiAuthCallback(), null);
}
复制代码
3、响应身份验证回调
我们在当前目录继续添加 CallBack 类 MasaMauiAuthCallback.cs ,该类需要继承 FingerprintManagerCompat.AuthenticationCallback,至少需要重写 OnAuthenticationSucceeded 方法
public class MasaMauiAuthCallback : FingerprintManagerCompat.AuthenticationCallback
{
// 随便写,但是app内保持唯一
byte[] SECRET_BYTES = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
public override void OnAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result)
{
if (result.CryptoObject.Cipher != null) //使用了Cipher
{
var doFinalResult = result.CryptoObject.Cipher.DoFinal(SECRET_BYTES);
if (doFinalResult.Any())
{
MessagingCenter.Send<MasaMauiAuthCallback,string>(this, "Validation", "验证成功");
}
}
else
{
// 没有使用Cipher?
// 我们这里的示例使用了Cipher,暂时不考虑不适用的情况
}
}
public override void OnAuthenticationFailed()
{
// 通知用户验证失败
MessagingCenter.Send<MasaMauiAuthCallback, string>(this, "Validation", "验证失败");
}
}
复制代码
除了 OnAuthenticationSucceeded 和 OnAuthenticationFailed 之外,还有 onAuthenticationHelp 和 onAuthenticationError 我们这里暂不考虑其他两种情景,有兴趣可以参考链接: AuthenticationCallback这里为了方便演示,验证成功或者失败都是通过 MessagingCenter 消息发送出去,在使用的时候需要订阅对应 topic,来获取验证结果。
4、测试
新建一个 MAUI Blazor 项目 Masa.Blazor.Maui.Plugin.BiometricsSample 在 AndroidManifest.xml 文件中添加指纹需要的权限 USE_FINGERPRINT
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
复制代码
简单修改一下 Index.razor 便于测试 Index.razor:
@page "/"
Welcome to your new app.
<MButton Block OnClick="Fingerprint">验证指纹</MButton>
复制代码
Index.razor.cs:
using Masa.Blazor.Maui.Plugin.Biometrics;
using Microsoft.AspNetCore.Components;
namespace Masa.Blazor.Maui.Plugin.BiometricsSample.Pages
{
public partial class Index
{
[Inject]
private IPopupService PopupService { get; set; }
private async Task Fingerprint()
{
var checkingEligibilityErrorMessage = await MasaMauiFingerprintService.CheckingEligibility();
if (string.IsNullOrEmpty(checkingEligibilityErrorMessage))
{
await HandledValidationAsync();
MasaMauiFingerprintService.FingerPrintAuthentication();
}
else
{
await PopupService.ToastErrorAsync(checkingEligibilityErrorMessage);
}
}
private async Task HandledValidationAsync()
{
// Cancel your subscription first to prevent duplicate subscriptions
MessagingCenter.Unsubscribe<MasaMauiAuthCallback, string>(this, "Validation");
MessagingCenter.Subscribe<MasaMauiAuthCallback, string>(this, "Validation", (sender, arg) =>
{
PopupService.ToastInfoAsync(arg);
});
}
}
}
复制代码
这里我使用到 MAUI 提供的发布和订阅消息 MessagingCenter, 参考连接链接: MessagingCenter代码比较简单,先检查资格,没有报错信息之后开启指纹验证,并异步接收 callback 方法发布的消息。启动一下,分别用正确和错误的指纹进行测试:不同的手机指纹验证的 UI 不同,我这里是 vivo 的手机
如果你对我们的开源项目感兴趣,无论是代码贡献、使用、提 Issue,欢迎联系我们
WeChat:MasaStackTechOps
QQ:7424099
评论