一、背景

货拉拉专送司机端存在一个账号在多台设备之间切换登录的场景,登录的token凭证也有时效性,当切换设备登录或token凭证失效时,需要走手机验证码登录流程(目前暂只支持此登录方式),若碰到手机号临时被三大运营端限制的场景,将无法正常获取验证码进行登录。

为了解决上述极端场景的登录问题,以及在一定程度上缩减短信验证码的开销,特引入指纹/人脸认证的登录方式。

本文将简单阐述接入指纹/人脸认证进行登录的整个流程的探索过程及部分核心代码实现,以及分享此过程中碰到的一些问题。

二、探究

2.1 业界现状

业界一些主流App登录方式对指纹/面容登录方式的支持调研如下:

支付宝 微信 招商银行 淘宝 京东 抖音 美团 快狗打车司机 12306
账号+密码/手机+验证码/第三方登录等常见的登录方式,大部分App都支持
刷脸登录
指纹登录

上述支付宝支持的刷脸登录是使用的自实现的人脸识别能力,其它支持指纹/刷脸登录的均是借助系统本身的生物识别能力。从上述统计到的数据可以看出,目前支持指纹/面容登录方式的App占比并不算太多,那么是否可以猜测大部分企业还在考虑其安全性?

相关安全性可以参考苹果官方文档:support.apple.com/zh-cn/guide…

按上述苹果官方安全白皮书的描述,此种识别在安全上可以得到有效的保障,再看淘宝、招商银行等App均已支持此种登录方式,所以扩展此生物识别认证进行登录在将来未必不会是一个大趋势!

2.2 货拉拉专送司机端接入指纹/面容识别认证进行登录的总体方案

先说结论:本地指纹/面容认证 + 单次授权码 + ECDSA本地加签&服务端验签

「生物识别认证」为指纹识别认证及面容识别认证的统称

开启|关闭指纹/面容认证

使用生物识别认证前,需要先开启生物识别能力建立其与对应账号的联系。开启后也可自行关闭生物识别认证的登录能力(类比绑定/解绑微信等第三方授权登录)。

前提:当前App处于账号已登录状态

货拉拉专送司机iOS指纹及面容认证登录实践与总结

使用指纹/面容认证登录

开启生物识别认证成功后即可使用此功能进行快捷登录

货拉拉专送司机iOS指纹及面容认证登录实践与总结

开启、关闭、使用生物识别认证的核心流程大体上一致,只是针对不同的操作,端上与服务端还需要适配对应场景的业务。

货拉拉专送司机iOS指纹及面容认证登录实践与总结

2.3 为什么要使用服务端远程校验?

单纯的本机认证无法保证数据的真实性(可能被篡改)

假设上述开启(绑定)指纹/面容认证流程仅使用本机认证,那么对应流程将如下:

货拉拉专送司机iOS指纹及面容认证登录实践与总结

例如上述第9步中的请求,可能会被外部劫持,数据可能会被篡改,若唯一标识被修改为其它值,绑定成功后,即可用篡改后的信息尝试快捷登录。所以服务端也需要对接收到的数据进行有效性校验。

2.4 为什么要用授权码?

防止重放攻击

外部劫持不一定是要修改数据,可能会执行恶意的重复请求,会造成服务端重复的做出响应。比如登录行为,每次登录都会重新生成一个登录token,若被重放攻击,则之前已正常登录获取的token会失效。

可以通过增加一个授权码(一个随机数),来保证单次请求的有效性,此授权码使用一次后即作废。若请求授权码的时机相对较早,还可通过增加授权码的时效性来提高整体校验的安全性。

2.5 为什么要用加签而不是加密?为什么选择ECDSA加签算法?

先看微信授权登录的相关流程:developers.weixin.qq.com/doc/oplatfo…

货拉拉专送司机iOS指纹及面容认证登录实践与总结

上述流程中通过code获取access_token的过程,可获取到openIdopenId可作为身份唯一标识直接用于绑定到我们自己App当前登录的用户,openIdaccess_token可被后端服务用于在微信开放平台校验本次授权操作和身份信息的有效性和真实性。

很显然微信授权拿到的相关信息,有微信开放平台进行背书,可确保信息的真实性和有效性。微信等第三方平台的授权的数据校验我将之称为有源校验

