LocalAuthentication开发实践

在iPhone 5s加入Touch ID后,指纹识别的功能在App中逐渐受到青睐,特别是对于本地安全较高的应用(如带支付的App)指纹识别是必备的功能,它既能解决在验证过程中输入密码的繁琐过程,同时指纹识的安全等级更高。那么,要想在自己开发的应用中使用指纹识别,就必须要LocalAuthentication.framework提供的API,下面将详细地介绍如何使用这个框架来实现指纹识别功能。

基础用法

我们先来看下面的例子:

LAContext *context = [[LAContext alloc] init];
    
NSError *error = nil;
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error])
{
    [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:@"输入指纹进行验证" reply:^(BOOL success, NSError * _Nullable error) {
            
        if (success)
        {
            NSLog(@"验证成功");
        }
        else
        {
            NSLog(@"验证失败");
        }

    }];
}
else
{
    NSLog(@"识别功能不可用");
}

我们来解读一下上面的代码:

  • LAContext为贯穿整个识别过程的对象类型。使用识别必须初始化一个LAContext对象。

  • 在进行指纹(人脸)识别前,需要判断识别功能是否可用,上面代码中的canEvaluatePolicy: error :方法就是做这样一件事情。当方法返回YES时则可以继续调用识别方法。否则需要根据error的描述来提示用户。该方法的policy参数决定了鉴权的行为方式,该参数取值如下:

取值 说明
LAPolicyDeviceOwnerAuthenticationWithBiometrics 指纹(人脸)识别。验证弹框有两个按钮,第一个是取消按钮,第二个按钮可以自定义标题名称(输入密码)。只有在第一次指纹验证失败后才会出现第二个按钮,这种方式下的第二个按钮功能需要自己定义。前三次指纹验证失败,指纹验证框不再弹出。再次重新进入验证,还有两次验证机会,如果还是验证失败,TOUCH ID 被锁住不再继续弹出指纹验证框。以后的每次验证都将会弹出设备密码输入框直至输入正确的设备密码才能重新使用指纹(人脸)识别
LAPolicyDeviceOwnerAuthentication 指纹(人脸)识别或系统密码验证。如果Touch ID (Face ID)可用,且已经录入指纹(人脸),则优先调用指纹(人脸)验证。其次是调用系统密码验证,如果没有开启设备密码,则不可以使用这种验证方式。指纹(人脸)识别验证失败三次将弹出设备密码输入框,如果不进行密码输入。再次进来还可以有两次机会验证指纹(人脸),如果都失败则Touch ID(Face ID)被锁住,以后每次进来验证都是调用系统的设备密码直至输入正确的设备密码才能重新使用指纹(人脸)识别

该方法可以能返回的错码如下所示:

错误码 说明
LAErrorPasscodeNotSet 没有设置设备密码,无法使用指纹(人脸)识别
LAErrorTouchIDNotAvailable 设备不支持Touch ID/Face ID,iOS 11被标注过时,需要使用LAErrorBiometryNotAvailable代替
LAErrorBiometryNotAvailable 设备不支持Touch ID/Face ID,iOS 11新增,由于新增Face ID,故用来代替LAErrorTouchIDNotAvailable
LAErrorTouchIDNotEnrolled 没有录入指纹/人脸,iOS 11被标注过时,需要使用LAErrorBiometryNotEnrolled代替
LAErrorBiometryNotEnrolled 没有录入指纹/人脸,iOS 11新增,由于新增Face ID,故用来代替LAErrorTouchIDNotEnrolled
LAErrorBiometryLockout 超过重试限制,Touch ID/Face ID被锁定,需要进行设备密码解锁后重新激活
  • 检测可用后,调用evaluatePolicy:localizedReason:reply:方法来进行指纹(人脸)识别。其中policy参数应该与调用canEvaluatePolicy: error :方法时传入的policy一致。而localizedReason则是显示在识别标题下面的一栏描述文本,如图所示:

localizedReason显示位置
localizedReason显示位置

该方法返回的错误如下所示:

