實(shí)戰(zhàn) | 將 Android 生物識(shí)別身份驗(yàn)證整合至應(yīng)用中

image

本文是 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 所示。

△ 圖 1A: 典型的登錄界面
△ 圖 1B: 啟用生物識(shí)別身份驗(yàn)證
△ 圖 2: 確認(rèn)使用生物識(shí)別身份驗(yàn)證進(jìn)行登錄

在圖 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ǔ)在 SharedPreferencesRoom 這樣的持久性存儲(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))
   }
}
△ 圖 3: 激活生物識(shí)別的提示

在圖 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ù)的行為。

△ 圖 4

若要為您的用戶實(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ǔ): SharedPreferencesRoom 或是任何別的存儲(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ù)。

△ 圖 5: 使用生物識(shí)別同服務(wù)器獲取授權(quán)的完整藍(lán)圖

總結(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ì)圖。

祝您編碼愉快!

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

推薦閱讀更多精彩內(nèi)容