本机指纹/面容识别借助的是设备系统自有的生物识别能力,苹果将系统录入的生物特征信息存储在Secure Enclave硬件安全区域,此区域保存的数据不会被直接加载到系统或用户空间,仅可使用预定义的命令集与Secure Enclave硬件进行交互,这些命令不允许访问原始数据,所以这些生物特征原始数据仅会存储在当前设备。使用生物识别能力时,系统Api也仅返回生物特征信息的比对结果,本机认证操作对应的生物特征数据没有外漏的可能,所以传给后端的认证结果数据也就无源可查。

对于无源可查的数据,服务端如何确保客户端传输的数据来源可靠且未被篡改?这种自证清白的数据校验我将之称为无源校验。

我们可以通过数据的加密解密或加签验签来进行数据的完备性检验。可以先了解一下加密与加签的区别:

特性/操作 加密(Encryption) 加签(Signing)
目的 保护数据的机密性。 验证数据的完整性和来源的真实性。
工作方式 对称加密使用同一密钥进行加解密 ,非对称加密使用公钥加密和私钥解密。 使用私钥生成签名,使用公钥验证签名。
密钥使用 对称加密中同一密钥用于加密和解密;非对称加密中使用一对密钥,公钥加密,私钥解密。 私钥用于生成签名,公钥用于验证签名。
数据可见性 加密后的数据不可读,只有拥有密钥的人能解密查看原始数据。 签名不影响数据的可讲性,任何人都可以读取数据,但只有签名者才能生成有效签名。
安全目标 防止未授权访问和阅读数据内容。 确保数据未被篡改,并验证数据来源的真实性。
应用场景 保护数据传输过程中的隐私(如加密邮件、文件、网络数据等)。 确认数据的合法来源和完整性(如软件更新验证、文档签名、身份认证等)。
典型使用 敏感数据的存储和传输,如个人信息、商业秘密等。 软件分发、电子商务交易、数字合同等场景,确保数据和交易的安全性。

可以看出加密侧重于数据内容的机密性,而加签侧重于验证数据的完整性和来源的真实性。使用加签验签的方案来确认数据未被篡改及来源可靠更符合当前使用场景。

那么为什么选用ECDSA(椭圆曲线数字签名算法)进行加签?

ECDSA其实是ECC非对称加密算法 + DSA签名算法的结合使用

货拉拉专送司机iOS指纹及面容认证登录实践与总结

从上述Security库中的定义说明可以看出,iOS系统提供的可直接使用的非对称加密算法只有RSA和ECC(原kSecAttrKeyTypeEC现用kSecAttrKeyTypeECSECPrimeRandom)。

RSA和ECC对比如下:

对比项 ECC RSA
密钥长度(相同加密强度) 一般采用256位加密长度 一般采用2048位加密长度
CPU占用 较高
内存使用 较高
加密速度 较慢
破解难度 基于ECC算法的数据特性,破解难度更大 一定的难度
抗攻击性 较弱
可扩展性 高,更短的密钥长度具有更大的扩展空间

出于移动端的性能、安全性、加密速度等综合考虑,选择ECC算法更佳。并且ECC算法的密钥可在上述所说的Secure Enclave硬件安全区域直接生成并存储,密钥无法被导出,更加安全。

Android端及接口服务端均可以很好的支持ECDSA加签/验签处理。

三、具体实现

上面对具体的探究过程和技术选型的原因进行了简单的阐述,理论说了一堆,但还是没有直接上代码来得亲切

3.1 环境配置

  • Xcode项目支持的目标系统版本号建议 >= 11.3 (对一些枚举值不需要做额外的兼容或显式转换)。

  • info.plist中添加NSFaceIDUsageDescription配置信息,用于首次使用面容识别能力时的弹窗授权使用说明。

3.2 核心库 LocalAuthentication

核心功能类LAContext

①检查设备生物识别能力

核心Api:-(BOOL)canEvaluatePolicy:(LAPolicy)policy error:(NSError **)error

判断当前设备硬件支持哪种生物识别能力,读取LAContext对象的biometryType值即可。注意读取此值前,必须保证调用过canEvaluatePolicy:error:方法,无论此方法的调用是成功还是失败,biometryType都将被赋予正确的值。

  • policy的值这里固定给LAPolicyDeviceOwnerAuthenticationWithBiometrics
  • biometryType具体值自行参考LABiometryType枚举定义

