項目需求討論-APP手勢解鎖及指紋解鎖

很久沒寫文章了,最近也比較偷懶,今天繼續討論我實際開發中遇到的需求,那就是關於APP解鎖,你們都知道。如今愈來愈多的APP在填入帳號密碼後,第二次登陸後,基本不會再次重複輸入帳號密碼了。而是快捷登陸,而經常使用的就是 指紋解鎖手勢解鎖 二種.javascript


好了,咱們就開始咱們今天的解鎖之旅。

這邊我只是展現個人需求的邏輯,不一樣項目可能邏輯不一樣,不影響本文主要內容。html

主要步驟就分三步:java

  1. 帳號密碼登陸。登陸成功後彈出一個彈框讓用戶選擇快捷登陸方式。
  2. 而後跳到相應的快捷登陸的設置界面
  3. 下次登陸的時候就進行快捷登陸

咱們一步步來看。android

快捷登陸方式選擇

當用帳號密碼登陸成功後,咱們就在登陸界面直接彈出一個彈框,而後讓用戶選擇想要的快捷登陸方式,固然若是用戶二種都不想要,那就直接按取消,而後登陸到主頁,而後下次再打開應用就會又要從新輸入帳號密碼。
git

快捷登陸方式選擇框


這裏就會遇到咱們的第一個問題:

由於Android手機有不少種類,有些有指紋,有些沒有指紋, 那咱們須要在有指紋的時候,跳出這個有二種選擇的彈框,若是沒有指紋解鎖,就直接跳到手勢解鎖的界面。github

個人判斷可能比較籠統,固然還有更好的:算法

  1. 我直接就判斷SDK是否>= 23,由於指紋解鎖是SDK 23 出來的,可是不少國產手機多是Android 5的系統,可是也有指紋解鎖。這裏我就直接忽略了。莫怪我心狠。
  2. 在網上看到有人用反射,就是在Application中,用反射獲取FingerprintManager這個類的對象,看是否能成功獲取,若是能,就存一個boolean變量爲ture,說明這個手機裏面有指紋相關的。若是獲取失敗,就說明沒有指紋。api

    public class MyApplication extends Application {
     public static final String HAS_FINGERPRINT_API = "hasFingerPrintApi";
     public static final String SETTINGS = "settings";
    
     @Override
     public void onCreate() {
         super.onCreate();
         SharedPreferences sp = getSharedPreferences(SETTINGS, MODE_PRIVATE);
         if (sp.contains(HAS_FINGERPRINT_API)) { // 檢查是否存在該值,沒必要每次都經過反射來檢查
             return;
         }
         SharedPreferences.Editor editor = sp.edit();
         try {
             Class.forName("android.hardware.fingerprint.FingerprintManager"); // 經過反射判斷是否存在該類
             editor.putBoolean(HAS_FINGERPRINT_API, true);
         } catch (ClassNotFoundException e) {
             editor.putBoolean(HAS_FINGERPRINT_API, false);
             e.printStackTrace();
         }
         editor.apply();
     }
    }複製代碼

咱們解決了彈出框彈出的時機後,咱們就要來作這個彈出框:

我之前作彈出框都是使用Dialog系列,後來無心間看到谷歌推薦你們使用DialogFragment來作彈框,取代原來的Dialog,因此正好藉着此次機會,本身寫了這個DialogFragment。我下面只給出重要部分。具體的你們去百度下DialogFragment便可。安全

public class LockChooseFragment extends DialogFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setRetainInstance(true);

        //設置DialogFragment 的主題及彈框的Style。
        setStyle(DialogFragment.STYLE_NO_TITLE, android.R.style.Theme_Material_Light_Dialog);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_lock_choose, container, false);
        unbinder = ButterKnife.bind(this, view);
        return view;
    }

    @Override
    public void onResume() {
        super.onResume();

        aty = (LoginActivity) getActivity();

        //讓咱們的彈框沒法點擊外面區域消失
        getDialog().setCanceledOnTouchOutside(false);
        getDialog().setCancelable(false);
    }

}複製代碼

好了。接下去彈框出來了要點擊一種解鎖,而後進行下一個界面。咱們先從簡單的手勢解鎖來講好了。app


手勢解鎖

我用的是Github的開源手勢解鎖:PatternLockView

哈哈,是否是太簡單了。。。莫怪我偷懶啊。由於github中的API寫的很清楚了。我就不重複介紹怎麼使用。我使用了以爲的確還不錯。推薦哈。

手勢解鎖


指紋解鎖