错误码 说明
LAErrorPasscodeNotSet 没有设置设备密码,无法使用指纹(人脸)识别
LAErrorTouchIDNotAvailable 设备不支持Touch ID/Face ID,iOS 11被标注过时,需要使用LAErrorBiometryNotAvailable代替
LAErrorBiometryNotAvailable 设备不支持Touch ID/Face ID,iOS 11新增,由于新增Face ID,故用来代替LAErrorTouchIDNotAvailable
LAErrorTouchIDNotEnrolled 没有录入指纹/人脸,iOS 11被标注过时,需要使用LAErrorBiometryNotEnrolled代替
LAErrorBiometryNotEnrolled 没有录入指纹/人脸,iOS 11新增,由于新增Face ID,故用来代替LAErrorTouchIDNotEnrolled
LAErrorBiometryLockout 超过重试限制,Touch ID/Face ID被锁定,需要进行设备密码解锁后重新激活
LAErrorAuthenticationFailed 验证失败,指的是指纹(人脸)不匹配
LAErrorUserCancel 用户点击了取消按钮
LAErrorUserFallback 用户点击了输入密码按钮
LAErrorSystemCancel 系统强制取消,可能由于其他应用进入前台
LAErrorAppCancel 应用调用了LAContextinvalidate方法
LAErrorNotInteractive 应用尚未启动完成或者已经进入非激活状态时调用验证方法会收到该错误,例如:将验证方法放到didEnterBackground方法中进行可能会导致这个错误。

通过上面的例子和解释,大家对LocalAuthentication这个框架应该有了一定的了解吧,但是作为一种验证方式,上面的做法是不够安全和严谨的。举个例子,如果我知道你的设备密码,然后通过密码登录你的手机,然后我在你的设备上登记了我的指纹,那么按照上面代码的逻辑,我的指纹也是能够验证通过的。因此,这里需要借助iOS 9上LAContext的一个新属性evaluatedPolicyDomainState来解决这个问题(没听错,是iOS 9上新增的,也就是说在iOS 8上会存在我说的这种问题,可能苹果爸爸一开始考虑,既然手机设备密码都泄漏了,那么自然手机里面的信息都是不安全的,但是往往App中的验证体系是独立于系统的,某些重要的App密码不泄漏还是很安全的,但是加入了指纹识别后可能就变得不安全了,所以不建议iOS 8上实现指纹识别功能)。

evaluatedPolicyDomainState表示当下Touch ID/Face ID的一个状态,没有其他的含义。当在在设备上添加、删除指纹(人脸)时,这个值就会发生变化。所以,拿到这个值作比对就能够很容易知道是否有发生变化,下面将继续介绍具体的实现方法。

最佳实践(仅iOS 9及以上)

在写代码前,先来理清楚整个实现的过程,在一般的情况下我们都会给应用做一个开关,用于控制是否开启指纹识别。那么,基于这个前提,我们可以作如下的流程处理:

  1. 设置一个开关(UISwitch
  2. 当开关开启时,要求用户进行指纹识别,在识别成功后将evaluatedPolicyDomainState保存起来,用于后续指纹验证时对比。
  3. 在需要验证的地方,使用指纹识别API进行验证,同时获取evaluatedPolicyDomainState来比对之前保存的值,如果相同则验证成功,否则验证失败,需要进行后续的处理,如需要输入应用账号的密码。
  4. 通过App自身验证体系检测通过后在把新的evaluatedPolicyDomainState保存起来,用于往后的验证操作。

理解上面的流程后,我们来看下面的示例代码:

- (IBAction)switchChangedHandler:(id)sender
{
    if (self.touchIdSwitch.on)
    {
        //开启指纹
        LAContext *context = [[LAContext alloc] init];
        if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:nil])
        {
            //进行第一次的验证,成功后记录验证状态
            [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:@"输入指纹开启验证" reply:^(BOOL success, NSError * _Nullable error) {
                
                if (success)
                {
                    self.policyDomainState = context.evaluatedPolicyDomainState;
                }
                else
                {
                    NSLog(@"验证失败");
                    self.touchIdSwitch.on = NO;
                }
                
            }];
        }
        else
        {
            NSLog(@"Touch ID/Face ID不可用");
            self.touchIdSwitch.on = NO;
        }
    }
    else
    {
        self.policyDomainState = nil;
    }
}
- (IBAction)validationButtonClickedHandler:(id)sender
{
    if (self.touchIdSwitch.on)
    {
        LAContext *context = [[LAContext alloc] init];
        if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:nil])
        {
            //进行第一次的验证,成功后记录验证状态
            [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:@"使用指纹识别验证" reply:^(BOOL success, NSError * _Nullable error) {
                
                if (success)
                {
                    if ([context.evaluatedPolicyDomainState isEqualToData:self.policyDomainState])
                    {
                        NSLog(@"验证通过!");
                    }
                    else
                    {
                        NSLog(@"指纹发生变化,请进行后续验证步骤");
                        //这里可以弹出App的登录界面让用户重新登录
                        //等待登录完成后再将evaluatedPolicyDomainState保存起用于后续的操作
                        [self doAppAuthentication:^(BOOL success) {
                            
                            if (success)
                            {
                                self.policyDomainState = context.evaluatedPolicyDomainState;
                                NSLog(@"验证通过!");
                            }
                            
                        }];
                    }
                }
                else
                {
                    NSLog(@"验证失败");
                }
                
            }];
        }
        else
        {
            NSLog(@"Touch ID/Face ID不可用");
        }
    }
}
- (void)doAppAuthentication:(void (^)(BOOL success))handler
{
    //执行应用自身验证体系,并将验证结果回调
    if (handler)
    {
        handler (YES);
    }
}

