如何在復雜業務場景中優雅實現Android指紋驗證?

前言

目前指紋領域無論從產品角度還是技術角度都已經趨于成熟,但是當各位開發者準備深入探究的時候,卻發現網上很多文章都是皮毛,很難有較深的啟示。本文將著重介紹指紋驗證開發整個過程,包括技術選型、產品的設計方案邏輯、代碼的架構以及后續測試中遇到的兼容性問題等幾個方面。在這里拋磚引玉,希望能給予大家一些啟發。

技術選型

產品:咱們 Android 端能做指紋驗證嗎?
開發:不能,一堆兼容問題。
產品:咱們 Android 端能做指紋驗證嗎?
開發:不能,一堆兼容問題。
產品:咱們 Android 端能做指紋驗證嗎?
開發:不能,一堆兼容問題。
產品:咱們 Android 端能做指紋驗證嗎?
開發:我……我試試吧……

著手調研,開發前肯定先拿市面上競品的功能來瞧瞧。我們同比了支付寶、微信支付和招商App。

產品:怎么支付寶和微信就沒兼容問題了?

開發:那是因為支付寶和騰迅有自己的協議?。ㄒ宦犜趺碭XX支持,怎么XXX沒問題,升起無名火)這個標準直接和設備廠商合作,而應用方只有微信和支付寶自己。支付寶指紋支付標準是 IFAA ,騰訊的指紋支付標準是 SOTER,也就是說沒有其他應用方會使用這個標準。所以很看應用方和設備廠商的協商程度。現在 IFAA 沒有開源,只有 SOTER 是開源的了,如果接入,我們能省去兼容性測試的工作量,而且有些 6.0 以下的機型 SOTER 也支持。還有?。ㄐ切茄郏┟總€指紋將會有唯一 ID,也就是說,我們能把賬號和指紋綁定起來,更加安全。

產品:不行不行!這 SOTER 壓根沒支持華為,華為用戶是我們的主要用戶群,而且以后機型的擴展受第三方支持的限制。

開發:之前小米和華為就沒有支持 SOTER 標準,現在小米是支持了,華為不見得會支持,因為 SOTER 和廠商合作,出廠的時候就將私鑰存儲在 TEE 中,華為目前多 TEE 系統開發尚未成熟,只能支持一個 TEE ,顯然華為不愿意將唯一的 TEE 交給騰訊掌控。其他手機廠商一般使用高通或第三方的 TEE 系統方案,這些系統目前都支持多 TEE 運行環境,即使將其中一個 TEE 的公共密鑰交給騰訊運營,并不影響手機廠商運營自己的 TEE 平臺。

產品:不接入了,我們用 Google API。

開發:那好,來制定下條件先:

  1. 設備硬件不支持直接沒得玩
  2. 手機要有除了指紋外的安全認證方式(比如密碼、圖案) ,這是安卓系統的雙重鎖規則。
  3. 用戶手機至少錄入了一個指紋,沒錄入指紋說明平時沒有用過指紋驗證功能,這種用戶我們就不管了。
  4. 使用 Google API,不管什么情況,只要驗證的指紋是系統指紋列表里存在的,就驗證通過,Google API 是沒有提供指紋唯一ID的,所以想要根據本機上的指紋索引來區別不同手指無法做到,也就無法實現指紋和賬號綁定。
  5. 僅支持 Android 6.0 以上系統,Google 官方支持指紋識別的標準接口是在 Android6.0 開始的,如果廠商在這之前就已經做了指紋識別,那我們就不管了。(開發者也可以使用廠商提供的第三方指紋識別SDK)

產品:(點頭)可以,開干吧!用 Google API 兼容性問題處理和測試量較大,所以我們支持的機型做成可配置,控制風險。第一期先支持幾個機型。

2018.12.10 更新
SOTER 已支持部分華為機型SOTER 支持機型 wiki

架構

好了,demo 寫完了,看下了產品文檔。啥?場景這么復雜?!分支繁多,還需要結合到之前存在的手勢驗證功能(用戶有兩種安全方式可選:指紋驗證和手勢驗證)。

業務場景有四個:

  1. 冷啟動app的指紋驗證
  2. 切換賬號登陸后的引導設置
  3. 在設置頁用戶手動開啟指紋登陸
  4. 設置頁手動關閉指紋登陸

每一次驗證的狀態,都會通過 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 的回調方法,我們用狀態模式來分離,這樣把與特定狀態相關的行為局部化,并且將不同場景下的行為分割開來。(需要給用戶什么提示,什么操作,包括驗證次數超限的處理,取決于當前所處的場景狀態)

另外一點:需要在運行時刻根據狀態來改變行為,比如說用戶從一個正常態,轉移到驗證過程異?;蛘唑炞C過程被劫持的狀態。

驗證過程異常情況,也即是說,受用戶 root 或自定制情況,通過測試的同一個機型有可能驗證過程異常。

驗證過程被劫持,因為 Google API 只返回 true 或 false,我們當然不能無條件相信這個驗證結果,所以需要在應用內產生一對非對稱的密鑰,保證驗證過程不會被篡改。如果拿到驗證結果解密失敗,就進入了被劫持的狀態了。

驗證過程異常和驗證被劫持的狀態基本處理一致,都是屬于用戶無法再繼續驗證的場景,我們可以把這兩個狀態合為一。按照開發的思路,有異常,被劫持,那肯定是失敗了,是吧? 但是按照產品的思路,其他 3 個業務場景按失敗處理,但如果是關閉指紋的場景下(4. 設置頁手動關閉指紋登陸),就算是失敗了,也要讓他去關閉成功,不然可能會出現用戶手機中途 root 或極端情況下,無法關閉指紋,從而引起客訴。

按照分析我們可以發現,被劫持和驗證過程異常的情況的處理,依賴于當時所處的場景,所以呢,我們無法把被劫持和驗證過程異常當做一個獨立的狀態了。只能抽出作為一個公共方法。

綠色底為 Activity 層,白色底為 Util 層

為了不和業務邏輯耦合在一起,工具類包裝了一層,主要封裝了驗證條件的判斷,指紋類的初始化等等,最主要的是封裝了加密類 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?

表現機型:MI 5s、vivo X9

在同一機型上調用 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。


更新:

Android P 引入了若干可提升應用和運行應用的設備安全性的功能。
其中一項:


我是 FeelsChaotic,一個寫得了代碼 p 得了圖,剪得了視頻畫得了畫的程序媛,致力于追求代碼優雅、架構設計和 T 型成長。

歡迎關注 FeelsChaotic 的簡書掘金,如果我的文章對你哪怕有一點點幫助,歡迎 ??!你的鼓勵是我寫作的最大動力!

最最重要的,請給出你的建議或意見,有錯誤請多多指正!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,662評論 25 708
  • 最近項目需要使用到指紋識別的功能,查閱了相關資料后,整理成此文。 指紋識別是在Android 6.0之后新增的功能...
    湫水長天閱讀 3,763評論 2 46
  • Apple Pay自推出以來就備受關注,很多朋友非??春闷淝熬埃贿^其安全性也受到更多人的關注,畢竟這是關乎大家錢...
    餅哥阿杜閱讀 1,807評論 0 1
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,785評論 18 139
  • 不是遠足才能看到的幽靜, 不是被矚目才會綻放的明靜。 我想要成為那一灣水, 順風到江里,順風到海中…… 有不經風的...
    棲風慢閱讀 215評論 0 1