若执行出错,则此方法返回NO且error有值,error.code为具体错误码,可自行参考LAError枚举定义。

②使用生物识别

核心Api: -(void)evaluatePolicy:(LAPolicy)policy localizedReason:(NSString *)localizedReason reply:(void(^)(BOOL success, NSError *error))reply

示例代码如下:

self.context = [LAContext new];
__weak typeof(self) wk_self = self;
[self.context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics 
             localizedReason:<#someReason#> 
                       reply:^(BOOL success, NSError * _Nullable error) {
    dispatch_async(dispatch_get_main_queue(), ^{
        __strong typeof(self) self = wk_self;
        if (success) {
            //NSData *data = [self.context evaluatedPolicyDomainState];
            //evaluatedPolicyDomainState可做缓存,后续可用来对比判断设备指纹信息是否发生了变化(删除、新增)
            //本机生物识别成功,可做后续其它操作
        }else {
            //识别失败,具体原因可见error.code,查看LAError.h中定义的错误码
            //根据LAError.h中的错误码进行对应的提示或其它交互处理
        }
    });
}];
  • LAContext的属性配置

    • localizedReason

      • 一个简短的描述串用于表述使用此能力的目的。交互弹窗展示的副标题,会优先读取evaluatePolicy:localizedReason:reply:方法中的入参localizedReason。

    • localizedFallbackTitle

      • 可将其视为降级按钮的文案。默认为nil,对应在交互弹窗中展示的降级按钮的文案为’Enter Password’/’输入密码'(根据系统语言自动适配)。若设置为空串,则此降级按钮将始终不会展示。

  • policy策略

    • 常用的策略值如下,更多说明可见LAPolicy官方文档说明。
    • LAPolicy枚举 简要说明
      LAPolicyDeviceOwnerAuthenticationWithBiometrics 仅使用设备生物识别功能进行授权。
      LAPolicyDeviceOwnerAuthentication 使用设备生物识别功能及屏幕解锁密码联合授权(包括iPhoneApple Watchmac电脑)。当设备已录入指纹ID或FaceId时,优先使用对应的生物识别功能进行授权;若当前设备未录入生物信息或生物识别功能暂被锁定时,则使用屏幕解锁密码来进行授权。

我们使用策略值:LAPolicyDeviceOwnerAuthenticationWithBiometrics

  • reply回调

    • 注意:此回调在一个内部私有队列上被调用,所以通常我们在处理结果及与上层业务进行交互时,一般会异步到其它的自定义队列或现有的mainQueue(若需要直接响应App内的UI交互)中去执行后续处理逻辑。

    • 识别流程中任意情况导致的流程结束,都会执行此reply回调,当success为NO时,可再结合error.code判断具体的失败原因。

③处理生物识别结果

识别成功则可以直接进入下一步数据加签等处理流程

canEvaluatePolicyevaluatePolicy方法调用时对应的的error.code均可参考LAError的枚举值

常见的主要失败场景对应错误码如下:

LAError枚举值 描述
LAErrorPasscodeNotSet 未设置密码,生物识别功能不可用
LAErrorBiometryNotAvailable 设备不支持Touch ID或 Face ID
LAErrorBiometryNotEnrolled 设备未录入Touch ID或 Face ID信息
LAErrorBiometryLockout 使用生物识别失败次数太多,当前处于被锁定状态,暂时无法使用生物识别能力。锁屏后通过密码解锁屏幕后可继续使用生物识别功能。
LAErrorAuthenticationFailed 正常识别失败
LAErrorUserCancel 识别失败:用户点击了取消按钮
LAErrorUserFallback 识别失败:用户点击了降级按钮(输入密码或自定义文案的按钮)
LAErrorSystemCancel 识别失败:被系统取消 (比如将app退到后台、锁屏等打断行为)
LAErrorAppCancel 识别失败:识别过程中主动调用了invalidate接口
LAErrorInvalidContext 识别失败:非识别过程中主动调用了invalidate接口

上层业务根据实际需要对上述错码码做不同的反馈处理即可。

  • LAErrorUserFallback,通常降级为普通登录流程。

  • LAErrorBiometryLockout,一般情况会是弹窗提示,也可在弹窗提示消失后自动执行降级处理,体验更好。

  • 其它错误码按需处理

想了解更多其它信息,可自行查阅 LocalAuthentication 库的官方说明文档。

