目前指紋領域不管從產品角度仍是技術角度都已經趨於成熟,可是當各位開發者準備深刻探究的時候,卻發現網上不少文章都是皮毛,很難有較深的啓示。本文將着重介紹指紋驗證開發整個過程,包括技術選型、產品的設計方案邏輯、代碼的架構以及後續測試中遇到的兼容性問題等幾個方面。在這裏拋磚引玉,但願能給予你們一些啓發。php
產品:我們 Android 端能作指紋驗證嗎?
開發:不能,一堆兼容問題。
產品:我們 Android 端能作指紋驗證嗎?
開發:不能,一堆兼容問題。
產品:我們 Android 端能作指紋驗證嗎?
開發:不能,一堆兼容問題。
產品:我們 Android 端能作指紋驗證嗎?
開發:我……我試試吧……java
着手調研,開發前確定先拿市面上競品的功能來瞧瞧。咱們同比了支付寶、微信支付和招商App。android
產品:怎麼支付寶和微信就沒兼容問題了?git
開發:那是由於支付寶和騰迅有本身的協議!(一聽怎麼XXX支持,怎麼XXX沒問題,升起無名火)這個標準直接和設備廠商合做,而應用方只有微信和支付寶本身。支付寶指紋支付標準是 IFAA ,騰訊的指紋支付標準是 SOTER,也就是說沒有其餘應用方會使用這個標準。因此很看應用方和設備廠商的協商程度。如今 IFAA 沒有開源,只有 SOTER 是開源的了,若是接入,咱們能省去兼容性測試的工做量,並且有些 6.0 如下的機型 SOTER 也支持。還有!(星星眼)每一個指紋將會有惟一 ID,也就是說,咱們能把帳號和指紋綁定起來,更加安全。github
產品:不行不行!這 SOTER 壓根沒支持華爲,華爲用戶是咱們的主要用戶羣,並且之後機型的擴展受第三方支持的限制。api
開發:以前小米和華爲就沒有支持 SOTER 標準,如今小米是支持了,華爲不見得會支持,由於 SOTER 和廠商合做,出廠的時候就將私鑰存儲在 TEE 中,華爲目前多 TEE 系統開發還沒有成熟,只能支持一個 TEE ,顯然華爲不肯意將惟一的 TEE 交給騰訊掌控。其餘手機廠商通常使用高通或第三方的 TEE 系統方案,這些系統目前都支持多 TEE 運行環境,即便將其中一個 TEE 的公共密鑰交給騰訊運營,並不影響手機廠商運營本身的 TEE 平臺。緩存
產品:不接入了,咱們用 Google API。安全
開發:那好,來制定下條件先:bash
產品:(點頭)能夠,開幹吧!用 Google API 兼容性問題處理和測試量較大,因此咱們支持的機型作成可配置,控制風險。第一期先支持幾個機型。微信
- Google官方Sample
- SOTER 介紹
SOTER 支持機型
SOTER SDK地址- 阿里指紋
- IFAA暫無開源
2018.12.10 更新
SOTER 已支持部分華爲機型 SOTER 支持機型 wiki
好了,demo 寫完了,看下了產品文檔。啥?場景這麼複雜?!分支繁多,還須要結合到以前存在的手勢驗證功能(用戶有兩種安全方式可選:指紋驗證和手勢驗證)。
業務場景有四個:
每一次驗證的狀態,都會經過 AuthenticationCallback 回調,咱們能夠理解爲是指紋驗證的生命週期。
public class MyAuthCallback extends FingerprintManagerCompat.AuthenticationCallback {
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
//驗證過程當中遇到不可恢復的錯誤
super.onAuthenticationError(errMsgId, errString);
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
//驗證過程當中遇到可恢復錯誤
super.onAuthenticationHelp(helpMsgId, helpString);
}
}
複製代碼
onAuthenticationSucceeded 和 onAuthenticationError 的回調意味着本次的認證結束,會根據當前所處業務場景給予用戶不一樣的引導。
而 onAuthenticationFailed 和 onAuthenticationHelp 的狀況,四個業務場景都是同樣的,都是在界面上提示用戶,咱們能夠合併一塊兒處理。
因此咱們根本不須要一個業務場景就對應一個 AuthenticationCallback 回調類,咱們能夠只用一個 AuthenticationCallback 回調類來根據當前所處的業務場景分發行爲。可是我又不想在 onAuthenticationSucceeded 和 onAuthenticationError 的回調中有 Switch 邏輯。因此對於四個場景各不相同的 onAuthenticationSucceeded 和 onAuthenticationError 的回調方法,咱們用狀態模式來分離,這樣把與特定狀態相關的行爲局部化,而且將不一樣場景下的行爲分割開來。(須要給用戶什麼提示,什麼操做,包括驗證次數超限的處理,取決於當前所處的場景狀態)
另一點:須要在運行時刻根據狀態來改變行爲,好比說用戶從一個正常態,轉移到驗證過程異常或者驗證過程被劫持的狀態。
驗證過程異常狀況,也便是說,受用戶 root 或自定製狀況,經過測試的同一個機型有可能驗證過程異常。
驗證過程被劫持,由於 Google API 只返回 true 或 false,咱們固然不能無條件相信這個驗證結果,因此須要在應用內產生一對非對稱的密鑰,保證驗證過程不會被篡改。若是拿到驗證結果解密失敗,就進入了被劫持的狀態了。
驗證過程異常和驗證被劫持的狀態基本處理一致,都是屬於用戶沒法再繼續驗證的場景,咱們能夠把這兩個狀態合爲一。按照開發的思路,有異常,被劫持,那確定是失敗了,是吧? 可是按照產品的思路,其餘 3 個業務場景按失敗處理,但若是是關閉指紋的場景下(4. 設置頁手動關閉指紋登錄),就算是失敗了,也要讓他去關閉成功,否則可能會出現用戶手機中途 root 或極端狀況下,沒法關閉指紋,從而引發客訴。
按照分析咱們能夠發現,被劫持和驗證過程異常的狀況的處理,依賴於當時所處的場景,因此呢,咱們沒法把被劫持和驗證過程異常當作一個獨立的狀態了。只能抽出做爲一個公共方法。
爲了避免和業務邏輯耦合在一塊兒,工具類包裝了一層,主要封裝了驗證條件的判斷,指紋類的初始化等等,最主要的是封裝了加密類 CryptoObjectCreatorHelper ,咱們考慮到安全因素,若是不加密的話,就意味着App 無條件信任認證的結果,這個過程可能被攻擊,數據能夠被篡改,這是 App 在這種狀況下必須承擔的風險。可是這個加密過程和業務是無關的,咱們不想讓 Activity 層感知到,因此密鑰和加密對象的銷燬,會統一由工具類來把控。
爲了安全,每次驗證過程的密鑰都不一樣,驗證過程一結束,也就是回調 onAuthenticationSucceeded 和 onAuthenticationError 時,都須要銷燬掉密鑰,可是咱們不想讓業務層來操做,因此工具類也有本身的一個 AuthenticationCallback ,在 AuthenticationCallback 裏作一些和業務無關的操做,再回調 Activity 的 AuthenticationCallbackListener 。
工具類的 CallBack 是 FingerprintManagerCompat.AuthenticationCallback 實現類,業務層的 AuthenticationCallbackListener 是自定義接口,由於不想把和業務無關的往上傳遞,好比說,驗證成功的 AuthenticationResult ,驗證錯誤的 typeId,這些業務並不關心。Activity 的 AuthenticationCallbackListener 會把請求統一轉發給控制器 FingerPrintTypeController,在轉發給控制器的先後,咱們能夠作一些通用的業務操做,好比說中止界面的掃描動畫,發一些異步的請求等等,這個就是代理模式的應用了。
那控制器 FingerPrintTypeController 和四個場景的關係又是如何?咱們看看類圖。
能夠看到,四個場景,對應四個狀態類,控制器和狀態類實現了同一個接口,在內部根據當前場景轉發給對應的類, 那怎麼根據場景轉發給對應類?咱們創建一個映射表,把場景和類對應起來。每次匹配的話只要 O(1) 複雜度。
private interface FingerPrintType {
void onAuthenticationSucceeded();
void onAuthenticationError(String content);
}
private class LoginAuthType implements FingerPrintType {
@Override
public void onAuthenticationSucceeded() { }
@Override
public void onAuthenticationError(String content) { }
}
private class ClearType implements FingerPrintType {
@Override
public void onAuthenticationSucceeded() { }
@Override
public void onAuthenticationError(String content) { }
}
private class LoginSettingType implements FingerPrintType {
@Override
public void onAuthenticationSucceeded() { }
@Override
public void onAuthenticationError(String content) { }
}
private class SettingType implements FingerPrintType {
@Override
public void onAuthenticationSucceeded() { }
@Override
public void onAuthenticationError(String content) { }
}
private class FingerPrintTypeController implements FingerPrintType {
private Map<String, FingerPrintType> typeMappingMap = new HashMap<>();
public FingerPrintTypeController() {
typeMappingMap.put(GESTURE_FINGER_SETTING, new SettingType());
typeMappingMap.put(GESTURE_FINGER_LOGIN_SETTING, new LoginSettingType());
typeMappingMap.put(GESTURE_FINGER_CLEAR, new ClearType());
typeMappingMap.put(GESTURE_FINGER_LOGIN, new LoginAuthType());
}
@Override
public void onAuthenticationSucceeded() {
typeMappingMap.get(mType).onAuthenticationSucceeded();
}
@Override
public void onAuthenticationError(String content) {
typeMappingMap.get(mType).onAuthenticationError(content);
}
}
複製代碼
這個時候產品又說了,一樣是異常狀況,可是被劫持和異常過程異常的提示文案要不同,ok,那咱們將提示語和操做分離開來,提示和業務場景的對應關係也預先緩存在 Map 裏,直接 get 獲取具體提示,做爲參數傳入就能夠了。
//普通異常狀況提示
exceptionTipsMappingMap = new HashMap<>();
exceptionTipsMappingMap.put(GESTURE_FINGER_SETTING, getString(R.string.fingerprint_no_support_fingerprint_gesture));
exceptionTipsMappingMap.put(GESTURE_FINGER_LOGIN_SETTING, getString(R.string.fingerprint_no_support_fingerprint_gesture));
exceptionTipsMappingMap.put(GESTURE_FINGER_CLEAR, null);
exceptionTipsMappingMap.put(GESTURE_FINGER_LOGIN, getString(R.string.fingerprint_no_support_fingerprint_account));
複製代碼
在同一機型上調用 FingerprintManagerCompat 的 isHardwareDetected() 和 hasEnrolledFingerprints() 時候,返回的都是 false,可是調用 FingerprintManager 的 isHardwareDetected() 和 hasEnrolledFingerprints() 時,倒是返回 true。
解決:是否符合指紋條件能夠多加一層判斷。
onAuthenticationError 和 onAuthenticationFailed,理論上應該是識別失敗的狀況,可是該機型點擊取消指紋識別也會先回調一次Error,若是遇到這種狀況,只能根據具體項目環境中去進行規避適配了。
onAuthenticationHelp 回調不按套路出牌,正常官網文檔解釋,這個方法的回調時機是在指紋認證期間發生可恢復性的錯誤時回調。結果在魅族上,啓動指紋識別認證的時候就會回調這個方法,裏面傳遞回來的信息提示是「等待按下手指」,也就是說,它的 onAuthenticationHelp 回調跟官網時機不同,並且方法的做用也變了,它在正常的狀況回調了 onAuthenticationHelp。
解決:不影響驗證流程,無需解決
產品需求:用戶鎖屏或切到後臺時(onStop)自動中止指紋驗證,回到界面時(onResume)自動調起驗證。
因此我在指紋回調方法中加入了標誌位 isInAuth。onStop時保存 isInAuth,onResume時 isInAuth == true 則自動調起驗證。
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
isInAuth = false;
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
isInAuth = false;
}
@Override
public void onAuthenticationFailed() {
isInAuth = true;
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
isInAuth = true;
}
複製代碼
然而小米六、米mix2 鎖屏時的生命週期是 onAuthenticationError -> onStop;切到後臺是 onStop -> onAuthenticationError。致使不一樣流程下拿到 isInAuth 標誌位不一致,沒法自動調起驗證。
解決:界面指紋按鈕能夠手動調起驗證,無需兼容處理。
小米5生命週期同上,可是不管是自動仍是手動調起驗證,立刻就回調了 onAuthenticationError,也就是說 MI5 從後臺切回來後,指紋驗證流程中斷。
解決:用一個棧來存儲調用方法順序,若是驗證方法調起,立刻就回調 onAuthenticationError 方法,則斷定是屬於兼容問題,按驗證失敗來解決。
三星SM-A9100 、Nexus 6P密鑰解密失敗
解決:暫沒法解決
其餘兼容解決方案:
- 三星passSdk(不過從2018下半年開始,Pass SDK 將再也不提供 DEVICE_FINGERPRINT_UNIQUE_ID 。也就是再也不爲每一個已註冊的指紋提供索引了。所以將沒法經過 SDK 區分使用哪一個指紋來驗證用戶。)
- 魅族 flyme開發平臺提供了指紋驗證官方api
系統中註冊了一個新的指紋的狀況下,即便指紋在系統指紋列表裏,驗證也不經過。
解決:刪除了當前無效的key,而後根據參數再次生成密鑰。
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
...
/**
* doFinal方法會檢查結果是否是會攔截或者篡改過,
* 若是是的話會拋出一個異常,異常的時候都將認證當作是失敗來處理
*/
try {
result.getCryptoObject().getCipher().doFinal();
mCustomCallback.onAuthenticationSucceeded(true);
} catch (IllegalBlockSizeException e) {
//若是是新錄入的指紋,會拋出該異常,須要從新生成密鑰對從新驗證,這裏加個次數限制,避免進入驗證異常->從新驗證->又驗證異常的死循環
if (happenCount == 0) {
beginAuthenticate();
happenCount++;
return;
}
mCustomCallback.onAuthenticationSucceeded(false);
} catch (Exception e) {
mCustomCallback.onAuthenticationSucceeded(false);
}
...
}
複製代碼
非復現,和設備無關,懷疑是谷歌 API 的坑。
java.lang.IllegalStateException: At least one fingerprint must be enrolled to create keys requiring user authentication for every use
複製代碼
解決:暫時只想到針對這個特定異常,直接使用無密鑰驗證,有必定的安全風險,有更好方案歡迎補充。
本文完整 Demo 地址 Demo 僅供參考架構和兼容處理,若是後續接入魅族和三星 SDK,能夠考慮用策略模式替換Goolge API。