前言
目前指紋領域無論從產品角度還是技術角度都已經趨於成熟,但是當各位開發者準備深入探究的時候,卻發現網上很多文章都是皮毛,很難有較深的啟示。本文將著重介紹指紋驗證開發整個過程,包括技術選型、產品的設計方案邏輯、程式碼的架構以及後續測試中遇到的相容性問題等幾個方面。在這裡拋磚引玉,希望能給予大家一些啟發。
技術選型
產品:我們們 Android 端能做指紋驗證嗎?
開發:不能,一堆相容問題。
產品:我們們 Android 端能做指紋驗證嗎?
開發:不能,一堆相容問題。
產品:我們們 Android 端能做指紋驗證嗎?
開發:不能,一堆相容問題。
產品:我們們 Android 端能做指紋驗證嗎?
開發:我……我試試吧……
著手調研,開發前肯定先拿市面上競品的功能來瞧瞧。我們同比了支付寶、微信支付和招商App。
產品:怎麼支付寶和微信就沒相容問題了?
開發:那是因為支付寶和騰迅有自己的協議!(一聽怎麼XXX支援,怎麼XXX沒問題,升起無名火)這個標準直接和裝置廠商合作,而應用方只有微信和支付寶自己。支付寶指紋支付標準是 IFAA ,騰訊的指紋支付標準是 SOTER,也就是說沒有其他應用方會使用這個標準。所以很看應用方和裝置廠商的協商程度。現在 IFAA 沒有開源,只有 SOTER 是開源的了,如果接入,我們能省去相容性測試的工作量,而且有些 6.0 以下的機型 SOTER 也支援。還有!(星星眼)每個指紋將會有唯一 ID,也就是說,我們能把賬號和指紋繫結起來,更加安全。
產品:不行不行!這 SOTER 壓根沒支援華為,華為使用者是我們的主要使用者群,而且以後機型的擴充套件受第三方支援的限制。
開發:之前小米和華為就沒有支援 SOTER 標準,現在小米是支援了,華為不見得會支援,因為 SOTER 和廠商合作,出廠的時候就將私鑰儲存在 TEE 中,華為目前多 TEE 系統開發尚未成熟,只能支援一個 TEE ,顯然華為不願意將唯一的 TEE 交給騰訊掌控。其他手機廠商一般使用高通或第三方的 TEE 系統方案,這些系統目前都支援多 TEE 執行環境,即使將其中一個 TEE 的公共金鑰交給騰訊運營,並不影響手機廠商運營自己的 TEE 平臺。
產品:不接入了,我們用 Google API。
開發:那好,來制定下條件先:
- 裝置硬體不支援直接沒得玩
- 手機要有除了指紋外的安全認證方式(比如密碼、圖案) ,這是安卓系統的雙重鎖規則。
- 使用者手機至少錄入了一個指紋,沒錄入指紋說明平時沒有用過指紋驗證功能,這種使用者我們就不管了。
- 使用 Google API,不管什麼情況,只要驗證的指紋是系統指紋列表裡存在的,就驗證通過,Google API 是沒有提供指紋唯一ID的,所以想要根據本機上的指紋索引來區別不同手指無法做到,也就無法實現指紋和賬號繫結。
- 僅支援 Android 6.0 以上系統,Google 官方支援指紋識別的標準介面是在 Android6.0 開始的,如果廠商在這之前就已經做了指紋識別,那我們就不管了。(開發者也可以使用廠商提供的第三方指紋識別SDK)
產品:(點頭)可以,開幹吧!用 Google API 相容性問題處理和測試量較大,所以我們支援的機型做成可配置,控制風險。第一期先支援幾個機型。
- Google官方Sample
- SOTER 介紹
SOTER 支援機型
SOTER SDK地址- 阿里指紋
- IFAA暫無開源
2018.12.10 更新
SOTER 已支援部分華為機型 SOTER 支援機型 wiki
架構
好了,demo 寫完了,看下了產品文件。啥?場景這麼複雜?!分支繁多,還需要結合到之前存在的手勢驗證功能(使用者有兩種安全方式可選:指紋驗證和手勢驗證)。
業務場景有四個:
- 冷啟動app的指紋驗證
- 切換賬號登陸後的引導設定
- 在設定頁使用者手動開啟指紋登陸
- 設定頁手動關閉指紋登陸
每一次驗證的狀態,都會通過 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));
複製程式碼
相容問題
1. 明明符合條件,isHardwareDetected() 返回 false?
在同一機型上呼叫 FingerprintManagerCompat 的 isHardwareDetected() 和 hasEnrolledFingerprints() 時候,返回的都是 false,但是呼叫 FingerprintManager 的 isHardwareDetected() 和 hasEnrolledFingerprints() 時,卻是返回 true。
解決:是否符合指紋條件可以多加一層判斷。
2. Letv X500 Android 6.0,API23 不按正常的套路回撥
onAuthenticationError 和 onAuthenticationFailed,理論上應該是識別失敗的情況,但是該機型點選取消指紋識別也會先回撥一次Error,如果遇到這種情況,只能根據具體專案環境中去進行規避適配了。
3. 魅族上遇到的坑
onAuthenticationHelp 回撥不按套路出牌,正常官網文件解釋,這個方法的回撥時機是在指紋認證期間發生可恢復性的錯誤時回撥。結果在魅族上,啟動指紋識別認證的時候就會回撥這個方法,裡面傳遞回來的資訊提示是“等待按下手指”,也就是說,它的 onAuthenticationHelp 回撥跟官網時機不一樣,而且方法的作用也變了,它在正常的情況回撥了 onAuthenticationHelp。
解決:不影響驗證流程,無需解決
4. 小米 鎖屏和切後臺生命週期不一致
產品需求:使用者鎖屏或切到後臺時(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;
}
複製程式碼
然而小米6、米mix2 鎖屏時的生命週期是 onAuthenticationError -> onStop;切到後臺是 onStop -> onAuthenticationError。導致不同流程下拿到 isInAuth 標誌位不一致,無法自動調起驗證。
解決:介面指紋按鈕可以手動調起驗證,無需相容處理。
小米5生命週期同上,但是無論是自動還是手動調起驗證,馬上就回撥了 onAuthenticationError,也就是說 MI5 從後臺切回來後,指紋驗證流程中斷。
解決:用一個棧來儲存呼叫方法順序,如果驗證方法調起,馬上就回撥 onAuthenticationError 方法,則判定是屬於相容問題,按驗證失敗來解決。
5. 金鑰解密失敗
三星SM-A9100 、Nexus 6P金鑰解密失敗
解決:暫無法解決
其他相容解決方案:
- 三星passSdk(不過從2018下半年開始,Pass SDK 將不再提供 DEVICE_FINGERPRINT_UNIQUE_ID 。也就是不再為每個已註冊的指紋提供索引了。因此將無法通過 SDK 區分使用哪個指紋來驗證使用者。)
- 魅族 flyme開發平臺提供了指紋驗證官方api
非相容問題
1. 新註冊指紋金鑰解密失敗
系統中註冊了一個新的指紋的情況下,即使指紋在系統指紋列表裡,驗證也不通過。
解決:刪除了當前無效的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);
}
...
}
複製程式碼
2. 裝置已有指紋,生成金鑰卻異常提示沒有指紋
非復現,和裝置無關,懷疑是谷歌 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。