3.3 信息加签 ECDSA

①创建删除密钥对

  • 创建密钥&存储keyChain

示例代码如下:

CFErrorRef err = NULL;
SecAccessControlRef secAcRef = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecAccessControlPrivateKeyUsage, &err);
NSDictionary *params = @{
    (id)kSecAttrTokenID: (id)kSecAttrTokenIDSecureEnclave,
    (id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
    (id)kSecAttrKeySizeInBits: @(256),
    (id)kSecAttrLabel: <#自定义的标识串#>
    (id)kSecAttrApplicationTag: <#自定义的标签串#>
    (id)kSecPrivateKeyAttrs: @{
        (id)kSecAttrAccessControl: CFBridgingRelease(secAcRef),
        (id)kSecAttrIsPermanent: @(YES)
    }
};
NSError *gen_err = nil;
SecKeyRef newPrivateKeyRef = SecKeyCreateRandomKey((CFDictionaryRef)params, (void *)&gen_err);
if (newPrivateKeyRef != NULL) {
    //私钥创建成功,可直接用来进行后续的加密操作
    CFRelease(newPrivateKeyRef);
}else if (gen_err) {
    //创建失败时,gen_err.code即对应OSStatus状态码,自行查看对应状态码意思
}

参考苹果官方API文档:developer.apple.com/documentati…

货拉拉专送司机iOS指纹及面容认证登录实践与总结

若想在Secure Enclave硬件安全区域生成ECC的密钥信息,按上述官方文档所述进行调用即可。

  • 查检密钥是否存在

通常在创建密钥前,我们会检查是否已存在满足条件的密钥,若已存在可根据实际需求决定是直接复用还是先删除再创建新的密钥

keyChain中匹配信息,调用方法SecItemCopyMatching,入参字典与创建密钥的字典信息大致一样但小有区别:

  • 去除kSecPrivateKeyAttrs键值信息

  • 指定kSecAttrKeyClass的值为:kSecAttrKeyClassPrivate

  • 指定kSecReturnRef的值为:YES

  • 删除密钥

当执行绑定失败或执行解绑成功后或其它特殊场景需要主动删除对应的密钥

将密钥信息从keyChain中删除,调用方法SecItemDelete,入参字典与上述匹配查询信息也大体一致。

②导出公钥串

私钥创建后,无法直接通过私钥引用对象获取到具体的公钥数据,需要先将公钥信息写入到keyChain中才能获取到具体的公钥数据

示例代码如下:

//cf_privateKeyRef 为上面流程生成的密钥对象
SecKeyRef cf_publicKeyRef = SecKeyCopyPublicKey(cf_privateKeyRef);
id publicKeyRef = (id)CFBridgingRelease(cf_publicKeyRef);
NSDictionary *params = @{
        (id)kSecClass: (id)kSecClassKey,
        (id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
        (id)kSecAttrKeySizeInBits: @(256),
        (id)kSecAttrLabel: <#自定义的标识串,与创建时传入的值要一致#>
        (id)kSecAttrApplicationTag: <#自定义的标签串,与创建时传入的值要一致#>
        (id)kSecValueRef: publicKeyRef,
        (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPublic,
        (id)kSecReturnData: @(YES),
};
CFTypeRef dataRef = NULL;
NSData *publicData = nil;
OSStatus status = SecItemAdd((CFDictionaryRef)params, &dataRef);
if (status == errSecSuccess) {
    publicData = (NSData *)CFBridgingRelease(dataRef);
}
if (publicData) {
    //⚠️这里得到的公参key数据不包含26字节长度的头部信息(与Android及后台保持一致,需要自行插入头部数据)
    NSMutableData *fullPublicKeyData = [[NSMutableData alloc] initWithBytes:Secp256r1header length:sizeof(Secp256r1header)];
    [fullPublicKeyData appendData:publicData];
    NSString *publicKeyBase64Str = [fullPublicKeyData base64EncodedStringWithOptions:0];
    //publicKeyBase64Str可传给后端作为公钥串
}

如上述代码里注释所述,iOS读取到的公钥Data是不包含Secp256r1头部数据的(即固定的ASN.1 OID 的标头数据),所以需要自行手动拼接,相关定义如下:

参考:developer.apple.com/forums/thre…

unsigned char Secp256r1header[] = {
    0x30, 0x59, 0x30, 0x13, 0x06, 0x07,
    0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02,
    0x01, 0x06, 0x08, 0x2A, 0x86, 0x48,
    0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03,
    0x42, 0x00
};

③使用私钥加签

示例代码如下:

NSData *toSignData = [toSignString dataUsingEncoding:NSUTF8StringEncoding];
NSError *err = nil;
//cf_privateKeyRef 为上述流程创建或读取到的私钥引用对象
CFDataRef signedDataRef = SecKeyCreateSignature(cf_privateKeyRef, kSecKeyAlgorithmECDSASignatureMessageX962SHA256, (CFDataRef)toSignData, (void *)&err);
if (signedDataRef != NULL) {
    NSString *base64Str = [(NSData *)CFBridgingRelease(signedDataRef) base64EncodedStringWithOptions:0];
    //base64Str即为加签后的base64加签串
}

上述代码中的toSignData即为需要进行加签的内容数据,要加签的内容数据的生成规则由业务侧与服务端自行协定。

④本地使用公钥验签

系统Api获取到的公钥信息不包含Secp256r1标头,所以本地验签时传入的公钥数据也需要先移除此标头

//keyData为移除了secp256r1头部信息的公钥数据
SecKeyRef publicKeyRef = SecKeyCreateWithData((CFDataRef)keyData, ...);
//originData 为要校验的用于加签的原始数据data
//signedData 为加签后的数据
NSError *err = nil;
Boolean verified = SecKeyVerifySignature(publicKeyRef, kSecKeyAlgorithmECDSASignatureMessageX962SHA256, (CFDataRef)originData], (CFDataRef)signedData, (void *)&err);

通常可以与Android端交换进行本地验签,若验签通过则能确保双端加签验签的一致性。

⑤远程账号、设备、识别信息管理

账号与设备、识别类型映射关系

货拉拉专送司机iOS指纹及面容认证登录实践与总结
货拉拉专送司机iOS指纹及面容认证登录实践与总结

如上图所述,一个账号可能与多台设备不同的生物识别类型有对应绑定关系,同样一个设备也可能关联多个账号,所以对于同一个账号,其绑定的设备及对应的生物识别类型关系及存储的公钥等信息应该需要进行统一的管理(增、删、改、查)

3.4 数据缓存

在整个绑定、解绑、使用的流程中,涉及的一些数据缓存,此处仅在基于本文所述的大致流程上对期间的数据缓存处理做一些简要的说明及建议。

以下’绑定’对应’开启’功能,’解绑’对应’关闭’功能。

  • 绑定流程中生成的用于加签内容的随机串(若需要则通常需要存储,后续使用时再读取此缓存参与加签的数据组装),绑定流程中本机认证成功后读取到的[context evaluatedPolicyDomainState](若需要对比先后两次数据是否一致来提示需要重新绑定,则此值会被用到)
  • 绑定/解绑后的开关状态标记及对应的基本账号信息(需要缓存的账号信息由使用快捷登录时进行加签所需及登录所需传递哪些账号数据而定)

上述提到的中间过程产生的数据,通常在开启操作时进行缓存,在开启结果失败或关闭结果成功后移除缓存。

3.5 更安全的本地校验探究

上述流程其实可以看出,本机生物识别认证校验与私钥的创建存储、加签处理是流程串连的,也就意味着如果抛开本机生物识别认证,仍然可以直接获取私钥引用对象进行后续的加签等处理,那么是否可以将本机生物识别认证校验与私钥的读取使用进行直接关联?

理论上的思路大致如下:

  1. 创建密钥并存储到keyChain时指定后续的访问必须通过生物识别认证(配置初始化)

  2. 要使用密钥进行数据加签,从keyChain读取密钥时,联合生物识别认证功能(配置验证)

Q1. 实际验证时发现,通过上述创建密钥的方式,就算指定访问时需要生物识别认证,但实际读取私钥时仍可直接读取。

创建ECC密钥时,创建访问权限对象时,调整相关方法入参如下:SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecAccessControlPrivateKeyUsage | kSecAccessControlBiometryAny, &err)最终尝试读取私钥引用对象时未触发生物识别认证的交互弹窗。暂不确定是使用方式不对,还是跟密钥在Secure Enclave硬件安全区域进行创建有关。

创建RSA类型密钥存储到keyChain并指定生物识别认证的访问,从keyChain读取指定RSA的密钥信息时可以正常触发生物识别认证的交互弹窗,且识别成功后能正确读取对应密钥引用对象。说明上述思路具备可行性。

Q2. 存储数据到keyChain并指定后续的访问需要通过生物识别认证,在按普通流程从keyChain读取指定数据时会自动触发生物识别弹窗交互,若识别成功则可正常读取到数据,若识别出错,则对应错误码见OSStatus的定义。

不建议使用keyChain直接访问去触发生物识别认证的交互弹窗,因此情况下自动出现的交互弹窗不能自定义弹窗中的提示文案

可以联合LocalAuthentication相关功能的使用将对应的LAContext对象绑定到KeyChain作为授权访问的对象入参,相关示例代码如下:

context = [LAContext new];
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics 
         localizedReason:<#描述使用目的#> 
         reply:^(BOOL success, NSError * _Nullable error) {
        dispatch_async(dispatch_get_main_queue(), ^{
                NSDictionary *readParams = @{
                                (id)kSecClass: (id)kSecClassKey,
                                (id)kSecAttrKeyType: ...,
                                (id)kSecAttrKeySizeInBits: ...,
                                (id)kSecAttrLabel: <#自定义的标识串,与创建时传入的值要一致#>
                                (id)kSecAttrApplicationTag: <#自定义的标签串,与创建时传入的值要一致#>
                                ...
                                (id)kSecReturnRef: @(YES),
                                (id)kSecUseAuthenticationContext: context //此处传入已本地认证通过的LAContext对象
                        };
                SecKeyRef readPrivateKeyRef = NULL;
                OSStatus st = SecItemCopyMatching((CFDictionaryRef)readParams, (CFTypeRef *)&readPrivateKeyRef);
        });
}];

