LocalAuthentication開發(fā)實踐

在iPhone 5s加入Touch ID后俊性,指紋識別的功能在App中逐漸受到青睞,特別是對于本地安全較高的應用(如帶支付的App)指紋識別是必備的功能,它既能解決在驗證過程中輸入密碼的繁瑣過程柄慰,同時指紋識的安全等級更高。那么税娜,要想在自己開發(fā)的應用中使用指紋識別坐搔,就必須要LocalAuthentication.framework提供的API,下面將詳細地介紹如何使用這個框架來實現(xiàn)指紋識別功能敬矩。

基礎用法

我們先來看下面的例子:

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時則可以繼續(xù)調(diào)用識別方法禽炬。否則需要根據(jù)error的描述來提示用戶涧卵。該方法的policy參數(shù)決定了鑒權的行為方式,該參數(shù)取值如下:

取值 說明
LAPolicyDeviceOwnerAuthenticationWithBiometrics 指紋(人臉)識別腹尖。驗證彈框有兩個按鈕柳恐,第一個是取消按鈕,第二個按鈕可以自定義標題名稱(輸入密碼)。只有在第一次指紋驗證失敗后才會出現(xiàn)第二個按鈕乐设,這種方式下的第二個按鈕功能需要自己定義讼庇。前三次指紋驗證失敗,指紋驗證框不再彈出近尚。再次重新進入驗證蠕啄,還有兩次驗證機會,如果還是驗證失敗戈锻,TOUCH ID 被鎖住不再繼續(xù)彈出指紋驗證框歼跟。以后的每次驗證都將會彈出設備密碼輸入框直至輸入正確的設備密碼才能重新使用指紋(人臉)識別
LAPolicyDeviceOwnerAuthentication 指紋(人臉)識別或系統(tǒng)密碼驗證。如果Touch ID (Face ID)可用舶沛,且已經(jīng)錄入指紋(人臉),則優(yōu)先調(diào)用指紋(人臉)驗證窗价。其次是調(diào)用系統(tǒng)密碼驗證如庭,如果沒有開啟設備密碼,則不可以使用這種驗證方式撼港。指紋(人臉)識別驗證失敗三次將彈出設備密碼輸入框坪它,如果不進行密碼輸入。再次進來還可以有兩次機會驗證指紋(人臉)帝牡,如果都失敗則Touch ID(Face ID)被鎖住往毡,以后每次進來驗證都是調(diào)用系統(tǒng)的設備密碼直至輸入正確的設備密碼才能重新使用指紋(人臉)識別

該方法可以能返回的錯碼如下所示:

錯誤碼 說明
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被鎖定,需要進行設備密碼解鎖后重新激活
  • 檢測可用后烘绽,調(diào)用evaluatePolicy:localizedReason:reply:方法來進行指紋(人臉)識別淋昭。其中policy參數(shù)應該與調(diào)用canEvaluatePolicy: error :方法時傳入的policy一致。而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 系統(tǒng)強制取消,可能由于其他應用進入前臺
LAErrorAppCancel 應用調(diào)用了LAContextinvalidate方法
LAErrorNotInteractive 應用尚未啟動完成或者已經(jīng)進入非激活狀態(tài)時調(diào)用驗證方法會收到該錯誤昼榛,例如:將驗證方法放到didEnterBackground方法中進行可能會導致這個錯誤境肾。

