本文是 Android 生物識別身份驗證系列文章的第二篇,上篇文章 主要經過比較傳統用戶名和密碼的認證方式和生物識別身份認證方式的不一樣,以及介紹生物識別加密的不一樣加密方式,來向開發者展現爲什麼須要在應用中使用生物識別身份認證技術。android
爲了拓展傳統的登陸受權流程,使其支持生物識別身份驗證,您能夠在用戶成功登陸以後提示用戶啓用生物識別身份驗證。圖 1A 展現了一個典型的登陸流程,您可能已經很熟悉了。當用戶點擊登陸按鈕,且應用獲取到服務器返回的 userToken 以後,再提示用戶是否啓用,如圖 1B 所示。一旦啓用,每次用戶須要登陸時,應用都應當自動彈出生物識別身份驗證對話框,如圖 2 所示。git
△ 圖 1A: 典型的登陸界面github
△ 圖 1B: 啓用生物識別身份驗證服務器
△ 圖 2: 確認使用生物識別身份驗證進行登陸app
在圖 2 中的界面有一個肯定按鈕,實際上該按鈕是可選的。舉個例子,若是您開發的是一個餐廳的應用,建議顯示該按鈕,由於可使用生物識別身份驗證的方式讓顧客支付用餐費用。對於敏感的交易和支付,咱們建議您要求用戶進行確認。若要在界面中包含此確認按鈕,您能夠在構建 BiometricPrompt.PromptInfo 時調用 setConfirmationRequired(true) 便可。這裏要注意的是,若是您不調用 setConfirmationRequired(true)),系統會默認將其設置爲 true。ide
示例中的代碼使用了帶有 CryptoObject 實例的加密版 BiometricPrompt。模塊化
若是您的應用須要認證,那麼您就應該建立一個專門的 LoginActivity 組件做爲應用的登陸界面。不管應用要求進行身份驗證的頻率多高,只要須要驗證,就應該這麼作。若用戶以前已認證過,那麼 LoginActivity 將調用 finish() 方法,讓用戶繼續使用。若是用戶尚未進行身份驗證,那麼您應該檢查生物識別身份驗證是否啓用。ui
有不少方法來檢查是否啓用了生物識別。與其在各類不一樣的替代方案中周旋,不如咱們直接深刻研究一個特別的方法: 直接檢查自定義屬性 ciphertextWrapper
是不是 null。當用戶在您的應用中啓用生物識別身份驗證後,您就能夠建立一個 CiphertextWrapper
數據類,來將加密後的 userToken
(也就是 ciphertext) 存儲在 SharedPreferences
或 Room 這樣的持久性存儲中。所以,若 ciphertextWrapper
不是 null,就至關於您擁有了訪問遠程服務所需的已加密的 userToken
,這也意味着當前生物識別已啓用。this
if (ciphertextWrapper != null) { // 用戶已啓用了生物識別 } else { // 生物識別未啓用 }
若生物識別未被啓用,則用戶能夠單擊 (如圖 1B 所示) 以啓用它,這時您將向用戶展現生物識別身份驗證提示框,如圖 3 所示。google
以下代碼示例中,showBiometricPromptForEncryption()
展現瞭如何設置與 BiometricPrompt 關聯的加密密鑰。本質上,就是從一個 String
初始化出一個 Cipher
,而後將該 Cipher
傳遞給 CryptoObject
。最後再將 CryptoObject
傳遞給 biometricPrompt.authenticate(promptInfo, cryptoObject)
方法。
binding.useBiometrics.setOnClickListener { showBiometricPromptForEncryption() } .... private fun showBiometricPromptForEncryption() { val canAuthenticate = BiometricManager.from(applicationContext).canAuthenticate() if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) { val secretKeyName = SECRET_KEY_NAME cryptographyManager = CryptographyManager() val cipher = cryptographyManager.getInitializedCipherForEncryption(secretKeyName) val biometricPrompt = BiometricPromptUtils.createBiometricPrompt(this, ::encryptAndStoreServerToken) val promptInfo = BiometricPromptUtils.createPromptInfo(this) biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher)) } }
△ 圖 3: 激活生物識別的提示
在圖 2 和圖 3 所示的場景下,應用只有 userToken
這個數據。可是除非用戶每次打開應用都要輸入一次密碼,不然該 userToken
就須要持久化用於以後的會話。然而,若是您直接存儲了未加密的 userToken
,那麼攻擊者就可能侵入設備讀取明文的 userToken
,而後使用它從遠程服務器上獲取數據。所以,在將 userToken
保存到本地以前,最好先將其加密,這就是圖 3 中 BiometricPrompt 的做用。當用戶使用生物識別驗證身份後,您的目標是使用 BiometricPrompt 解鎖密鑰 (可使用 auth-per-use 密鑰,也可以使用 time-bound 密鑰),而後用該密鑰對服務器生成的 userToken 進行加密,再將其保存到本地。自此,當用戶須要登陸時,就可使用生物識別驗證身份 (即生物識別認證 -> 解鎖密鑰 -> 解密 userToken 進行數據訪問)。
這裏要注意區分用戶是第一次啓用生物識別,仍是在使用生物識別進行登陸。啓用生物識別時,應用調用 showBiometricPromptForEncryption()
方法,該方法會初始化一個 Cipher
用於加密 userToken
。另外一方面,若用戶是在使用生物識別進行登陸,那應該調用 showBiometricPromptForDecryption()
方法,它會初始化一個用於解密的 Cipher
,再使用該 Cipher
來解密 userToken
。
啓用生物識別以後,用戶下次返回應用時,會經過生物識別身份驗證對話框進行認證,如圖 4 所示。請注意,因爲圖 4 是用於登陸應用,而圖 2 是用於肯定交易的,因此在圖 4 中沒有確認按鈕,由於登陸行爲是一個被動的、易逆向恢復的行爲。
△ 圖 4
若要爲您的用戶實現這一流程,當您的 LoginActivity
完成認證過程後,使用成功經過 BiometricPrompt 認證解鎖的加密對象來解密 userToken
,而後在 LoginActivity
中調用 finish()
方法。
override fun onResume() { super.onResume() if (ciphertextWrapper != null) { if (SampleAppUser.fakeToken == null) { showBiometricPromptForDecryption() } else { // 用戶已經成功登陸,所以直接進入接下來的應用流程 // 以後的就交給開發者您來完成了 updateApp(getString(R.string.already_signedin)) } } } .... private fun showBiometricPromptForDecryption() { ciphertextWrapper?.let { textWrapper -> val canAuthenticate = BiometricManager.from(applicationContext).canAuthenticate() if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) { val secretKeyName = getString(R.string.secret_key_name) val cipher = cryptographyManager.getInitializedCipherForDecryption( secretKeyName, textWrapper.initializationVector ) biometricPrompt = BiometricPromptUtils.createBiometricPrompt( this, ::decryptServerTokenFromStorage ) val promptInfo = BiometricPromptUtils.createPromptInfo(this) biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher)) } } } private fun decryptServerTokenFromStorage(authResult: BiometricPrompt.AuthenticationResult) { ciphertextWrapper?.let { textWrapper -> authResult.cryptoObject?.cipher?.let { val plaintext = cryptographyManager.decryptData(textWrapper.ciphertext, it) SampleAppUser.fakeToken = plaintext // 如今您有了 token,就能夠查詢服務器上的其餘數據了 // 咱們之因此稱這個爲 fakeToken,是由於它並非真正從服務器中獲取到的 // 在真實場景下,您會在從服務器上獲取到 token 數據 // 此時,它才能算是一個真正的 token updateApp(getString(R.string.already_signedin)) } } }
圖 5 展現了一個完整的工程設計流程圖,這也是咱們所推薦的流程。既然您在實際編碼過程當中可能會在不少地方偏離此流程,例如,您所使用的加密解決方案中解鎖密鑰可能只會用於加密而不用於解密,可是在這裏咱們仍然但願可以經過提供這樣一個完整的示例爲可能須要的開發者們提供幫助。
凡是圖中提到 密鑰 的地方,您均可以按照需求使用 auth-per-use 密鑰或是 time-bound 密鑰。另外,凡是圖中提到的 "應用中的存儲系統" 的地方,您也均可以將其理解爲您所偏心的結構化存儲: SharedPreferences
、Room
或是任何別的存儲方案。最後,對於 userToken 您能夠將其理解爲一個令牌,有了它就能夠去服務器上訪問被保護的用戶數據。服務器一般會將這種令牌做爲調用方已被受權的證據。
圖中的 "對 userToken 進行加密" 的箭頭極可能會指向 "登陸完成",而不是回到 "LoginActivity"。儘管如此,咱們仍是在圖中讓其指向了 "LoginActivity",就是爲了提醒你們注意,在用戶點擊 "激活生物識別" 後,可使用一個額外的 Activity (例如 EnableBiometricAuthActivity),使代碼更加模塊化,更具可讀性。或者,您也能夠建立帶有兩個 Fragment 的 LoginActivity: 一個 Fragments用於實際的認證流程,另外一個用來響應用戶點擊 "啓用生物識別"。
除了下面這個流程圖以外,咱們還發布了一個設計指南,您能夠在設計應用時進行參考。另外,咱們 在 Github 上的示例代碼 但願也可以幫助您更好地理解如何使用生物識別身份驗證技術。
△ 圖 5: 使用生物識別同服務器獲取受權的完整藍圖
在本篇文章中,咱們介紹了:
祝您編碼愉快!