整个示例的界面如下所示:

示例运行效果
示例运行效果

代码中的switchChangedHandler:方法为点击界面中的开关按钮的触发事件。可以看到当触发该事件时,而且开关处于打开状态,就要进行一次指纹验证,只有当指纹识别通过时才视为真正启用指纹识别功能,然后把evaluatedPolicyDomainState保存起来(为了方便演示,示例中只在内存中保留,原则上要将该值进行本地保存)。

然后当点击示例中的“验证指纹”按钮时就会触发validationButtonClickedHandler方法,这个时候会调起指纹验证。在验证回调中如果successYES并且evaluatedPolicyDomainState与之前记录的值相同时才算是验证通过。如果仅仅是successYES那么就要进行进一步的身份验证,示例中就会调起doAppAuthentication方法。在该方法中进行App相关的验证方法,例如让用户重新使用账号密码进行登录等一系列鉴定用户的操作。那么,为了方便演示,示例中直接认为App验证用户身份成功,在App验证成功后要做的一步操作就是将原来保存的evaluatedPolicyDomainState替换成新的,后续就基于这个指纹状态来校验用户身份了。(注:我们没有办法知道指纹的删除和录入是否是设备持有人的操作,所以基于安全的角度考虑,只要有修改就应该进行应用自身的验证操作来确保一些恶意行为)。

上面所说的是一个相对完整的实践过程,可能会存在一些理解有误的地方,如果发现了希望同学们给予指出。讲到这里,LocalAuthentication框架的内容并没有全部结束,还有一些其他的内容,下面进行一一讲解。

Face ID的验证过程实践

Face ID其实就是人脸识别中对人脸的唯一标识。苹果目前在iPhone X设备中应用了这项技术,通过人脸识别来解锁设备。同样如果App内需要使用人脸识别来解锁某些访问,也是使用LAContext来实现,而且实现流程跟Touch ID一样,几乎不需要改写任何代码。唯一需要注意的地方是,iOS 11后LAContext新增一个只读属性biometryType,该属性表示当前设备支持生物识别类型(是Touch ID还是Face ID),其枚举值说明如下:

枚举值 说明
LABiometryTypeNone 表示设备不支持生物识别技术
LABiometryNone 在iOS 11中已经过时,使用LABiometryTypeNone代替
LABiometryTypeTouchID 表示当前设备支持指纹识别
LABiometryTypeFaceID 表示当前设备支持人脸识别

那么,这个值最重要的作用就是让你可以区分到底现在使用的是Touch ID还是Face ID,然后编码时可以根据类型来作不同的提示和判断。例如:

NSString *reason = @"使用指纹识别验证";
if (@available(iOS 11.0, *))
{
    if (context.biometryType == LABiometryTypeFaceID)
    {
        reason = @"使用人脸识别验证";
    }
}
            
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:reason reply:^(BOOL success, NSError * _Nullable error) {

    //Do something...
}];

注意:使用Face ID功能必须要在Info.plist中加入NSFaceIDUsageDescription并添加准确的使用描述。

控制Keychain访问权限

在iOS 9之前我们写入到Keychain的数据,在设备解锁后就能够对keychain中的数据进行访问,其实这样是不够安全的,特别是在你的App使用第三方SDK的情况底下,很有可能就会去窃取App中的keychain数据。那么,在iOS 9之后,系统加入了一项新的功能,就是允许应用来控制Keychain的数据访问。它的实现过程是在写入数据时添加一个应用级别的访问密码,后续要访问这个数据除了要设备解锁,还需要有正确的密码才能够访问Keychain中的数据。而这功能正好是集成到了LocalAuthentication这个框架中,下面我们来探索一下这个功能的用法。