若能解决上述Q1中碰到的问题,那么通过生物认证联合读取私钥的流程的安全级别会更高。

四、总结

4.1 部分页面截图说明

生物识别认证登录功能集成后,最终App上呈现的部分页面如下:

①绑定页

货拉拉专送司机iOS指纹及面容认证登录实践与总结
此为引导绑定的页面,作为一个新功能的出现,通常会展示一个引导页,此页面不应该堵塞主流程,故可交互进行跳过或指定下次不再提醒。在开关设置页执行开启时,将会直接触发认证交互弹窗。

②使用页

货拉拉专送司机iOS指纹及面容认证登录实践与总结
货拉拉专送司机iOS指纹及面容认证登录实践与总结
货拉拉专送司机iOS指纹及面容认证登录实践与总结
前提:先绑定成功1. 退出登录后展示的快捷登录页(左图)。
  1. 点击登录后出现的系统认证交互弹窗(中图)。指纹认证时默认出现取消按钮,面容认证时会在认证失败后出现此取消按钮。
  2. 尝试生物认证失败时出现的系统交互弹窗(展示了’验证码登录’按钮)(右图)(指纹失败一次即出现,面容失败多次才出现)。”验证码登录”即为自定义的降级按钮文案,在结果回调中可以区分此点击事件降级为原有的验证码登录流程。 | | |

