本文是 Android 生物識(shí)別身份驗(yàn)證系列文章的第二篇,上篇文章* 主要通過(guò)比較傳統(tǒng)用戶名和密碼的認(rèn)證方式和生物識(shí)別身份認(rèn)證方式的不同,以及介紹生物識(shí)別加密的不同加密方式,來(lái)向開(kāi)發(fā)者展示為何需要在應(yīng)用中使用生物識(shí)別身份認(rèn)證技術(shù)。*
為了拓展傳統(tǒng)的登錄授權(quán)流程,使其支持生物識(shí)別身份驗(yàn)證,您可以在用戶成功登錄之后提示用戶啟用生物識(shí)別身份驗(yàn)證。圖 1A 展示了一個(gè)典型的登錄流程,您可能已經(jīng)很熟悉了。當(dāng)用戶點(diǎn)擊登錄按鈕,且應(yīng)用獲取到服務(wù)器返回的 userToken 之后,再提示用戶是否啟用,如圖 1B 所示。一旦啟用,每次用戶需要登錄時(shí),應(yīng)用都應(yīng)當(dāng)自動(dòng)彈出生物識(shí)別身份驗(yàn)證對(duì)話框,如圖 2 所示。
在圖 2 中的界面有一個(gè)確定按鈕,實(shí)際上該按鈕是可選的。舉個(gè)例子,如果您開(kāi)發(fā)的是一個(gè)餐廳的應(yīng)用,建議顯示該按鈕,因?yàn)榭梢允褂蒙镒R(shí)別身份驗(yàn)證的方式讓顧客支付用餐費(fèi)用。對(duì)于敏感的交易和支付,我們建議您要求用戶進(jìn)行確認(rèn)。若要在界面中包含此確認(rèn)按鈕,您可以在構(gòu)建 BiometricPrompt.PromptInfo 時(shí)調(diào)用 setConfirmationRequired(true) 即可。這里要注意的是,如果您不調(diào)用 setConfirmationRequired(true),系統(tǒng)會(huì)默認(rèn)將其設(shè)置為 true。
接入生物識(shí)別的設(shè)計(jì)流程
示例中的代碼使用了帶有 CryptoObject 實(shí)例的加密版 BiometricPrompt。
如果您的應(yīng)用需要認(rèn)證,那么您就應(yīng)該創(chuàng)建一個(gè)專門的 LoginActivity 組件作為應(yīng)用的登錄界面。無(wú)論應(yīng)用要求進(jìn)行身份驗(yàn)證的頻率多高,只要需要驗(yàn)證,就應(yīng)該這么做。若用戶之前已認(rèn)證過(guò),那么 LoginActivity 將調(diào)用 finish() 方法,讓用戶繼續(xù)使用。如果用戶還沒(méi)有進(jìn)行身份驗(yàn)證,那么您應(yīng)該檢查生物識(shí)別身份驗(yàn)證是否啟用。
有很多方法來(lái)檢查是否啟用了生物識(shí)別。與其在各種不同的替代方案中周旋,不如我們直接深入研究一個(gè)特別的方法: 直接檢查自定義屬性 ciphertextWrapper
是否是 null。當(dāng)用戶在您的應(yīng)用中啟用生物識(shí)別身份驗(yàn)證后,您就可以創(chuàng)建一個(gè) CiphertextWrapper
數(shù)據(jù)類,來(lái)將加密后的 userToken
(也就是 ciphertext) 存儲(chǔ)在 SharedPreferences
或 Room 這樣的持久性存儲(chǔ)中。因此,若 ciphertextWrapper
不是 null,就相當(dāng)于您擁有了訪問(wèn)遠(yuǎn)程服務(wù)所需的已加密的 userToken
,這也意味著當(dāng)前生物識(shí)別已啟用。
if (ciphertextWrapper != null) {
// 用戶已啟用了生物識(shí)別
} else {
// 生物識(shí)別未啟用
}
若生物識(shí)別未被啟用,則用戶可以單擊 (如圖 1B 所示) 以啟用它,這時(shí)您將向用戶展示生物識(shí)別身份驗(yàn)證提示框,如圖 3 所示。
如下代碼示例中,showBiometricPromptForEncryption()
展示了如何設(shè)置與 BiometricPrompt 關(guān)聯(lián)的加密密鑰。本質(zhì)上,就是從一個(gè) String
初始化出一個(gè) 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))
}
}
在圖 2 和圖 3 所示的場(chǎng)景下,應(yīng)用只有 userToken
這個(gè)數(shù)據(jù)。但是除非用戶每次打開(kāi)應(yīng)用都要輸入一次密碼,否則該 userToken
就需要持久化用于之后的會(huì)話。然而,如果您直接存儲(chǔ)了未加密的 userToken
,那么攻擊者就可能侵入設(shè)備讀取明文的 userToken
,然后使用它從遠(yuǎn)程服務(wù)器上獲取數(shù)據(jù)。因此,在將 userToken
保存到本地之前,最好先將其加密,這就是圖 3 中 BiometricPrompt 的作用。當(dāng)用戶使用生物識(shí)別驗(yàn)證身份后,您的目標(biāo)是使用 BiometricPrompt 解鎖密鑰 (可以使用 auth-per-use 密鑰,也可使用 time-bound 密鑰),然后用該密鑰對(duì)服務(wù)器生成的 userToken 進(jìn)行加密,再將其保存到本地。自此,當(dāng)用戶需要登錄時(shí),就可以使用生物識(shí)別驗(yàn)證身份 (即生物識(shí)別認(rèn)證 -> 解鎖密鑰 -> 解密 userToken 進(jìn)行數(shù)據(jù)訪問(wèn))。
這里要注意區(qū)分用戶是第一次啟用生物識(shí)別,還是在使用生物識(shí)別進(jìn)行登錄。啟用生物識(shí)別時(shí),應(yīng)用調(diào)用 showBiometricPromptForEncryption()
方法,該方法會(huì)初始化一個(gè) Cipher
用于加密 userToken
。另一方面,若用戶是在使用生物識(shí)別進(jìn)行登錄,那應(yīng)該調(diào)用 showBiometricPromptForDecryption()
方法,它會(huì)初始化一個(gè)用于解密的 Cipher
,再使用該 Cipher
來(lái)解密 userToken
。
啟用生物識(shí)別之后,用戶下次返回應(yīng)用時(shí),會(huì)通過(guò)生物識(shí)別身份驗(yàn)證對(duì)話框進(jìn)行認(rèn)證,如圖 4 所示。請(qǐng)注意,由于圖 4 是用于登錄應(yīng)用,而圖 2 是用于確定交易的,所以在圖 4 中沒(méi)有確認(rèn)按鈕,因?yàn)榈卿浶袨槭且粋€(gè)被動(dòng)的、易逆向恢復(fù)的行為。
若要為您的用戶實(shí)現(xiàn)這一流程,當(dāng)您的 LoginActivity
完成認(rèn)證過(guò)程后,使用成功通過(guò) BiometricPrompt 認(rèn)證解鎖的加密對(duì)象來(lái)解密 userToken
,然后在 LoginActivity
中調(diào)用 finish()
方法。
override fun onResume() {
super.onResume()
if (ciphertextWrapper != null) {
if (SampleAppUser.fakeToken == null) {
showBiometricPromptForDecryption()
} else {
// 用戶已經(jīng)成功登錄,因此直接進(jìn)入接下來(lái)的應(yīng)用流程
// 之后的就交給開(kāi)發(fā)者您來(lái)完成了
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
// 現(xiàn)在您有了 token,就可以查詢服務(wù)器上的其他數(shù)據(jù)了
// 我們之所以稱這個(gè)為 fakeToken,是因?yàn)樗⒉皇钦嬲龔姆?wù)器中獲取到的
// 在真實(shí)場(chǎng)景下,您會(huì)在從服務(wù)器上獲取到 token 數(shù)據(jù)
// 此時(shí),它才能算是一個(gè)真正的 token
updateApp(getString(R.string.already_signedin))
}
}
}
完整的藍(lán)圖
圖 5 展示了一個(gè)完整的工程設(shè)計(jì)流程圖,這也是我們所推薦的流程。既然您在實(shí)際編碼過(guò)程中可能會(huì)在很多地方偏離此流程,例如,您所使用的加密解決方案中解鎖密鑰可能只會(huì)用于加密而不用于解密,但是在這里我們?nèi)匀幌M軌蛲ㄟ^(guò)提供這樣一個(gè)完整的示例為可能需要的開(kāi)發(fā)者們提供幫助。
凡是圖中提到 密鑰 的地方,您都可以按照需求使用 auth-per-use 密鑰或是 time-bound 密鑰。另外,凡是圖中提到的 "應(yīng)用中的存儲(chǔ)系統(tǒng)" 的地方,您也都可以將其理解為您所偏愛(ài)的結(jié)構(gòu)化存儲(chǔ): SharedPreferences
、Room
或是任何別的存儲(chǔ)方案。最后,對(duì)于 userToken 您可以將其理解為一個(gè)令牌,有了它就可以去服務(wù)器上訪問(wèn)被保護(hù)的用戶數(shù)據(jù)。服務(wù)器通常會(huì)將這種令牌作為調(diào)用方已被授權(quán)的證據(jù)。
圖中的 "對(duì) userToken 進(jìn)行加密" 的箭頭很可能會(huì)指向 "登錄完成",而不是回到 "LoginActivity"。盡管如此,我們還是在圖中讓其指向了 "LoginActivity",就是為了提醒大家注意,在用戶點(diǎn)擊 "激活生物識(shí)別" 后,可以使用一個(gè)額外的 Activity (例如 EnableBiometricAuthActivity),使代碼更加模塊化,更具可讀性?;蛘?,您也可以創(chuàng)建帶有兩個(gè) Fragment 的 LoginActivity: 一個(gè) Fragments用于實(shí)際的認(rèn)證流程,另一個(gè)用來(lái)響應(yīng)用戶點(diǎn)擊 "啟用生物識(shí)別"。
除了下面這個(gè)流程圖之外,我們還發(fā)布了一個(gè)設(shè)計(jì)指南,您可以在設(shè)計(jì)應(yīng)用時(shí)進(jìn)行參考。另外,我們 在 Github 上的示例代碼 希望也能夠幫助您更好地理解如何使用生物識(shí)別身份驗(yàn)證技術(shù)。
總結(jié)
在本篇文章中,我們介紹了:
- 如何擴(kuò)展 UI 來(lái)支持生物識(shí)別身份驗(yàn)證;
- 針對(duì)生物識(shí)別身份驗(yàn)證的流程,您的應(yīng)用應(yīng)著重解決的關(guān)鍵點(diǎn)是什么;
- 如何設(shè)計(jì)您的代碼來(lái)處理生物識(shí)別認(rèn)證的不同場(chǎng)景;
- 登錄流程的完整工程設(shè)計(jì)圖。
祝您編碼愉快!