通過上面的例子和解釋,大家對LocalAuthentication這個框架應該有了一定的了解吧胆屿,但是作為一種驗證方式奥喻,上面的做法是不夠安全和嚴謹?shù)摹Ee個例子非迹,如果我知道你的設備密碼环鲤,然后通過密碼登錄你的手機,然后我在你的設備上登記了我的指紋憎兽,那么按照上面代碼的邏輯冷离,我的指紋也是能夠驗證通過的。因此纯命,這里需要借助iOS 9上LAContext的一個新屬性evaluatedPolicyDomainState來解決這個問題(沒聽錯酒朵,是iOS 9上新增的,也就是說在iOS 8上會存在我說的這種問題扎附,可能蘋果爸爸一開始考慮蔫耽,既然手機設備密碼都泄漏了,那么自然手機里面的信息都是不安全的留夜,但是往往App中的驗證體系是獨立于系統(tǒng)的匙铡,某些重要的App密碼不泄漏還是很安全的,但是加入了指紋識別后可能就變得不安全了碍粥,所以不建議iOS 8上實現(xiàn)指紋識別功能)鳖眼。

evaluatedPolicyDomainState表示當下Touch ID/Face ID的一個狀態(tài),沒有其他的含義嚼摩。當在在設備上添加钦讳、刪除指紋(人臉)時矿瘦,這個值就會發(fā)生變化。所以愿卒,拿到這個值作比對就能夠很容易知道是否有發(fā)生變化缚去,下面將繼續(xù)介紹具體的實現(xiàn)方法。

最佳實踐(僅iOS 9及以上)

