在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ù)調用識別方法。否則需要根據(jù)error的描述來提示用戶。該方法的policy參數(shù)決定了鑒權的行為方式,該參數(shù)取值如下:
| 取值 | 說明 |
|---|---|
LAPolicyDeviceOwnerAuthenticationWithBiometrics |
指紋(人臉)識別。驗證彈框有兩個按鈕,第一個是取消按鈕,第二個按鈕可以自定義標題名稱(輸入密碼)。只有在第一次指紋驗證失敗后才會出現(xiàn)第二個按鈕,這種方式下的第二個按鈕功能需要自己定義。前三次指紋驗證失敗,指紋驗證框不再彈出。再次重新進入驗證,還有兩次驗證機會,如果還是驗證失敗,TOUCH ID 被鎖住不再繼續(xù)彈出指紋驗證框。以后的每次驗證都將會彈出設備密碼輸入框直至輸入正確的設備密碼才能重新使用指紋(人臉)識別 |
LAPolicyDeviceOwnerAuthentication |
指紋(人臉)識別或系統(tǒng)密碼驗證。如果Touch ID (Face ID)可用,且已經(jīng)錄入指紋(人臉),則優(yōu)先調用指紋(人臉)驗證。其次是調用系統(tǒng)密碼驗證,如果沒有開啟設備密碼,則不可以使用這種驗證方式。指紋(人臉)識別驗證失敗三次將彈出設備密碼輸入框,如果不進行密碼輸入。再次進來還可以有兩次機會驗證指紋(人臉),如果都失敗則Touch ID(Face ID)被鎖住,以后每次進來驗證都是調用系統(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被鎖定,需要進行設備密碼解鎖后重新激活 |
- 檢測可用后,調用
evaluatePolicy:localizedReason:reply:方法來進行指紋(人臉)識別。其中policy參數(shù)應該與調用canEvaluatePolicy: error :方法時傳入的policy一致。而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 |
應用調用了LAContext的invalidate方法 |
LAErrorNotInteractive |
應用尚未啟動完成或者已經(jīng)進入非激活狀態(tài)時調用驗證方法會收到該錯誤,例如:將驗證方法放到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)的過程,在一般的情況下我們都會給應用做一個開關,用于控制是否開啟指紋識別。那么,基于這個前提,我們可以作如下的流程處理:
- 設置一個開關(
UISwitch) - 當開關開啟時,要求用戶進行指紋識別,在識別成功后將
evaluatedPolicyDomainState保存起來,用于后續(xù)指紋驗證時對比。 - 在需要驗證的地方,使用指紋識別API進行驗證,同時獲取
evaluatedPolicyDomainState來比對之前保存的值,如果相同則驗證成功,否則驗證失敗,需要進行后續(xù)的處理,如需要輸入應用賬號的密碼。 - 通過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í)行應用自身驗證體系,并將驗證結果回調
if (handler)
{
handler (YES);
}
}
整個示例的界面如下所示:

代碼中的switchChangedHandler:方法為點擊界面中的開關按鈕的觸發(fā)事件??梢钥吹疆斢|發(fā)該事件時,而且開關處于打開狀態(tài),就要進行一次指紋驗證,只有當指紋識別通過時才視為真正啟用指紋識別功能,然后把evaluatedPolicyDomainState保存起來(為了方便演示,示例中只在內存中保留,原則上要將該值進行本地保存)。
然后當點擊示例中的“驗證指紋”按鈕時就會觸發(fā)validationButtonClickedHandler方法,這個時候會調起指紋驗證。在驗證回調中如果success為YES并且evaluatedPolicyDomainState與之前記錄的值相同時才算是驗證通過。如果僅僅是success為YES那么就要進行進一步的身份驗證,示例中就會調起doAppAuthentication方法。在該方法中進行App相關的驗證方法,例如讓用戶重新使用賬號密碼進行登錄等一系列鑒定用戶的操作。那么,為了方便演示,示例中直接認為App驗證用戶身份成功,在App驗證成功后要做的一步操作就是將原來保存的evaluatedPolicyDomainState替換成新的,后續(xù)就基于這個指紋狀態(tài)來校驗用戶身份了。(注:我們沒有辦法知道指紋的刪除和錄入是否是設備持有人的操作,所以基于安全的角度考慮,只要有修改就應該進行應用自身的驗證操作來確保一些惡意行為)。
上面所說的是一個相對完整的實踐過程,可能會存在一些理解有誤的地方,如果發(fā)現(xiàn)了希望同學們給予指出。講到這里,LocalAuthentication框架的內容并沒有全部結束,還有一些其他的內容,下面進行一一講解。
Face ID的驗證過程實踐
Face ID其實就是人臉識別中對人臉的唯一標識。蘋果目前在iPhone X設備中應用了這項技術,通過人臉識別來解鎖設備。同樣如果App內需要使用人臉識別來解鎖某些訪問,也是使用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解鎖后一段時間內自己的App也不需要重新彈出解鎖界面,就可以使用LAContext的touchIDAuthenticationAllowableReuseDuration屬性,該屬性表示從設備解鎖后多長時間內不需要重新驗證的時間(有些文章說支付寶解鎖后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(@"識別功能不可用");
}
上面的例子你可以通過兩種情況來測試:
- 設備解鎖,立馬進入應用執(zhí)行該段代碼,出來的結果應該是不會彈出驗證界面,然后直接就返回回調結果了。
- 設備解鎖,等待5秒過后,進入應用執(zhí)行該段代碼,這時候就應該會出現(xiàn)驗證界面。
更加靈活的訪問控制
上面我們說了evaluatePolicy方法是用來驗證用戶從而實現(xiàn)訪問控制。在iOS 9后,蘋果提供了一種更加靈活的訪問控制方式,可以實現(xiàn)各種驗證方式的組合,讓你跳脫出內置policy的限制。那么接下來要說的就是evaluateAccessControl方法。先看一下方法原型:
- (void)evaluateAccessControl:(SecAccessControlRef)accessControl
operation:(LAAccessControlOperation)operation
localizedReason:(NSString *)localizedReason
reply:(void(^)(BOOL success, NSError * __nullable error))reply;
相比evaluatePolicy方法來說去掉了policy參數(shù),取而代之的是accessControl和operation。其中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),下面我列舉幾種驗證方式的使用:
- 與
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) {
}];
- 與
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) {
}];
- 多種條件組合方式:
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) {
}];
注意:不能同時使用
kSecAccessControlAnd和kSecAccessControlOr
上面所說的就是這個方法的基本用法,不過這個方法在回調完成后并不會返回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í)行應用自身驗證體系,并將驗證結果回調
if (handler)
{
handler (YES);
}
}
上面例子主要調整了兩個地方:
- 將
evaluatePolicy方法改為了evaluateAccessControl方法 - 之前保留
evaluatedPolicyDomainState作為驗證憑證,由于evaluateAccessControl不提供,所以使用了kSecAccessControlTouchIDCurrentSet這個Flag來代替之前的操作。這個值是當Touch ID/Face ID變更時,傳入Keychain的數(shù)據(jù)就會被刪除。依靠這樣的特性,來判斷是否發(fā)生變更,然后使用應用自身的驗證體系。
對于evaluateAccessControl方法我的理解是它給LAContext填充一些必要的信息和訪問權限,一旦授權成功這些信息就會寫入LAContext中,然后可以通過kSecUseAuthenticationContext來綁定這個上下文對象進行Keychain的操作。從上面的例子我們就可以看到讀取或者寫入Keychain時都沒有再使用kSecAttrAccessControl來指定訪問權限了,這些權限都在LAContext中存儲著。
其他屬性方法說明
| 屬性/方法 | 說明 |
|---|---|
| invalidate | 該方法能夠使LAContext無效,如果正在驗證過程中,則會直接回調方法返回錯誤碼LAErrorAppCancel。 |
| isCredentialSet | 該方法用于判斷LAContext是否有設置授權憑證,type參數(shù)用于指定憑證類型 |
| localizedFallbackTitle | 該屬性表示授權失敗后顯示第二個按鈕的標題,如:context.localizedFallbackTitle = @"我要輸入密碼",效果如圖所示:![]() localizedFallbackTitle設置效果
|
| localizedCancelTitle | 驗證界面的取消按鈕標題 |
| maxBiometryFailures | 最大驗證失敗次數(shù),默認為nil,表示輸入3次后就會回調失敗,超過5次就鎖定識別功能。該屬性在iOS 9標識過期,測試下來貌似沒有起到限制的作用 |
| interactionNotAllowed | 允許在非交互模式下進行身份認證。這個是iOS 11新增功能,可以用來解決后臺運行時授權處理 |
關于LocalAuthentication這個框架的所有內容到這里算是告一段落了,從頭文件看起來內容很少,但要完全理解確實不容易,文章就寫到這里吧,感謝各位看官留到最后(我去歇歇,腦子有點發(fā)脹。。。。)