要实现Keychain的控制访问,依然还是要依靠LAContext来实现,我们可以看到在iOS 9之后,这个类型新增了一个方法setCredential:type:。这个方法的作用就是把访问Keychain的密码设置到LAContext对象中,配合LACredentialTypeApplicationPassword这个类型就能够实现控制访问了,下面先来看一下实现的示例代码:

OSStatus status = noErr;
CFErrorRef error = NULL;

SecAccessControlRef acl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleAfterFirstUnlock, kSecAccessControlApplicationPassword, &error);
if (acl)
{
    NSString *data = @"要写入的数据";
    
    LAContext *context = [[LAContext alloc] init];
    NSData *password = [@"123456" dataUsingEncoding:NSUTF8StringEncoding];
    [context setCredential:password type:LACredentialTypeApplicationPassword];
    
    NSDictionary *saveDictionary = @{
                                     (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
                                     (__bridge id)kSecAttrService: @"testService",
                                     (__bridge id)kSecAttrAccount: @"testAccount",
                                     (__bridge id)kSecValueData: [data dataUsingEncoding:NSUTF8StringEncoding],
                                     (__bridge id)kSecAttrAccessControl: (__bridge id)acl,
                                     (__bridge id)kSecUseAuthenticationContext:context
                                     };
    
    status = SecItemAdd((__bridge CFDictionaryRef)saveDictionary, nil);
    if (status == noErr)
    {
        NSLog(@"Item stored to keychain");
    }
    
    CFRelease(acl);
}

从上面写入Keychain信息的代码可以看到,在创建访问控制权限时使用的是kSecAccessControlApplicationPassword枚举值,这是iOS 9新增的选项,表示你的Keychain数据访问要使用应用密码。同时也可以看到保存的Keychain数据结构中多了一项kSecUseAuthenticationContext,这也是iOS 9新增的,它对应的值就是setCredential后的LAContext对象。

通过这样的操作,Keychain数据就能够实现应用密码访问的功能了。下面我们再来看看如何读取这样类型的数据:

OSStatus status = noErr;
CFErrorRef error = NULL;
SecAccessControlRef acl = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                                          kSecAttrAccessibleAfterFirstUnlock,
                                                          kSecAccessControlApplicationPassword, &error);
if (acl)
{
    NSString *password = @"访问密码";
    
    LAContext *context = [[LAContext alloc] init];
    NSData *appPassword = [password dataUsingEncoding:NSUTF8StringEncoding];
    [context setCredential:appPassword type:LACredentialTypeApplicationPassword];
    
    NSDictionary *loadDictionary = @{
                                     (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
                                     (__bridge id)kSecAttrService: @"testService",
                                     (__bridge id)kSecAttrAccount: @"testAccount",
                                     (__bridge id)kSecReturnData: @(YES),
                                     (__bridge id)kSecMatchLimit: (NSString *)kSecMatchLimitOne,
                                     (__bridge id)kSecAttrAccessControl: (__bridge id)acl,
                                     (__bridge id)kSecUseAuthenticationContext:context
                                     };
    
    CFDataRef data = NULL;
    status = SecItemCopyMatching((CFDictionaryRef)loadDictionary, (CFTypeRef *)&data);
    
    if ( (status == noErr))
    {
        if ( 0 < CFDataGetLength(data)) 
        {
            CFStringRef string = CFStringCreateWithBytes(kCFAllocatorDefault, CFDataGetBytePtr(passwordData), CFDataGetLength(passwordData), kCFStringEncodingUTF8, FALSE);
            if (string)
            {
                NSLog(@"data = %@", [[NSString alloc] initWithData:(__bridge NSData *)data encoding:NSUTF8StringEncoding]);
                CFRelease(string);
            }
        }
    }
    else
    {
        NSLog(@"密码错误,无法访问");
    }
    
    
    
    if (data != NULL)
    {
        CFRelease(data);
    }
    
    CFRelease(acl);
}

读取的操作跟写入操作类似,同样要将访问权限设置为kSecAccessControlApplicationPassword,然后搜索信息中要通过kSecUseAuthenticationContext来指定已经设置过密码的LAContext对象。如果密码正确就能够访问到数据,否则无法取到对应的数据。

那么,这个功能的使用场景应该是怎么样的呢?其实我个人觉得还是挺多地方会用到的,例如:你跟服务器进行用户验证后通常都会下发一个令牌(token),而这个令牌就是你能操作应用的凭证,如果存在普通的文件或者数据库中那其实有可能会被利用。那么,就可以利用这样的功能让服务器提供一个随机的密码,然后将令牌保存到keychain中。又例如像一些本地的密码工具软件,用于加密数据的密钥不能直接保存到文件中,也可以利用这个功能,将用户解锁密码以及密钥保存到Keychain,只有用户成功解锁应用,才能够读取里面的信息。

复用设备解锁授权

如果你的应用想要在设备使用Touch ID/Face ID解锁后一段时间内自己的App也不需要重新弹出解锁界面,就可以使用LAContexttouchIDAuthenticationAllowableReuseDuration属性,该属性表示从设备解锁后多长时间内不需要重新验证的时间(有些文章说支付宝解锁后Home健返回桌面再重新进去的时间,其实是错误的)。该属性默认值为0,表示不采用设备解锁来授权应用。该属性允许最大的设置时长为5分钟(注:属性值为300,因为是以秒为单位,也可以使用LATouchIDAuthenticationMaximumAllowableReuseDuration常量值)。具体看下面例子:

LAContext *context = [[LAContext alloc] init];
context.touchIDAuthenticationAllowableReuseDuration = 5;

NSError *error = nil;
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error])
{
    [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:@"输入指纹进行验证" reply:^(BOOL success, NSError * _Nullable error) {
            
        if (success)
        {
            NSLog(@"验证成功");
        }
        else
        {
            NSLog(@"验证失败");
        }

    }];
}
else
{
    NSLog(@"识别功能不可用");
}