③开关设置页

货拉拉专送司机iOS指纹及面容认证登录实践与总结
设置页有此功能的开关项,可自行开启关闭操作。开关状态建议结合本地缓存数据及后台绑定状态联合判定。当本地存在上一次此账号绑定时的加签私钥等信息缓存数据&服务端存在此账号与此设备及指定类型/面容类型的绑定关系,此两个条件均满足时,此开启状态才准确可用。

4.2 踩坑记

①问题现踪

功能上线前,我们对整个交互流程关键节点及认证结果增加了埋点上报,以观测功能运作是否正常。但是灰度阶段就发现了部分异常数据。

异常的场景及错误信息如下:

错误场景 错误信息 直接原因
认证授权登录(使用) 设备密钥校验失败 使用快捷认证登录的接口返回的错误信息
开启认证授权(绑定) 设备密钥绑定数量已达上限 开启(绑定)的接口返回的错误信息
关闭认证授权(解绑) 关闭失败 端上未查询到开启记录,直接按操作失败处理

②排查定位

参考第①步统计到的具体错误描述串,再通过进一步查看对应几个接口调度日志,最终发现绑定接口的入参账号id为0的异常数据记录,对应的异常流程梳理如下:

货拉拉专送司机iOS指纹及面容认证登录实践与总结