在寫代碼前琼开,先來理清楚整個實現(xiàn)的過程易结,在一般的情況下我們都會給應用做一個開關,用于控制是否開啟指紋識別柜候。那么搞动,基于這個前提,我們可以作如下的流程處理:

  1. 設置一個開關(UISwitch
  2. 當開關開啟時渣刷,要求用戶進行指紋識別鹦肿,在識別成功后將evaluatedPolicyDomainState保存起來,用于后續(xù)指紋驗證時對比辅柴。
  3. 在需要驗證的地方箩溃,使用指紋識別API進行驗證,同時獲取evaluatedPolicyDomainState來比對之前保存的值碌识,如果相同則驗證成功碾篡,否則驗證失敗虱而,需要進行后續(xù)的處理筏餐,如需要輸入應用賬號的密碼。
  4. 通過App自身驗證體系檢測通過后在把新的evaluatedPolicyDomainState保存起來牡拇,用于往后的驗證操作魁瞪。

理解上面的流程后,我們來看下面的示例代碼:

- (IBAction)switchChangedHandler:(id)sender
{
    if (self.touchIdSwitch.on)
    {
        //開啟指紋
        LAContext *context = [[LAContext alloc] init];
        if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:nil])
        {
            //進行第一次的驗證惠呼,成功后記錄驗證狀態(tài)
            [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])
        {
            //進行第一次的驗證导俘,成功后記錄驗證狀態(tài)
            [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:@"使用指紋識別驗證" reply:^(BOOL success, NSError * _Nullable error) {
                
                if (success)
                {
                    if ([context.evaluatedPolicyDomainState isEqualToData:self.policyDomainState])
                    {
                        NSLog(@"驗證通過!");
                    }
                    else
                    {
                        NSLog(@"指紋發(fā)生變化,請進行后續(xù)驗證步驟");
                        //這里可以彈出App的登錄界面讓用戶重新登錄
                        //等待登錄完成后再將evaluatedPolicyDomainState保存起用于后續(xù)的操作
                        [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
{
    //執(zhí)行應用自身驗證體系剔蹋,并將驗證結果回調(diào)
    if (handler)
    {
        handler (YES);
    }
}

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

示例運行效果

代碼中的switchChangedHandler:方法為點擊界面中的開關按鈕的觸發(fā)事件旅薄。可以看到當觸發(fā)該事件時泣崩,而且開關處于打開狀態(tài)少梁,就要進行一次指紋驗證,只有當指紋識別通過時才視為真正啟用指紋識別功能矫付,然后把evaluatedPolicyDomainState保存起來(為了方便演示凯沪,示例中只在內(nèi)存中保留,原則上要將該值進行本地保存)买优。

然后當點擊示例中的“驗證指紋”按鈕時就會觸發(fā)validationButtonClickedHandler方法妨马,這個時候會調(diào)起指紋驗證挺举。在驗證回調(diào)中如果successYES并且evaluatedPolicyDomainState與之前記錄的值相同時才算是驗證通過。如果僅僅是successYES那么就要進行進一步的身份驗證烘跺,示例中就會調(diào)起doAppAuthentication方法湘纵。在該方法中進行App相關的驗證方法,例如讓用戶重新使用賬號密碼進行登錄等一系列鑒定用戶的操作液荸。那么瞻佛,為了方便演示,示例中直接認為App驗證用戶身份成功娇钱,在App驗證成功后要做的一步操作就是將原來保存的evaluatedPolicyDomainState替換成新的伤柄,后續(xù)就基于這個指紋狀態(tài)來校驗用戶身份了。(注:我們沒有辦法知道指紋的刪除和錄入是否是設備持有人的操作文搂,所以基于安全的角度考慮适刀,只要有修改就應該進行應用自身的驗證操作來確保一些惡意行為)。

上面所說的是一個相對完整的實踐過程煤蹭,可能會存在一些理解有誤的地方笔喉,如果發(fā)現(xiàn)了希望同學們給予指出。講到這里硝皂,LocalAuthentication框架的內(nèi)容并沒有全部結束常挚,還有一些其他的內(nèi)容,下面進行一一講解稽物。

Face ID的驗證過程實踐

Face ID其實就是人臉識別中對人臉的唯一標識奄毡。蘋果目前在iPhone X設備中應用了這項技術,通過人臉識別來解鎖設備贝或。同樣如果App內(nèi)需要使用人臉識別來解鎖某些訪問吼过,也是使用LAContext來實現(xiàn),而且實現(xiàn)流程跟Touch ID一樣咪奖,幾乎不需要改寫任何代碼盗忱。唯一需要注意的地方是,iOS 11后LAContext新增一個只讀屬性biometryType羊赵,該屬性表示當前設備支持生物識別類型(是Touch ID還是Face ID)趟佃,其枚舉值說明如下:

枚舉值 說明
LABiometryTypeNone 表示設備不支持生物識別技術
LABiometryNone 在iOS 11中已經(jīng)過時,使用LABiometryTypeNone代替
LABiometryTypeTouchID 表示當前設備支持指紋識別
LABiometryTypeFaceID 表示當前設備支持人臉識別

那么昧捷,這個值最重要的作用就是讓你可以區(qū)分到底現(xiàn)在使用的是Touch ID還是Face ID闲昭,然后編碼時可以根據(jù)類型來作不同的提示和判斷。例如:

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的數(shù)據(jù)汤纸,在設備解鎖后就能夠對keychain中的數(shù)據(jù)進行訪問,其實這樣是不夠安全的芹血,特別是在你的App使用第三方SDK的情況底下贮泞,很有可能就會去竊取App中的keychain數(shù)據(jù)楞慈。那么,在iOS 9之后啃擦,系統(tǒng)加入了一項新的功能囊蓝,就是允許應用來控制Keychain的數(shù)據(jù)訪問。它的實現(xiàn)過程是在寫入數(shù)據(jù)時添加一個應用級別的訪問密碼令蛉,后續(xù)要訪問這個數(shù)據(jù)除了要設備解鎖聚霜,還需要有正確的密碼才能夠訪問Keychain中的數(shù)據(jù)。而這功能正好是集成到了LocalAuthentication這個框架中珠叔,下面我們來探索一下這個功能的用法蝎宇。

要實現(xiàn)Keychain的控制訪問,依然還是要依靠LAContext來實現(xiàn)祷安,我們可以看到在iOS 9之后姥芥,這個類型新增了一個方法setCredential:type:。這個方法的作用就是把訪問Keychain的密碼設置到LAContext對象中汇鞭,配合LACredentialTypeApplicationPassword這個類型就能夠實現(xiàn)控制訪問了凉唐,下面先來看一下實現(xiàn)的示例代碼:

OSStatus status = noErr;
CFErrorRef error = NULL;

SecAccessControlRef acl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleAfterFirstUnlock, kSecAccessControlApplicationPassword, &error);

if (acl)
{
    NSString *data = @"要寫入的數(shù)據(jù)";
    
    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信息的代碼可以看到,在創(chuàng)建訪問控制權限時使用的是kSecAccessControlApplicationPassword枚舉值霍骄,這是iOS 9新增的選項台囱,表示你的Keychain數(shù)據(jù)訪問要使用應用密碼。同時也可以看到保存的Keychain數(shù)據(jù)結構中多了一項kSecUseAuthenticationContext读整,這也是iOS 9新增的簿训,它對應的值就是setCredential后的LAContext對象。

通過這樣的操作绘沉,Keychain數(shù)據(jù)就能夠實現(xiàn)應用密碼訪問的功能了煎楣。下面我們再來看看如何讀取這樣類型的數(shù)據(jù):

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來指定已經(jīng)設置過密碼的LAContext對象喻喳。如果密碼正確就能夠訪問到數(shù)據(jù)另玖,否則無法取到對應的數(shù)據(jù)。

那么表伦,這個功能的使用場景應該是怎么樣的呢谦去?其實我個人覺得還是挺多地方會用到的,例如:你跟服務器進行用戶驗證后通常都會下發(fā)一個令牌(token)蹦哼,而這個令牌就是你能操作應用的憑證鳄哭,如果存在普通的文件或者數(shù)據(jù)庫中那其實有可能會被利用。那么纲熏,就可以利用這樣的功能讓服務器提供一個隨機的密碼妆丘,然后將令牌保存到keychain中锄俄。又例如像一些本地的密碼工具軟件,用于加密數(shù)據(jù)的密鑰不能直接保存到文件中勺拣,也可以利用這個功能奶赠,將用戶解鎖密碼以及密鑰保存到Keychain,只有用戶成功解鎖應用药有,才能夠讀取里面的信息毅戈。

復用設備解鎖授權

如果你的應用想要在設備使用Touch ID/Face ID解鎖后一段時間內(nèi)自己的App也不需要重新彈出解鎖界面,就可以使用LAContexttouchIDAuthenticationAllowableReuseDuration屬性愤惰,該屬性表示從設備解鎖后多長時間內(nèi)不需要重新驗證的時間(有些文章說支付寶解鎖后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. 設備解鎖,立馬進入應用執(zhí)行該段代碼凉倚,出來的結果應該是不會彈出驗證界面兼都,然后直接就返回回調(diào)結果了。
  2. 設備解鎖稽寒,等待5秒過后扮碧,進入應用執(zhí)行該段代碼,這時候就應該會出現(xiàn)驗證界面杏糙。

更加靈活的訪問控制

上面我們說了evaluatePolicy方法是用來驗證用戶從而實現(xiàn)訪問控制慎王。在iOS 9后,蘋果提供了一種更加靈活的訪問控制方式宏侍,可以實現(xiàn)各種驗證方式的組合赖淤,讓你跳脫出內(nèi)置policy的限制。那么接下來要說的就是evaluateAccessControl方法谅河。先看一下方法原型:

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

相比evaluatePolicy方法來說去掉了policy參數(shù)咱旱,取而代之的是accessControloperation。其中accessControl的類型是SecAccessControlRef绷耍,可想而知其實與Keychain操作相關吐限,我們先了解一下構造SecAccessControlRef實例的SecAccessControlCreateFlags枚舉:

枚舉值 說明
kSecAccessControlUserPresence 訪問item需要通過鎖屏密碼或者Touch ID/Face ID進行驗證,Touch ID/Face ID不設置時使用鎖屏密碼驗證褂始,當Touch ID/Face ID發(fā)生變更時也能夠訪問item诸典。與LAPolicyDeviceOwnerAuthentication方式一致
kSecAccessControlBiometryAny 訪問item需要通過Touch ID/Face ID驗證,Touch ID/Face ID必須設置崎苗,當Touch ID/Face ID發(fā)生變更時也能夠訪問item。
kSecAccessControlTouchIDAny 已過時,使用kSecAccessControlBiometryAny代替
kSecAccessControlBiometryCurrentSet 訪問item只能通過Touch ID/Face ID進行驗證垮兑,當Touch ID/Face ID發(fā)生變更時item將被刪除捐韩。
kSecAccessControlTouchIDCurrentSet 已過時,使用kSecAccessControlBiometryCurrentSet代替
kSecAccessControlDevicePasscode item通過鎖屏密碼驗證訪問。
kSecAccessControlOr 如果設置多個flag,只要有一個滿足就可以訪問item。
kSecAccessControlAnd 如果設置多個flag轰豆,必須所有的都滿足才能訪問item。
kSecAccessControlPrivateKeyUsage 私鑰簽名操作
kSecAccessControlApplicationPassword 使用應用設置的item密碼驗證齿诞,驗證通過后才能訪問酸休。

通過上面的了解,我們可以知道只要組合這些條件就能夠設計出適合自身應用的驗證機制祷杈。我們再來看一下operation這個參數(shù)的取值:

枚舉值 說明
LAAccessControlOperationCreateItem 訪問控制用于創(chuàng)建新的item
LAAccessControlOperationUseItem 訪問控制用于使用已存在的item
LAAccessControlOperationCreateKey 訪問控制用于創(chuàng)建新的密鑰
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

上面所說的就是這個方法的基本用法宿刮,不過這個方法在回調(diào)完成后并不會返回evaluatedPolicyDomainState,所以用它來代替evaluatePolicy方法看起來并不可行私蕾。但是從它使用SecAccessControlRef作為參數(shù)來看僵缺,能夠理解蘋果是想讓這方法與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(@"開啟指紋驗證時發(fā)生錯誤");
        }
    }
}

- (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發(fā)生變化磕潮,請進行后續(xù)驗證步驟");
                        [self doAppAuthentication^(BOOL success){

                            if (success)
                            {
                                //使用現(xiàn)有指紋狀態(tài)進行保存
                                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
{
    //執(zhí)行應用自身驗證體系,并將驗證結果回調(diào)
    if (handler)
    {
        handler (YES);
    }
}

上面例子主要調(diào)整了兩個地方:

  • evaluatePolicy方法改為了evaluateAccessControl方法
  • 之前保留evaluatedPolicyDomainState作為驗證憑證容贝,由于evaluateAccessControl不提供自脯,所以使用了kSecAccessControlTouchIDCurrentSet這個Flag來代替之前的操作。這個值是當Touch ID/Face ID變更時斤富,傳入Keychain的數(shù)據(jù)就會被刪除膏潮。依靠這樣的特性,來判斷是否發(fā)生變更茂缚,然后使用應用自身的驗證體系戏罢。

對于evaluateAccessControl方法我的理解是它給LAContext填充一些必要的信息和訪問權限屋谭,一旦授權成功這些信息就會寫入LAContext中脚囊,然后可以通過kSecUseAuthenticationContext來綁定這個上下文對象進行Keychain的操作。從上面的例子我們就可以看到讀取或者寫入Keychain時都沒有再使用kSecAttrAccessControl來指定訪問權限了桐磁,這些權限都在LAContext中存儲著悔耘。

其他屬性方法說明

屬性/方法 說明
invalidate 該方法能夠使LAContext無效,如果正在驗證過程中我擂,則會直接回調(diào)方法返回錯誤碼LAErrorAppCancel衬以。
isCredentialSet 該方法用于判斷LAContext是否有設置授權憑證缓艳,type參數(shù)用于指定憑證類型
localizedFallbackTitle 該屬性表示授權失敗后顯示第二個按鈕的標題,如:context.localizedFallbackTitle = @"我要輸入密碼"看峻,效果如圖所示:
localizedFallbackTitle設置效果
localizedCancelTitle 驗證界面的取消按鈕標題
maxBiometryFailures 最大驗證失敗次數(shù)阶淘,默認為nil,表示輸入3次后就會回調(diào)失敗互妓,超過5次就鎖定識別功能溪窒。該屬性在iOS 9標識過期,測試下來貌似沒有起到限制的作用
interactionNotAllowed 允許在非交互模式下進行身份認證冯勉。這個是iOS 11新增功能澈蚌,可以用來解決后臺運行時授權處理

關于LocalAuthentication這個框架的所有內(nèi)容到這里算是告一段落了,從頭文件看起來內(nèi)容很少灼狰,但要完全理解確實不容易宛瞄,文章就寫到這里吧,感謝各位看官留到最后(我去歇歇交胚,腦子有點發(fā)脹份汗。。蝴簇。裸影。)

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市军熏,隨后出現(xiàn)的幾起案子轩猩,更是在濱河造成了極大的恐慌,老刑警劉巖荡澎,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件均践,死亡現(xiàn)場離奇詭異,居然都是意外死亡摩幔,警方通過查閱死者的電腦和手機彤委,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來或衡,“玉大人焦影,你說我怎么就攤上這事》舛希” “怎么了斯辰?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長坡疼。 經(jīng)常有香客問我彬呻,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任闸氮,我火速辦了婚禮剪况,結果婚禮上,老公的妹妹穿的比我還像新娘蒲跨。我一直安慰自己译断,他們只是感情好,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布或悲。 她就那樣靜靜地躺著镐作,像睡著了一般。 火紅的嫁衣襯著肌膚如雪隆箩。 梳的紋絲不亂的頭發(fā)上该贾,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天,我揣著相機與錄音捌臊,去河邊找鬼杨蛋。 笑死,一個胖子當著我的面吹牛理澎,可吹牛的內(nèi)容都是我干的逞力。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼糠爬,長吁一口氣:“原來是場噩夢啊……” “哼寇荧!你這毒婦竟也來了?” 一聲冷哼從身側響起执隧,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤揩抡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后镀琉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體峦嗤,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年屋摔,在試婚紗的時候發(fā)現(xiàn)自己被綠了烁设。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡钓试,死狀恐怖装黑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情弓熏,我是刑警寧澤恋谭,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站硝烂,受9級特大地震影響箕别,放射性物質發(fā)生泄漏铜幽。R本人自食惡果不足惜滞谢,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一串稀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧狮杨,春花似錦母截、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至护蝶,卻和暖如春华烟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背持灰。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工盔夜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人堤魁。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓喂链,卻偏偏與公主長得像,于是被迫代替她去往敵國和親妥泉。 傳聞我的和親對象是個殘疾皇子椭微,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,985評論 3 119
  • 本文首發(fā)個人博客:聊聊 iOS 數(shù)據(jù)保護 文件系統(tǒng)中的文件盲链、keychain中的項蝇率,都是加密存儲的。當用戶解鎖設備...
    Chars閱讀 3,062評論 0 5
  • 14—4—31 摘要:每個人心中都要有愛刽沾,但如何讓孩子感受到愛瓢剿,接受我們的教育,達到我們教書育人的目標悠轩,則需要一些...
    一笑而過_0e6f閱讀 122評論 0 2
  • 荷風才動影婆娑间狂, 晴好已非三兩天。 身在秋冬慕春夏火架, 茍且又摘一枝蓮鉴象。 2018.03.27.巳時 附王川詩: ...
    無影樹閱讀 225評論 0 2
  • 夜里,口渴何鸡,我起來喝水纺弊,發(fā)現(xiàn)兒子的房間還亮著燈光。我看了下墻上的掛鐘骡男,都夜里一點多了淆游,小家伙還在用功。 是呀,初三...
    白衣秀士閱讀 768評論 0 1