寫在前面
指紋識別大家都不陌生,現在比較新的Android/ target=_blank class=infotextkey>安卓手機大多都已經支持面部識別了,指紋識別更是主流安卓手機的標配功能。這兩個功能可以說用過都說好,確實是方便快捷。
不過大家觀察一下會發現,這些手機的指紋識別和面部識別也就是支持一下手機的鎖屏解鎖而已,數量巨大的App對于這兩個技術的應用可以說比較少。這何嘗不是APP良好體驗性的損失呢?
慶幸的是,基于 google API 實現指紋識別的基礎性功能并不復雜。
指紋識別的兼容性和安全性問題
除了實現指紋識別的基礎性功能,我認為開發者還需要關注和選擇性處理的問題有兩個:兼容性和安全性。
為什么說選擇性處理?
首先說兼容性,指紋識別的 API 是 Google 在 Android 6.0 開放出來的。
在 Android 6.0 以下的系統上,某些手機廠商自行支持了指紋識別,如果我們的 APP 要兼容這些設備,就還要集成廠商的指紋識別的SDK,這是最大的兼容性問題。不過,現在 Android 6.0 以下的設備已經很少了,其中支持指紋識別的設備就更少了,不對其進行兼容,我認為也是可以的。
在Android 6.0 以上的系統上,由于廠商對 Android 系統和指紋識別模塊的定制化普遍,導致會出現一些兼容性問題。這個沒有什么好的辦法,就需要開發者見招拆招了。已經踩過坑的開發者很多,大家可以到網上搜索相關的文章看。
然后說下安全性,由于已添加的指紋是存儲在手機上的,Google API 驗證指紋后僅僅返回 true 或者 false,我們是很難無條件相信這個識別結果的。比如說用戶的手機 root 了或者是自定制設備,指紋識別是有可能被劫持進而返回有誤的識別結果的。
好在這種情況發生的概率比較低。如果指紋識別的應用場景非交易非支付,僅僅是類似于 “啟動 APP 進行指紋驗證” 這樣的情況的話,Google API 提供的指紋識別就夠用了。
關于兼容性和安全性的問題,本文不過多探討了,給大家推薦一篇文章,同時也請大家關注文章中提到的支付寶和騰訊的處理方式,及其開源情況:
指紋識別 API 的版本演進
在 Android 6.0(Android M Api23),Android 系統開放了指紋識別的api,存在于 android.hardware.fingerprint包下,核心類是FingerprintManager,提供了基礎的指紋識別的功能。要注意的是,FingerprintManager在 Android 9.0(Android P Api28)做了 @Deprecated 標記,將被棄用。
后來,在android.support.v4.hardware.fingerprint包和 androidx.core.hardware.fingerprint包中,FingerprintManager升級為了 FingerprintManagerCompat,對功能進行了增強,也做了一些兼容性的處理,比如增加了系統版本號的判斷,對指紋支持加密處理等。實際上閱讀源碼會發現,他的核心功能還是調用 FingerprintManager 實現的。
再之后,在 Android 9.0(Android P Api 28),Google 對生物識別進行了進一步增強,開放了以 BiometricPrompt 為核心的新 Api,存在于 androidx.biometric 包和android.hardware.biometrics包下,Google 在開發者文檔中是這樣解釋的:
On devices running P and above, this will show a system-provided authentication prompt, using a device's supported biometric (fingerprint, iris, face, etc).
大意是,在 Android P 及以上版本的系統中,BiometricPrompt 將展現一個由系統提供的驗證提示,用于支持設備提供的生物識別,包括指紋、虹膜、面部等。
目前來看,虹膜和面部等生物識別 Api 尚未開放,僅支持指紋識別,不過在指紋識別上進行了統一,比如要求使用統一的指紋識別 UI ,不允許開發者自定義了。
指紋識別關鍵方法 authenticate
這是指紋識別中最核心的方法,用于拉起指紋識別掃描器進行指紋識別。
以 FingerprintManagerCompat 中 authenticate() 方法為例,開發者文檔中是這樣定義的:
解釋一下各個參數:
- FingerprintManagerCompat.CryptoObject crypto
源碼中是這樣解釋的:
/** * A wrapper class for the crypto objects supported by FingerprintManager. Currently the * framework supports {@link Signature} and {@link Cipher} objects. */
大意是,這是一個密碼對象的包裝類,當前支持 Signature 形式和 Cipher 形式的密碼對象加密。
作用是,指紋掃描器會使用這個對象判斷指紋認證結果的合法性。Android 6.0 是 @Nullable,但不建議傳 null,且在 Android 9.0 之后就是@NonNull 了。
我的代碼中提供了一個 Cipher 的幫助類,可用來創建一個 Cipher 對象,可參考使用。
- int flags
可選標志,暫無用處,傳 0 即可。只用于 Android 6.0。
- CancellationSignal cancel
這個對象的作用是用來取消指紋掃描器的掃描操作。比如在用戶點擊識別框上的“取消”按鈕或者“密碼驗證”按鈕后,就要及時取消掃描器的掃描操作。
不及時取消的話,指紋掃描器就會一直掃描,直至超時。這會造成兩個問題:
(1) 耗電
(2) 在超時時間內,用戶將無法再次調起指紋識別。
同樣,這個參數在 Android 6.0 是 @Nullable,在 Android 9.0 之后是 @NonNull ,由于上述的原因,不建議傳 null 。
- FingerprintManagerCompat.AuthenticationCallback callback
指紋識別結果的回調接口,是 @NonNull 的,其中聲明了如下幾個方法:
重點關注一下 onAuthenticationError() 和 onAuthenticationHelp() 兩個方法,當出現指紋識別異常時會被回調:
(1) 參數 errString 和 helpString 是具體的異常信息,例如“手指移動過快”、“驗證失敗”等。這個異常信息是由系統提供的,目前看到的情況是系統會根據應用使用的語言匹配對應的異常信息。
應用有做國際化的小伙伴,對系統匹配對應語言的異常信息不放心的話,可根據 errMsgId 和 helpMsgId 自行處理。
(2) 參數 errMsgId 和 helpMsgId 是某個異常的 Id 標識,有很多,這里不贅述,各位請自行查看開發者文檔。注意注意的是,在 Android 6.0 上和 Android 9.0 上有差別。
- Handler handler
這個參數用于 Android 6.0,是 @Nullable 的,作用是告訴系統使用這個 Handler 的 Looper 處理指紋識別的 Message。默認就是交給主線程的 Looper 處理,傳 null 即可。
- Executor executor (補充,是)
這個參數是 Android 9.0 Api BiometricPrompt.authenticate() 中的參數,是 @NonNull的,作用與上個參數 Handler handler 類似,用來分發指紋識別的回調事件。
當通過主線程進行分發時,可通過 Context#getMainExecutor() 傳參;
當通過共享線程池進行分發時,可通過 AsyncTask#THREAD_POOL_EXECUTOR 傳參。
指紋識別的實踐
在指紋識別功能的實踐中,我將其做成了開源庫發布在了 Github 上,可通過 gradle 進行依賴,使用方法比較簡單,兩三行代碼,再傳入一個驗證結果監聽即可。
下面對部分實現過程做一下介紹,詳細的 Api 及源碼請移步 Github。
Github 地址:https://github.com/ZuoHailong/BiometricPrompt
示例
Android 6.0 指紋識別框,開發者自定義:
Android 9.0 指紋識別框,系統提供:
其中指紋 icon 和取消按鈕的文字顏色,由屬性 colorPrimary 的顏色值決定。
指紋識別管理類
FingerprintVerifyManager 是指紋識別庫的入口,對指紋識別進行管理,通過 FingerprintVerifyManager.Builder 對指紋識別進行初始化。
在這個類中,有根據手機系統版本調用不同的指紋識別 Api(FingerprintManagerCompat或者 BiometricPrompt),其中 BiometricPrompt 支持開啟或者關閉,默認關閉。
public FingerprintVerifyManager(Builder builder) { IFingerprint fingerprint; // >= Android P if (AndrVersionUtil.isAboveAndrP()) { //在 Android P 上是否展示系統提供的識別框 if (builder.enableAndroidP) fingerprint = FingerprintAndrP.newInstance(); else fingerprint = FingerprintAndrM.newInstance(); } else if (AndrVersionUtil.isAboveAndrM()) {// Android 6.0 =< Version fingerprint = FingerprintAndrM.newInstance(); } else {// < Android 6.0 ,官方未開放指紋識別,某些機型自行支持的情況暫不做處理 builder.callback.onError(builder.context.getString(R.string.biometricprompt_verify_error_below_m)); return; } …… fingerprint.authenticate(builder.context, bean, builder.callback); }
其中 IFingerprint 是指紋識別的接口,兼容 Android 6.0 的 FingerprintAndrM 和兼容 Android 9.0 的 FingerprintAndrP 都實現了此接口。
public interface IFingerprint { /** * 初始化并調起指紋識別 * * @param context * @param verificationDialogStyleBean 指紋識別框樣式 * @param callback 通知開發者指紋識別結果 */ void authenticate(Activity context, VerificationDialogStyleBean verificationDialogStyleBean, FingerprintCallback callback); }
基于Android 6.0 實現指紋識別
上文有提及,FingerprintAndrM 是基于 Android 6.0 的具體的指紋識別實現類:
@RequiresApi(api = Build.VERSION_CODES.M) public class FingerprintAndrM implements IFingerprint { private final String TAG = FingerprintAndrM.class.getName(); private Activity context; private static FingerprintAndrM fingerprintAndrM; //指紋驗證框 private static FingerprintDialog fingerprintDialog; //指向調用者的指紋回調 private FingerprintCallback fingerprintCallback; //用于取消掃描器的掃描動作 private CancellationSignal cancellationSignal; //指紋加密 private static FingerprintManagerCompat.CryptoObject cryptoObject; //Android 6.0 指紋管理 private FingerprintManagerCompat fingerprintManagerCompat; @Override public void authenticate(Activity context, VerificationDialogStyleBean bean, FingerprintCallback callback) { //判斷指紋識別是否可用 if (!canAuthenticate(context, callback)) return; this.context = context; this.fingerprintCallback = callback; //Android 6.0 指紋管理 實例化 fingerprintManagerCompat = FingerprintManagerCompat.from(context); //取消掃描,每次取消后需要重新創建新示例 cancellationSignal = new CancellationSignal(); cancellationSignal.setOnCancelListener(() -> fingerprintDialog.dismiss()); //調起指紋驗證 fingerprintManagerCompat.authenticate(cryptoObject, 0, cancellationSignal, authenticationCallback, null); //指紋驗證框 fingerprintDialog = FingerprintDialog.newInstance(context).setActionListener(dialogActionListener).setDialogStyle(bean); fingerprintDialog.show(context.getFragmentManager(), TAG); } public static FingerprintAndrM newInstance() { if (fingerprintAndrM == null) { synchronized (FingerprintAndrM.class) { if (fingerprintAndrM == null) { fingerprintAndrM = new FingerprintAndrM(); } } } //指紋加密,提前進行Cipher初始化,防止指紋認證時還沒有初始化完成 try { cryptoObject = new FingerprintManagerCompat.CryptoObject(new CipherHelper().createCipher()); } catch (Exception e) { e.printStackTrace(); } return fingerprintAndrM; } /** * 指紋驗證框按鍵監聽 */ private FingerprintDialog.OnDialogActionListener dialogActionListener = new FingerprintDialog.OnDialogActionListener() { @Override public void onUsepwd() { if (fingerprintCallback != null) fingerprintCallback.onUsepwd(); } @Override public void onCancle() {//取消指紋驗證,通知調用者 if (fingerprintCallback != null) fingerprintCallback.onCancel(); } @Override public void onDismiss() {//驗證框消失,取消指紋驗證 if (cancellationSignal != null && !cancellationSignal.isCanceled()) cancellationSignal.cancel(); } }; /** * 指紋驗證結果回調 */ private FingerprintManagerCompat.AuthenticationCallback authenticationCallback = new FingerprintManagerCompat.AuthenticationCallback() { @Override public void onAuthenticationError(int errMsgId, CharSequence errString) { super.onAuthenticationError(errMsgId, errString); fingerprintDialog.setTip(errString.toString(), R.color.biometricprompt_color_FF5555); } @Override public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { super.onAuthenticationHelp(helpMsgId, helpString); fingerprintDialog.setTip(helpString.toString(), R.color.biometricprompt_color_FF5555); } @Override public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) { super.onAuthenticationSucceeded(result); fingerprintDialog.setTip(context.getString(R.string.biometricprompt_verify_success), R.color.biometricprompt_color_82C785); fingerprintCallback.onSucceeded(); fingerprintDialog.dismiss(); } @Override public void onAuthenticationFailed() { super.onAuthenticationFailed(); fingerprintDialog.setTip(context.getString(R.string.biometricprompt_verify_failed), R.color.biometricprompt_color_FF5555); fingerprintCallback.onFailed(); } }; /* * 在 Android Q,Google 提供了 Api BiometricManager.canAuthenticate() 用來檢測指紋識別硬件是否可用及是否添加指紋 * 不過尚未開放,標記為"Stub"(存根) * 所以暫時還是需要使用 Andorid 6.0 的 Api 進行判斷 * */ private boolean canAuthenticate(Context context, FingerprintCallback fingerprintCallback) { /* * 硬件是否支持指紋識別 * */ if (!FingerprintManagerCompat.from(context).isHardwareDetected()) { fingerprintCallback.onError(FingerprintManager.FINGERPRINT_ERROR_HW_NOT_PRESENT, context.getString(R.string.biometricprompt_verify_error_no_hardware)); return false; } //是否已添加指紋 if (!FingerprintManagerCompat.from(context).hasEnrolledFingerprints()) { fingerprintCallback.onNoneEnrolled(); return false; } return true; } }
這里面要重點關注 CancellationSignal 與指紋識別框的關聯,也就是識別框消失,就一定要取消指紋掃描器的掃描操作,否則在超時時間內,用戶將無法再次拉起指紋識別(盡管可以彈出指紋識別框)。
基于Android 9.0 實現指紋識別
上文有提及,FingerprintAndrP 是基于 Android 9.0 的具體的指紋識別實現類:
@RequiresApi(api = Build.VERSION_CODES.P) public class FingerprintAndrP implements IFingerprint { private static FingerprintAndrP fingerprintAndrP; //指向調用者的指紋回調 private FingerprintCallback fingerprintCallback; //用于取消掃描器的掃描動作 private CancellationSignal cancellationSignal; //指紋加密 private static BiometricPrompt.CryptoObject cryptoObject; @Override public void authenticate(Activity context, VerificationDialogStyleBean verificationDialogStyleBean, FingerprintCallback callback) { //判斷指紋識別是否可用 if (!canAuthenticate(context, callback)) return; this.fingerprintCallback = callback; /* * 初始化 BiometricPrompt.Builder */ …… //構建 BiometricPrompt BiometricPrompt biometricPrompt = builder.build(); //取消掃描,每次取消后需要重新創建新示例 cancellationSignal = new CancellationSignal(); cancellationSignal.setOnCancelListener(() -> { }); /* * 拉起指紋驗證模塊,等待驗證 * Executor: * context.getMainExecutor() */ biometricPrompt.authenticate(cryptoObject, cancellationSignal, context.getMainExecutor(), authenticationCallback); } public static FingerprintAndrP newInstance() { if (fingerprintAndrP == null) { synchronized (FingerprintAndrM.class) { if (fingerprintAndrP == null) { fingerprintAndrP = new FingerprintAndrP(); } } } //指紋加密,提前進行Cipher初始化,防止指紋認證時還沒有初始化完成 try { cryptoObject = new BiometricPrompt.CryptoObject(new CipherHelper().createCipher()); } catch (Exception e) { e.printStackTrace(); } return fingerprintAndrP; } /** * 認證結果回調 */ private BiometricPrompt.AuthenticationCallback authenticationCallback = new BiometricPrompt.AuthenticationCallback() { @Override public void onAuthenticationError(int errorCode, CharSequence errString) { super.onAuthenticationError(errorCode, errString); if (fingerprintCallback != null) { if (errorCode == 5) {//用戶取消指紋驗證,不必向用戶拋提示信息 fingerprintCallback.onCancel(); return; } fingerprintCallback.onError(errorCode, errString.toString()); } } @Override public void onAuthenticationHelp(int helpCode, CharSequence helpString) { super.onAuthenticationHelp(helpCode, helpString); if (fingerprintCallback != null) fingerprintCallback.onError(helpCode, helpString.toString()); } @Override public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { super.onAuthenticationSucceeded(result); if (fingerprintCallback != null) fingerprintCallback.onSucceeded(); } @Override public void onAuthenticationFailed() { super.onAuthenticationFailed(); if (fingerprintCallback != null) fingerprintCallback.onFailed(); } }; /* * 在 Android Q,Google 提供了 Api BiometricManager.canAuthenticate() 用來檢測指紋識別硬件是否可用及是否添加指紋 * 不過尚未開放,標記為"Stub"(存根) * 所以暫時還是需要使用 Andorid 6.0 的 Api 進行判斷 * */ private boolean canAuthenticate(Context context, FingerprintCallback fingerprintCallback) { /* * 硬件是否支持指紋識別 * */ if (!FingerprintManagerCompat.from(context).isHardwareDetected()) { fingerprintCallback.onError(FingerprintManager.FINGERPRINT_ERROR_HW_NOT_PRESENT, context.getString(R.string.biometricprompt_verify_error_no_hardware)); return false; } //是否已添加指紋 if (!FingerprintManagerCompat.from(context).hasEnrolledFingerprints()) { fingerprintCallback.onNoneEnrolled(); return false; } return true; } }
這里需要開發者關注的有兩點:
(1) Android 9.0 不允許開發者自定義指紋識別框,但系統提供的指紋識別框的靈活性堪憂。比如說,目前來看,系統只允許在識別框出現一個按鈕,放了 “取消” 就不能放 “密碼驗證” ,放了 “密碼驗證” 就不能放 “取消”。(尷尬臉……)
(2) 系統提供的指紋識別框只能在界面底部,不可以上下居中。但在某些手機上(如OPPO reno),指紋傳感器也是在界面底部,當拉起指紋識別時,會在指紋傳感器的位置顯示一個指紋圖標,以提示用戶在哪下指。然而,系統提供的指紋識別框上也有一個指紋圖標,這兩個指紋圖標就發生了重合或者離的很近。(尷尬臉……)
示例:
鑒于以上問題,指紋開源庫提供了一個方法 builder.enableAndroidP(boolean enableAndroidP) ,允許調用者開啟或者關閉 Android 9.0 系統提供的指紋識別框。當關閉時,將使用 Android 6.0 的指紋識別 Api,并使用自定義的指紋識別框。
指紋識別庫 Github 地址:https://github.com/ZuoHailong/BiometricPrompt
最后
如果你看到了這里,覺得文章寫得不錯就給個贊唄?如果你覺得那里值得改進的,請給我留言。一定會認真查詢,修正不足。謝謝。