上面的例子你可以通过两种情况来测试:

  1. 设备解锁,立马进入应用执行该段代码,出来的结果应该是不会弹出验证界面,然后直接就返回回调结果了。
  2. 设备解锁,等待5秒过后,进入应用执行该段代码,这时候就应该会出现验证界面。

更加灵活的访问控制

上面我们说了evaluatePolicy方法是用来验证用户从而实现访问控制。在iOS 9后,苹果提供了一种更加灵活的访问控制方式,可以实现各种验证方式的组合,让你跳脱出内置policy的限制。那么接下来要说的就是evaluateAccessControl方法。先看一下方法原型:

- (void)evaluateAccessControl:(SecAccessControlRef)accessControl
                    operation:(LAAccessControlOperation)operation
              localizedReason:(NSString *)localizedReason
                        reply:(void(^)(BOOL success, NSError * __nullable error))reply;

相比evaluatePolicy方法来说去掉了policy参数,取而代之的是accessControloperation。其中accessControl的类型是SecAccessControlRef,可想而知其实与Keychain操作相关,我们先了解一下构造SecAccessControlRef实例的SecAccessControlCreateFlags枚举:

枚举值 说明
kSecAccessControlUserPresence 访问item需要通过锁屏密码或者Touch ID/Face ID进行验证,Touch ID/Face ID不设置时使用锁屏密码验证,当Touch ID/Face ID发生变更时也能够访问item。与LAPolicyDeviceOwnerAuthentication方式一致
kSecAccessControlBiometryAny 访问item需要通过Touch ID/Face ID验证,Touch ID/Face ID必须设置,当Touch ID/Face ID发生变更时也能够访问item。
kSecAccessControlTouchIDAny 已过时,使用kSecAccessControlBiometryAny代替
kSecAccessControlBiometryCurrentSet 访问item只能通过Touch ID/Face ID进行验证,当Touch ID/Face ID发生变更时item将被删除。
kSecAccessControlTouchIDCurrentSet 已过时,使用kSecAccessControlBiometryCurrentSet代替
kSecAccessControlDevicePasscode item通过锁屏密码验证访问。
kSecAccessControlOr 如果设置多个flag,只要有一个满足就可以访问item。
kSecAccessControlAnd 如果设置多个flag,必须所有的都满足才能访问item。
kSecAccessControlPrivateKeyUsage 私钥签名操作
kSecAccessControlApplicationPassword 使用应用设置的item密码验证,验证通过后才能访问。

通过上面的了解,我们可以知道只要组合这些条件就能够设计出适合自身应用的验证机制。我们再来看一下operation这个参数的取值:

枚举值 说明
LAAccessControlOperationCreateItem 访问控制用于创建新的item
LAAccessControlOperationUseItem 访问控制用于使用已存在的item
LAAccessControlOperationCreateKey 访问控制用于创建新的密钥
LAAccessControlOperationUseKeySign 访问控制用于使用已存在的密钥签名
LAAccessControlOperationUseKeyDecrypt 访问控制用于使用已存在的密钥解密
LAAccessControlOperationUseKeyKeyExchange 访问控制用于密钥交换

我们先不深究所有取值的使用场景和方法(笔者研究了好几天也没有研究透彻T_T),下面我列举几种验证方式的使用:

  1. LAPolicyDeviceOwnerAuthenticationWithBiometrics效果相同的方式,就是只使用Touch ID/Face ID验证:
CFErrorRef error;
SecAccessControlRef acl = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                                          kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
                                                          kSecAccessControlBiometryAny, &error);
LAContext *context = [[LAContext alloc] init];
[context evaluateAccessControl:acl 
                     operation:LAAccessControlOperationUserItem 
               localizedReason:@"开启指纹验证" 
                         reply:^(BOOL success, NSError * _Nullable error) {
}];
  1. LAPolicyDeviceOwnerAuthentication效果效果相同的方式,就是采用TouchID/FaceID和锁屏密码验证:
CFErrorRef error;
SecAccessControlRef acl = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                                          kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
                                                          kSecAccessControlUserPresence, &error);
LAContext *context = [[LAContext alloc] init];
[context evaluateAccessControl:acl 
                     operation:LAAccessControlOperationUserItem 
               localizedReason:@"开启指纹验证" 
                         reply:^(BOOL success, NSError * _Nullable error) {
}];
  1. 多种条件组合方式:
CFErrorRef error;
SecAccessControlRef acl = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                                          kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
                                                          kSecAccessControlTouchIDAny | kSecAccessControlOr | kSecAccessControlDevicePasscode, &error);
LAContext *context = [[LAContext alloc] init];
[context evaluateAccessControl:acl 
                     operation:LAAccessControlOperationUserItem 
               localizedReason:@"开启指纹验证" 
                         reply:^(BOOL success, NSError * _Nullable error) {
}];

或者

CFErrorRef error;
SecAccessControlRef acl = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                                          kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
                                                          kSecAccessControlTouchIDAny | kSecAccessControlAnd | kSecAccessControlDevicePasscode, &error);
LAContext *context = [[LAContext alloc] init];
[context evaluateAccessControl:acl 
                     operation:LAAccessControlOperationUserItem 
               localizedReason:@"开启指纹验证" 
                         reply:^(BOOL success, NSError * _Nullable error) {
}];

注意:不能同时使用kSecAccessControlAndkSecAccessControlOr

上面所说的就是这个方法的基本用法,不过这个方法在回调完成后并不会返回evaluatedPolicyDomainState,所以用它来代替evaluatePolicy方法看起来并不可行。但是从它使用SecAccessControlRef作为参数来看,能够理解苹果是想让这方法与keychain结合起来使用(找了很久都没有相关,资料只能靠想象力来做到最好了)。接下来我们把最佳实践的例子进行改写:

- (IBAction)switchChangedHandler:(id)sender
{
    if (self.touchIdSwitch.on)
    {
        //开启指纹
        CFErrorRef error;
        SecAccessControlRef acl = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
                                                                  kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
                                                                  kSecAccessControlTouchIDCurrentSet, &error);
        if (acl)
        {

            NSDictionary *saveDictionary = @{
                                             (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
                                             (__bridge id)kSecAttrService: @"passService",
                                             (__bridge id)kSecAttrAccount: @"vim",
                                             (__bridge id)kSecValueData: [@"123456" dataUsingEncoding:NSUTF8StringEncoding],
                                             (__bridge id)kSecAttrAccessControl: (__bridge id)acl
                                             };
     
            OSStatus status = SecItemAdd((CFDictionaryRef)saveDictionary, nil);
            if (status == noErr)
            {
                NSLog(@"开启指纹验证成功");
            }
            else
            {
                self.touchIdSwitch.on = NO;
                NSLog(@"开启指纹验证失败");
            }
        }
        else
        {
            self.touchIdSwitch.on = NO;
            NSLog(@"开启指纹验证时发生错误");
        }
    }
}
- (IBAction)validationButtonClickedHandler:(id)sender
{
    if (self.touchIdSwitch.on)
    {
        LAContext *validationContext = [[LAContext alloc] init];
        [validationContext evaluateAccessControl:acl operation:LAAccessControlOperationUseItem localizedReason:@"解锁指纹验证" reply:^(BOOL success, NSError * _Nullable error) {
                        
            if (success)
            {
                NSDictionary *loadDictionary = @{
                                                 (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
                                                 (__bridge id)kSecAttrService: @"passService",
                                                 (__bridge id)kSecAttrAccount: @"vim",
                                                 (__bridge id)kSecReturnData: @(YES),
                                                 (__bridge id)kSecMatchLimit: (NSString *)kSecMatchLimitOne,
                                                 (__bridge id)kSecUseAuthenticationContext:validationContext
                                                 };
                
                CFDataRef passwordData = NULL;
                OSStatus status = SecItemCopyMatching((CFDictionaryRef)loadDictionary, (CFTypeRef *)&passwordData);
                if (status == noErr)
                {
                    NSLog(@"验证通过");
                }
                else
                {
                    if (status == errSecItemNotFound)
                    {
                        NSLog(@"Touch ID/Face ID发生变化,请进行后续验证步骤");
                        [self doAppAuthentication^(BOOL success){

                            if (success)
                            {
                                //使用现有指纹状态进行保存
                                NSDictionary *saveDictionary = @{
                                                         (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
                                                         (__bridge id)kSecAttrService: @"passService",
                                                         (__bridge id)kSecAttrAccount: @"vim",
                                                         (__bridge id)kSecValueData: [@"123456" dataUsingEncoding:NSUTF8StringEncoding],
                                                         (__bridge id)kSecAttrAccessControl: (__bridge id)acl
                                                         };
                                SecItemAdd((CFDictionaryRef)saveDictionary, nil);
                                NSLog(@"验证通过");
                            }
                            else
                            {
                                NSLog(@"验证失败");
                            }

                        }];
                    }
                    else
                    {
                        NSLog(@"验证失败");
                    }
                }

                if (passwordData)
                {
                    CFRelease(passwordData);
                }
            }
                        
        }];
    }
}
- (void)doAppAuthentication:(void (^)(BOOL success))handler
{
    //执行应用自身验证体系,并将验证结果回调
    if (handler)
    {
        handler (YES);
    }
}

上面例子主要调整了两个地方:

  • evaluatePolicy方法改为了evaluateAccessControl方法
  • 之前保留evaluatedPolicyDomainState作为验证凭证,由于evaluateAccessControl不提供,所以使用了kSecAccessControlTouchIDCurrentSet这个Flag来代替之前的操作。这个值是当Touch ID/Face ID变更时,传入Keychain的数据就会被删除。依靠这样的特性,来判断是否发生变更,然后使用应用自身的验证体系。

对于evaluateAccessControl方法我的理解是它给LAContext填充一些必要的信息和访问权限,一旦授权成功这些信息就会写入LAContext中,然后可以通过kSecUseAuthenticationContext来绑定这个上下文对象进行Keychain的操作。从上面的例子我们就可以看到读取或者写入Keychain时都没有再使用kSecAttrAccessControl来指定访问权限了,这些权限都在LAContext中存储着。

其他属性方法说明

属性/方法 说明
invalidate 该方法能够使LAContext无效,如果正在验证过程中,则会直接回调方法返回错误码LAErrorAppCancel
isCredentialSet 该方法用于判断LAContext是否有设置授权凭证,type参数用于指定凭证类型
localizedFallbackTitle 该属性表示授权失败后显示第二个按钮的标题,如:context.localizedFallbackTitle = @"我要输入密码",效果如图所示:
localizedFallbackTitle设置效果
localizedFallbackTitle设置效果

localizedCancelTitle验证界面的取消按钮标题maxBiometryFailures最大验证失败次数,默认为nil,表示输入3次后就会回调失败,超过5次就锁定识别功能。该属性在iOS 9标识过期,测试下来貌似没有起到限制的作用interactionNotAllowed允许在非交互模式下进行身份认证。这个是iOS 11新增功能,可以用来解决后台运行时授权处理

关于LocalAuthentication这个框架的所有内容到这里算是告一段落了,从头文件看起来内容很少,但要完全理解确实不容易,文章就写到这里吧,感谢各位看官留到最后(我去歇歇,脑子有点发胀。。。。)

参考链接


LocalAuthentication开发实践

发布者

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注