如上述流程,若多个账号、多个设备在执行各自的开启认证操作时,传入的账号id均为0,那么当绑定到异常账号id=0的设备达到指定的数量上限时,即绑定接口就会触发返回”设备密钥绑定数量已达上限”的错误信息;

当未触发绑定数据的上限时则表现为绑定成功,但实际绑定的账号id为0,所以后续使用此生物识别认证进行登录时,对应登录接口返回”设备密钥校验失败”的错误信息;

当执行解绑操作时,因之前绑定到了账号id为0的账户,故当前实际登录的账号查不到绑定信息,所以直接提示了”关闭失败”;

排查端上代码,发现在发起获取授权码、绑定、使用的相关接口请求时,部分请求参数的赋值处理存在App版本兼容性问题,具体如下:

- (void)loadDefaultParam:(__kindof QuickAuthBizBaseReqParam *)param {
    param.deviceId = ...;
    param.deviceKeyType = ...;
    <#SomeQuickAuthLoginClass#> *info = [<#SomeQuickAuthLoginClass#> latestLoginUserInfo];
    param.userId = [info.driverId integerValue];
}

上述latestLoginUserInfo为此功能相关的账号信息缓存数据。非登录态时使用快捷登录、登录态时进行绑定、解绑等操作对应接口的请求参数共用了此处的赋值逻辑。

而当前仅在以下两个场景才执行相关信息的缓存:

  • App执行常规登录流程后,会更新缓存latestLoginUserInfo数据
  • App在退出登录前,会更新缓存latestLoginUserInfo数据

正常的登录后再使用的操作流程满足上述先缓存再使用的要求,所以正常自测及测试时并未出现上述异常。但若App是从低版本升级到新功能对应的版本后,在操作操作前未重新执行登录流程,则不会触发上述缓存处理,后续执行绑定操作时读到的latestLoginUserInfo为nil,即导致出现后续的一系列接口返回错误信息的异常。

关于上述无法正常关闭的异常,或可判定是解绑逻辑过于严谨,我们在解绑流程中仍然执行了完整的数据校验流程,只要数据不能正常完成校验,则整个解绑流程按失败处理。获取当前账号、设备、生物识别类型的绑定关系的接口传参正常,上述绑定时的错误数据导致此时获取到的结果是无绑定信息,从而直接按解绑失败处理。

③修复

所以在上述 3.4 数据缓存章节,特意阐述了数据缓存的时机。

在执行绑定操作时执行一次数据缓存,即可保证后续流程中读取到的缓存数据有效。此问题已在后续版本中修复:

版本兼容:旧版本时已出现上述异常的情况下,升级到新版本使用此功能时,对特定错误码的场景做修正处理:清除本机对应缓存数据&提示用户重新绑定

为了解决本次数据错误导致的解绑一直失败的问题,特在解绑流程中增加此异常场景的兼容处理:

当查询不到当前账号与当前设备、生物类型的绑定关系时,执行解绑操作时,直接清除本地缓存数据并反馈为解绑成功。

所以在上述截图说明中也再次阐明了绑定状态标识的有效性判定逻辑。相对较好的方案就是保证本地缓存与后端存储关系的有效同步更新。

④自省

出现上述问题的本质原因还是App版本兼容问题。

我们在做一个新功能的时候,应该充分考虑兼容问题,不仅是系统版本兼容,还要考虑App新旧版本的兼容,需要我们提前规划准备好兼容性的自测用例,用例覆盖面越全,则对应兼容的逻辑遗漏点越少。

根据实际需要还可考虑降级的方案,当出现问题后能及时进行降级,防止影响面扩大,造成线上事故!

4.3 小结与展望

本文简单阐述了指纹/面容识别授权进行登录的大致流程,也提供了部分核心代码实现供大家参考。

其中关于加签密钥的访问、使用与生物识别认证结合的代码实现经过多次调试验证终究还是未能如愿,但官方文档也确实明确指明了此交互的可行性:support.apple.com/zh-cn/guide…

若不考虑第三方引流的需要(第三方授权登录,如微信授权登录等),使用各平台设备系统自带的认证功能或关联系统级账号的方式来授权进行快捷登录,可以让用户基本忘记要记密码的烦恼,让整个登录流程体验更佳!