首先咱們知道谷歌提供了fingerprint包。包下面的類具體有下面這些:

  1. FingerprintManager:主要用來協調管理和訪問指紋識別硬件設備
  2. FingerprintManager.AuthenticationCallback這個一個callback接口,當指紋認證後系統會回調這個接口通知app認證的結果是什麼
  3. FingerprintManager.AuthenticationResult這是一個表示認證結果的類,會在回調接口中以參數給出
  4. FingerprintManager.CryptoObject這是一個加密的對象類,用來保證認證的安全性。

在開始以前,咱們須要知道使用指紋識別硬件的基本步驟:

  1. 在AndroidManifest.xml中申明以下權限:
    <uses-permission android:name="android.permission.USE_FINGERPRINT"/>

  2. 得到FingerprintManager的對象引用

  3. 在運行是檢查設備指紋識別的兼容性,好比是否有指紋識別設備等。

下面咱們詳細說一下上面的2和3 步驟:

得到FingerprintManager的對象引用

這是app開發中得到系統服務對象的經常使用方式,以下:

// Using the Android Support Library v4
fingerprintManager = FingerprintManagerCompat.from(this);
// Using API level 23:
fingerprintManager = (FingerprintManager)getSystemService(Context.FINGERPRINT_SERVICE);複製代碼

上面給出兩種方式,第一種是經過V4支持包得到兼容的對象引用,這是google推行的作法;還有就是直接使用api 23 framework中的接口得到對象引用。


在運行是檢查設備指紋識別的兼容性,好比是否有指紋識別設備等

檢查運行條件要使得咱們的指紋識別app可以正常運行,有一些條件是必須知足的。

  1. API level 23
    指紋識別API是在api level 23也就是android 6.0中加入的,所以咱們的app必須運行在這個系統版本之上。所以google推薦使用 Android Support Library v4包來得到FingerprintManagerCompat對象,由於在得到的時候這個包會檢查當前系統平臺的版本。
  2. 硬件
    指紋識別確定要求你的設備上有指紋識別的硬件,所以在運行時須要檢查系統當中是否是有指紋識別的硬件:

使用 fingerprintManager.isHardwareDetected()來判斷是否有該硬件支持,fingerprintManager.hasEnrolledFingerprints()判斷是否手機中錄有指紋。

這裏我在使用個人手機作開發時候就遇到了一個大坑,上面提到了。谷歌推薦使用FingerprintManagerCompat,可是我在用FingerprintManagerCompat 來調用isHardwareDetected()和hasEnrolledFingerprints()時候,返回的都是false,可是用FingerprintManager來調用isHardwareDetected()和hasEnrolledFingerprints()時候,倒是返回true,而實際上我用的是小米5,Android 6 ,API23 的手機,也的確是有指紋功能的,因此我不知道爲何反而FingerprintManagerCompat這個兼容類返回是有問題的,應該跟國內廠商的底層源碼修改有關。我在Google Issue Tracker中也有不少人遇到了這個問題。但基本都什麼華爲,小米,三星等,都不是谷歌親兒子。因此後來我用的是FingerprintManager這個類,這個類的使用要求在API23及以上,由於畢竟谷歌的指紋是API23纔出來的,而我上面又正好直接判斷API23才顯示指紋解鎖的選項。不謀而合。。哈哈。可能這裏有點偷懶了。

判斷了是否有硬件支持,和手機是否有指紋以後,要注意,谷歌還須要判斷當前設備必須是處於安全保護中的,即:你的設備必須是使用屏幕鎖保護的,這個屏幕鎖能夠是password,PIN或者圖案都行。爲何是這樣呢?由於google原生的邏輯就是:想要使用指紋識別的話,必須首先使能屏幕鎖才行,這個和android 5.0中的smart lock邏輯是同樣的,這是由於google認爲目前的指紋識別技術仍是有不足之處,安全性仍是不能和傳統的方式比較的。

KeyguardManager keyguardManager =(KeyguardManager)getSystemService(Context.KEYGUARD_SERVICE); 
if (keyguardManager.isKeyguardSecure()) { 
// this device is secure. 
}複製代碼

因此這裏總結判斷是:

  1. 設備是否有硬件支持
  2. 手機是否處於安全保護中(沒開就提示用戶開啓鎖屏功能)
  3. 手機中是否有指紋記錄(沒有就提示用戶去設置應用中添加一個指紋)

好了,這些前戲都作好了,咱們就要開始指紋的驗證了。

驗證指紋

要開始掃描用戶按下的指紋是很簡單的,只要調用FingerprintManager的authenticate方法便可,那麼如今咱們來看一下這個接口:

上圖是google的api文檔中的描述,如今咱們挨個解釋一下這些參數都是什麼:

  1. crypto這是一個加密類的對象,指紋掃描器會使用這個對象來判斷認證結果的合法性。這個對象能夠是null,可是這樣的話,就意味這app無條件信任認證的結果,雖然從理論上這個過程可能被攻擊,數據能夠被篡改,這是app在這種狀況下必須承擔的風險。所以,建議這個參數不要置爲null。這個類的實例化有點麻煩,主要使用javax的security接口實現。
  2. cancel 這個是CancellationSignal類的一個對象,這個對象用來在指紋識別器掃描用戶指紋的是時候取消當前的掃描操做,若是不取消的話,那麼指紋掃描器會移植掃描直到超時(通常爲30s,取決於具體的廠商實現),這樣的話就會比較耗電。建議這個參數不要置爲null。
  3. flags 標識位,根據圖的文檔描述,這個位暫時應該爲0,這個標誌位應該是保留未來使用的。
  4. callback 這個是FingerprintManager.AuthenticationCallback類的對象,這個是這個接口中除了第一個參數以外最重要的參數了。當系統完成了指紋認證過程(失敗或者成功都會)後,會回調這個對象中的接口,通知app認證的結果。這個參數不能爲NULL。
  5. handler 這是Handler類的對象,若是這個參數不爲null的話,那麼FingerprintManager將會使用這個handler中的looper來處理來自指紋識別硬件的消息。一般來說,開發這不用提供這個參數,能夠直接置爲null,由於FingerprintManager會默認使用app的main looper來處理。

根據上面的參數,咱們一個個來具體的分析:

建立CryptoObject類對象

上面咱們分析FingerprintManager的authenticate方法的時候,看到這個方法的第一個參數就是CryptoObject類的對象,如今咱們看一下這個對象怎麼去實例化。
咱們知道,指紋識別的結果可靠性是很是重要的,咱們確定不但願認證的過程被一個第三方以某種形式攻擊,由於咱們引入指紋認證的目的就是要提升安全性。可是,從理論角度來講,指紋認證的過程是可能被第三方的中間件惡意攻擊的,常見的攻擊的手段就是攔截和篡改指紋識別器提供的結果。這裏咱們能夠提供CryptoObject對象給authenticate方法來避免這種形式的攻擊。
FingerprintManager.CryptoObject是基於Java加密API的一個包裝類,而且被FingerprintManager用來保證認證結果的完整性。一般來說,用來加密指紋掃描結果的機制就是一個Javax.Crypto.Cipher對象。Cipher對象自己會使用由應用調用Android keystore的API產生一個key來實現上面說道的保護功能。
爲了理解這些類之間是怎麼協同工做的,這裏我給出一個用於實例化CryptoObject對象的包裝類代碼,咱們先看下這個代碼是怎麼實現的,而後再解釋一下爲何是這樣。

public class CryptoObjectHelper {
    // This can be key name you want. Should be unique for the app.
    static final String KEY_NAME = "com.createchance.android.sample.fingerprint_authentication_key";

    // We always use this keystore on Android.
    static final String KEYSTORE_NAME = "AndroidKeyStore";

    // Should be no need to change these values.
    static final String KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
    static final String BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC;
    static final String ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7;
    static final String TRANSFORMATION = KEY_ALGORITHM + "/" +
    BLOCK_MODE + "/" +
    ENCRYPTION_PADDING;
    final KeyStore _keystore;

    public CryptoObjectHelper() throws Exception
    {
        _keystore = KeyStore.getInstance(KEYSTORE_NAME);
        _keystore.load(null);
    }

    public FingerprintManagerCompat.CryptoObject buildCryptoObject() throws Exception
    {
        Cipher cipher = createCipher(true);
        return new FingerprintManagerCompat.CryptoObject(cipher);
    }

    Cipher createCipher(boolean retry) throws Exception
    {
        Key key = GetKey();
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        try
        {
            cipher.init(Cipher.ENCRYPT_MODE | Cipher.DECRYPT_MODE, key);
        } catch(KeyPermanentlyInvalidatedException e)
        {
            _keystore.deleteEntry(KEY_NAME);
            if(retry)
            {
                createCipher(false);
            } else
            {
                throw new Exception("Could not create the cipher for fingerprint authentication.", e);
            }
        }
        return cipher;
    }

    Key GetKey() throws Exception
    {
        Key secretKey;
        if(!_keystore.isKeyEntry(KEY_NAME))
        {
            CreateKey();
        }

        secretKey = _keystore.getKey(KEY_NAME, null);
        return secretKey;
    }

    void CreateKey() throws Exception
    {
        KeyGenerator keyGen = KeyGenerator.getInstance(KEY_ALGORITHM, KEYSTORE_NAME);
        KeyGenParameterSpec keyGenSpec =
                new KeyGenParameterSpec.Builder(KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                        .setBlockModes(BLOCK_MODE)
                        .setEncryptionPaddings(ENCRYPTION_PADDING)
                        .setUserAuthenticationRequired(true)
                        .build();
        keyGen.init(keyGenSpec);
        keyGen.generateKey();
    }
}複製代碼

上面的類會針對每一個CryptoObject對象都會新建一個Cipher對象,而且會使用由應用生成的key。這個key的名字是使用KEY_NAME變量定義的,這個名字應該是保證惟一的,建議使用域名區別。GetKey方法會嘗試使用Android Keystore的API來解析一個key(名字就是上面咱們定義的),若是key不存在的話,那就調用CreateKey方法新建一個key。
cipher變量的實例化是經過調用Cipher.getInstance方法得到的,這個方法接受一個transformation參數,這個參數制定了數據怎麼加密和解密。而後調用Cipher.init方法就會使用應用的key來完成cipher對象的實例化工做。
這裏須要強調一點,在如下狀況下,android會認爲當前key是無效的:

  1. 一個新的指紋image已經註冊到系統中
  2. 當前設備中的曾經註冊過的指紋如今不存在了,多是被所有刪除了
  3. 用戶關閉了屏幕鎖功能
  4. 用戶改變了屏幕鎖的方式
    當上面的狀況發生的時候,Cipher.init方法都會拋出KeyPermanentlyInvalidatedException的異常,上面個人代碼中捕獲了這個異常,而且刪除了當前無效的key,而後根據參數嘗試再次建立。
    上面的代碼中使用了android的KeyGenerator來建立一個key而且把它存儲在設備中。KeyGenerator類會建立一個key,可是須要一些原始數據才能建立key,這些原始的信息是經過KeyGenParameterSpec類的對象來提供的。KeyGenerator類對象的實例化是使用它的工廠方法getInstance進行的,從上面的代碼中咱們能夠看到這裏使用的AES(Advanced Encryption Standard )加密算法的,AES會將數據分紅幾個組,而後針對幾個組進行加密。
    接下來,KeyGenParameterSpec的實例化是使用它的Builder方法,KeyGenParameterSpec.Builder封裝瞭如下重要的信息:
  5. key的名字
  6. key必須在加密和解密的時候是有效的
  7. 上面代碼中BLOCK_MODE被設置爲Cipher Block Chaining也就是KeyProperties.BLOCK_MODE_CBC,這意味着每個被AES切分的數據塊都與以前的數據塊進行了異或運算了,這樣的目的就是爲了創建每一個數據塊之間的依賴關係。
  8. CryptoObjectHelper類使用了PKSC7(Public Key Cryptography Standard #7)的方式去產生用於填充AES數據塊的字節,這樣就是要保證每一個數據塊的大小是等同的(由於須要異或計算還有方面算法進行數據處理,詳細能夠查看AES的算法原理)。
  9. setUserAuthenticationRequired(true)調用意味着在使用key以前用戶的身份須要被認證。
    每次KeyGenParameterSpec建立的時候,他都被用來初始化KeyGenerator,這個對象會產生存儲在設備上的key。

怎麼使用CryptoObjectHelper呢?

下面咱們看一下怎麼使用CryptoObjectHelper這個類,咱們直接看代碼就知道了:

CryptoObjectHelper cryptoObjectHelper = new CryptoObjectHelper();
fingerprintManager.authenticate(cryptoObjectHelper.buildCryptoObject(), 0,cancellationSignal, myAuthCallback, null);複製代碼

使用是比較簡單的,首先new一個CryptoObjectHelper對象,而後調用buildCryptoObject方法就能獲得CryptoObject對象了。


取消指紋掃描

上面咱們提到了取消指紋掃描的操做,這個操做是很常見的。這個時候可使用CancellationSignal這個類的cancel方法實現:


這個方法專門用於發送一個取消的命令給特定的監聽器,讓其取消當前操做。
所以,app能夠在須要的時候調用cancel方法來取消指紋掃描操做。


處理用戶的指紋認證結果

前面咱們分析authenticate接口的時候說道,調用這個接口的時候必須提供FingerprintManager.AuthenticationCallback類的對象,這個對象會在指紋認證結束以後系統回調以通知app認證的結果的。在android 6.0中,指紋的掃描和認證都是在另一個進程中完成(指紋系統服務)的,所以底層何時可以完成認證咱們app是不能假設的。所以,咱們只能採起異步的操做方式,也就是當系統底層完成的時候主動通知咱們,通知的方式就是經過回調咱們本身實現的FingerprintManager.AuthenticationCallback類,這個類中定義了一些回調方法以供咱們進行必要的處理:

這裏寫圖片描述
下面咱們簡要介紹一下這些接口的含義:

  1. OnAuthenticationError(int errorCode, ICharSequence errString) 這個接口會再系統指紋認證出現不可恢復的錯誤的時候纔會調用,而且參數errorCode就給出了錯誤碼,標識了錯誤的緣由。這個時候app能作的只能是提示用戶從新嘗試一遍。
  2. OnAuthenticationFailed() 這個接口會在系統指紋認證失敗的狀況的下才會回調。注意這裏的認證失敗和上面的認證錯誤是不同的,雖然結果都是不能認證。認證失敗是指全部的信息都採集完整,而且沒有任何異常,可是這個指紋和以前註冊的指紋是不相符的;可是認證錯誤是指在採集或者認證的過程當中出現了錯誤,好比指紋傳感器工做異常等。也就是說認證失敗是一個能夠預期的正常狀況,而認證錯誤是不可預期的異常狀況。
  3. OnAuthenticationHelp(int helpMsgId, ICharSequence helpString) 上面的認證失敗是認證過程當中的一個異常狀況,咱們說那種狀況是由於出現了不可恢復的錯誤,而咱們這裏的OnAuthenticationHelp方法是出現了能夠回覆的異常纔會調用的。什麼是能夠恢復的異常呢?一個常見的例子就是:手指移動太快,當咱們把手指放到傳感器上的時候,若是咱們很快地將手指移走的話,那麼指紋傳感器可能只採集了部分的信息,所以認證會失敗。可是這個錯誤是能夠恢復的,所以只要提示用戶再次按下指紋,而且不要太快移走就能夠解決。
  4. OnAuthenticationSucceeded(FingerprintManagerCompati.AuthenticationResult result)這個接口會在認證成功以後回調。咱們能夠在這個方法中提示用戶認證成功。這裏須要說明一下,若是咱們上面在調用authenticate的時候,咱們的CryptoObject不是null的話,那麼咱們在這個方法中能夠經過AuthenticationResult來得到Cypher對象而後調用它的doFinal方法。doFinal方法會檢查結果是否是會攔截或者篡改過,若是是的話會拋出一個異常。當咱們發現這些異常的時候都應該將認證當作是失敗來來處理,爲了安全建議你們都這麼作。
    關於上面的接口還有2點須要補充一下:
    1. 上面咱們說道OnAuthenticationError 和 OnAuthenticationHelp方法中會有錯誤或者幫助碼以提示爲何認證不成功。Android系統定義了幾個錯誤和幫助碼在FingerprintManager類中,以下:

      咱們的callback類實現的時候最好須要處理這些錯誤和幫助碼。
    2. 當指紋掃描器正在工做的時候,若是咱們取消本次操做的話,系統也會回調OnAuthenticationError方法的,只是這個時候的錯誤碼是FingerprintManager.FINGERPRINT_ERROR_CANCELED(值爲5),所以app須要區別對待。

好比這是我寫的自定義的AuthenticationCallback類

class FingerAuthCallback extends FingerprintManagerCompat.AuthenticationCallback {

        @Override
        public void onAuthenticationError(int errMsgId, CharSequence errString) {
            super.onAuthenticationError(errMsgId, errString);
            showError(errString);
        }

        @Override
        public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
            super.onAuthenticationHelp(helpMsgId, helpString);
            showError(helpString);
        }

        @Override
        public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
            super.onAuthenticationSucceeded(result);


            mIcon.setImageResource(R.drawable.ic_fingerprint_success);
            mErrorTextView.setTextColor(
                    ContextCompat.getColor(context, R.color.success_color));
            mErrorTextView.setText(
                    context.getResources().getString(R.string.fingerprint_success));


        }

        @Override
        public void onAuthenticationFailed() {
            super.onAuthenticationFailed();
            showError(context.getResources().getString(R.string.fingerprint_not_recognized));
        }
    }複製代碼

額外補充

指紋解鎖能夠用這個Github上的開源的庫:FingerprintAuthHelper
我使用了。起碼我測試沒問題。

谷歌的指紋解鎖的Demo:FingerprintDialog (進入後點擊右上角的download按鈕,下載demo)


參考文章:
感謝createchance的 Android 6.0指紋識別App開發demo

相關文章
相關標籤/搜索