前言
最近,在查看 [SampleProject](https://github.com/WangJie0822/SampleProject) 這個項目的時候,就覺得吧,這個登錄有點麻煩,總是要輸密碼,現(xiàn)在很多 **APP** 都是可以指紋登錄的呀,這個必須支持一波;而且開發(fā)這么多年還沒嘗試過指紋識別,這可不行,學(xué)到老活到老嘛。
指紋登錄流程
**指紋登錄** 不就是簡單的調(diào)用 **指紋識別** 的 `API` 然后登錄賬號嗎,這有什么可說的?可能有人會問了。然而并非如此,一開始我也很天真的以為就這么簡單,但是在網(wǎng)上找了很多文章之后發(fā)現(xiàn),大多數(shù)的文章都只是詳細(xì)的說明了怎么去調(diào)用 **指紋識別** 的 `API`,至于怎么用于登錄,怎么去實現(xiàn) **指紋登錄** 的業(yè)務(wù)確是很少提及,所以,這里我會先給大家講清楚實現(xiàn) **指紋登錄** 的流程再去實現(xiàn)。
指紋識別在指紋登錄中的作用
登錄是我們應(yīng)用中的邏輯,我們把 **指紋識別** 穿插在其中是需要他做什么?
實際上,我們可以把 **指紋登錄** 簡單的理解成 **不用密碼登錄**,那么為了能過 **不用密碼登錄**,我們肯定需要把用戶登錄需要的信息 `保存到本地`,那么我們首先考慮到的就是 **本地數(shù)據(jù)的安全問題**,在這里,**指紋識別** 就為我們本地數(shù)據(jù)提供了 `加密、解密` 的功能。
指紋登錄相關(guān)流程

指紋登錄流程
如上圖所示,在使用 **指紋登錄** 功能前,我們需要先 **開啟指紋登錄**,這個步驟是為了獲取登錄所需要的數(shù)據(jù),并進行加密存儲,之后再 **指紋登錄** 時,獲取存儲加密的數(shù)據(jù),進行解密,然后進行登錄。
指紋識別面臨的問題
在實現(xiàn)功能之前,我們需要知道,**Google** 從 `Android 6.0` 才開始支持 **指紋識別**,所以,如果你想兼顧 **6.0** 以下的機型,那么你可能需要自己去集成不同 **手機廠商** 的 `SDK`,當(dāng)然了,現(xiàn)在 `6.0` 以下的手機已經(jīng)很少了,而且我的項目只是一個自用的 **DEMO**,就不去做那些復(fù)雜的東西了,有需要的可以自行了解;
其次,從 `Android 6.0` **Google** 新增了 `FingerprintManager` 用于指紋識別,后續(xù)又新增了 `FingerprintManagerCompat` 提供了一些兼容性操作,再到 `Android 9.0` 新增了 `BiometricPrompt` `API` 用于生物識別,并將 `FingerprintManager` 添加了 `@Deprecated` 標(biāo)記,所以在實現(xiàn)時我們也需要考慮版本兼容問題。
指紋登錄實現(xiàn)
從上面,我們了解了 **指紋登錄** 的流程以及 **指紋識別** 的發(fā)展及要注意的問題,接下來我們開始實現(xiàn)。
指紋識別的集成
首先,我們將 **指紋識別** 功能集成進來。
雖然 `Android 6.0` 就有了 **指紋識別** 的 `API`,但顯然并不是所有手機都會支持,所以,我們首先來判斷當(dāng)前手機是否支持 **指紋識別**。
/** [Build.VERSION_CODES.M] 以上指紋管理對象 */
private val fingerprintManager: FingerprintManagerCompat by lazy {
FingerprintManagerCompat.from(activity)
}
/** 檢查指紋識別支持狀態(tài) */
fun checkBiometric(): Int {
// 獲取鎖屏管理
val km = context.getSystemService(KeyguardManager::class.java)
return when {
!fingerprintManager.isHardwareDetected -> {
// 不支持指紋
BiometricInterface.ERROR_HW_UNAVAILABLE
}
!km.isKeyguardSecure -> {
// 未設(shè)置鎖屏
BiometricInterface.ERROR_NO_DEVICE_CREDENTIAL
}
!fingerprintManager.hasEnrolledFingerprints() -> {
// 未注冊有效指紋
BiometricInterface.ERROR_NO_BIOMETRICS
}
else -> {
// 支持指紋識別
BiometricInterface.HW_AVAILABLE
}
}
}
在確定手機支持 **指紋識別** 后,我們就可以調(diào)用 `API` 拉起指紋識別功能了。
/** [Build.VERSION_CODES.M] 以上指紋管理對象 */
private val fingerprintManager: FingerprintManagerCompat by lazy {
FingerprintManagerCompat.from(activity)
}
/** [Build.VERSION_CODES.M] 以上拉起指紋認(rèn)證 */
fun authenticateM() {
fingerprintManager.authenticate(
crypto, // 包裝了 Cipher 對象的 FingerprintManagerCompat.CryptoObject 對象,用于加解密
flags, // 可選 flag,建議為 0
cancel, // CancellationSignal 對象,用于取消指紋認(rèn)證,可空,但不建議為 null
callback, // 認(rèn)證回調(diào)接口
handler // 回調(diào)所在 Handler,一般為 null
)
}
上面只是 `Android 6.0` 以上的簡單的 **指紋認(rèn)證** 代碼,但是 `FingerprintManagerCompat` 并沒有提供相關(guān)提示彈窗,所以,在這基礎(chǔ)上,我們還需要加上相關(guān)彈窗邏輯。
/** [Build.VERSION_CODES.M] 以上拉起指紋認(rèn)證 */
fun authenticateM() {
val cancellationSignal = CancellationSignal()
cancellationSignal.setOnCancelListener {
// 取消回調(diào)
}
val dialog = BiometricDialog.create()
dialog.setOnCancelListener {
// 取消指紋認(rèn)證
cancellationSignal.cancel()
}
dialog.show()
fingerprintManager.authenticate(
crypto, // 包裝了 Cipher 對象的 FingerprintManagerCompat.CryptoObject 對象,用于加解密
flags, // 可選 flag,建議為 0
cancellationSignal, // CancellationSignal 對象,用于取消指紋認(rèn)證,可空,但不建議為 null
object: FingerprintManagerCompat.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: FingerprintManagerCompat.AuthenticationResult?) {
// 認(rèn)證成功
dialog.dismiss()
}
override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence?) {
// 認(rèn)證提示
dialog.setHint(helpString)
}
override fun onAuthenticationFailed() {
// 認(rèn)證失敗
dialog.dismiss()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
// 認(rèn)證異常
dialog.dismiss()
}
}, // 認(rèn)證回調(diào)接口
null // 回調(diào)所在 Handler,一般為 null
)
}
這樣,我們的認(rèn)證功能就完成了,而在 `Android 9.0` 新增的 `BiometricPrompt` `API` 中已經(jīng)提供相關(guān)提示彈窗,所以不需要我們自己手動實現(xiàn)彈窗。
/** [Build.VERSION_CODES.Q] 以上拉起指紋認(rèn)證 */
fun authenticateQ() {
val cancellationSignal = CancellationSignal()
cancellationSignal.setOnCancelListener {
// 取消回調(diào)
}
// 生成認(rèn)證對象
val prompt = with(BiometricPrompt.Builder(activity)) {
setTitle(title)
setSubtitle(subTitle)
setDescription(hint)
setNegativeButton(negative, activity.mainExecutor, { dialog, _ ->
// 取消回調(diào)
dialog?.dismiss()
cancellationSignal.cancel()
})
build()
}
prompt.authenticate(
crypto, // 包裝了 Cipher 對象的 BiometricPrompt.CryptoObject 對象,用于加解密
cancellationSignal, // CancellationSignal 對象,用于取消指紋認(rèn)證,不能為空
executor, // 回調(diào) Executor,不能為空,可使用 activity.mainExecutor
callback // 認(rèn)證回調(diào)
)
}
那么 `crypto` 怎么獲取呢?這個就要結(jié)合業(yè)務(wù)場景來說了,因為 `Cipher` 對象在用于 **加密、解密** 時獲取的方式是不同的。
開啟指紋登錄功能
上文有說到過,**指紋登錄** 功能要先提供 **開啟指紋登錄** 來保存登錄需要的數(shù)據(jù),因為 **玩Android** 沒有單獨提供相關(guān)的 `API`,所以這里我們就使用 **登錄** 接口來驗證密碼的正確性;
所以,首先彈窗提示,讓用戶輸入密碼,確認(rèn)后調(diào)用 **登錄接口** 驗證密碼正確性,確認(rèn)密碼正確后,將密碼暫時緩存,拉起 **指紋認(rèn)證**;
指紋認(rèn)證流程在上面已經(jīng)說過了,這里我們重點介紹 `Cipher` 對象的獲取。
/** 獲取 Cipher 對象 */
fun loadCipher(): Cipher {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
// keyAlias 為密鑰別名,可自己定義,加密解密要一致
if (!keyStore.containsAlias(keyAlias)) {
// 不包含改別名,重新生成
// 秘鑰生成器
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val builder = KeyGenParameterSpec.Builder(
keyAlias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(false)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
keyGenerator.init(builder.build())
keyGenerator.generateKey()
}
// 根據(jù)別名獲取密鑰
val key = keyStore.getKey(keyAlias, null)
val cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7)
// 開啟登錄時用于加密,使用 Cipher.ENCRYPT_MODE 初始化
cipher.init(Cipher.ENCRYPT_MODE, key)
return cipher
}
獲取到 `Cipher` 對象后,調(diào)用指紋認(rèn)證,不同版本 `CryptoObject` 的對象是不同的,直接新建對應(yīng)對象,將 `Cipher` 對象傳入即可
/** 指紋認(rèn)證回調(diào)成功 */
override fun onAuthenticationSucceeded(result: AuthenticationResult?) {
// 認(rèn)證成功,獲取 Cipher 對象
val cipher = result?.cryptoObject?.cipher ?: throw RuntimeException("cipher is null!")
// 使用 cipher 對登錄信息進行加密并保存
val encryptInfo = cipher.doFinal(loginInfo.toByteArray()).toHexString()
// 保存 encryptInfo 到本地
// 保存加密向量到本地
save(encryptInfo)
save(cipher.iv.toHexString())
}
這樣 **開啟指紋登錄** 就完成了。
需要注意的有三點:
- 加密時,
Cipher對象使用cipher.init(Cipher.ENCRYPT_MODE, key)進行初始化;- 指紋認(rèn)證成功后,使用回調(diào)返回
Cipher對象對數(shù)據(jù)進行加密;- 指紋認(rèn)證成功后,要將
Cipher對象中的加密向量iv保存起來。
指紋登錄功能
通過上面開啟了 **指紋登錄** 之后,我們就可以在登錄頁進行 **指紋登錄** 了。
進入登錄頁后,可以自動拉起 **指紋登錄** 或者用戶點擊 **指紋登錄** 后拉起。
/** 獲取 Cipher 對象 */
fun loadCipher(): Cipher {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
// keyAlias 為密鑰別名,可自己定義,加密解密要一致
if (!keyStore.containsAlias(keyAlias)) {
// 不包含改別名,重新生成
// 秘鑰生成器
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val builder = KeyGenParameterSpec.Builder(
keyAlias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(false)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
keyGenerator.init(builder.build())
keyGenerator.generateKey()
}
// 根據(jù)別名獲取密鑰
val key = keyStore.getKey(keyAlias, null)
val cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7)
// 獲取開啟指紋登錄時保存的加密向量數(shù)據(jù)
val ivBytes = get(IV_BYTES).toHexByteArray()
val iv = IvParameterSpec(ivBytes)
// 使用指紋登錄,使用 Cipher.DECRYPT_MODE 和 iv 進行初始化
cipher.init(Cipher.DECRYPT_MODE, key, iv)
return cipher
}
和上面開啟一樣,拉起指紋認(rèn)證。
/** 指紋認(rèn)證回調(diào)成功 */
override fun onAuthenticationSucceeded(result: AuthenticationResult?) {
// 認(rèn)證成功,獲取 Cipher 對象
val cipher = result?.cryptoObject?.cipher ?: throw RuntimeException("cipher is null!")
// 使用 cipher 對登錄信息進行解密
val logintInfo = cipher.doFinal(get(encryptInfo).toHexByteArray()
// 使用 loginInfo 進行登錄
login(loginInfo)
}
這樣就完成了 **指紋登錄**。
需要注意的有兩點:
- 解密時,
Cipher對象使用cipher.init(Cipher.DECRYPT_MODE, key, iv)進行初始化;- 指紋認(rèn)證成功后,使用回調(diào)返回
Cipher對象對數(shù)據(jù)進行解密。
總結(jié)
看完全文,你學(xué)會怎么集成 **指紋登錄** 功能了嗎?我們需要牢記的是,**指紋登錄** 就是一個類似于 **記住密碼** 的功能,在這個過程中使用到了 **指紋認(rèn)證** 來對登錄信息進行加密解密,使用的都是 `Cipher` 對象,而用于加密和解密時 `Cipher` 對象的初始化方式有所不同,解密時需要使用到加密時生成的 `IvParameterSpec` 加密向量,而由我們初始化出來的 `Cipher` 對象是無法直接使用的,需要使用 **指紋認(rèn)證** 處理之后才能用于加密解密。
想要我的源碼嗎?想要的話可以全部給你,去找吧!我把所有源碼都放在那里!>> [SampleProject](https://github.com/WangJie0822/SampleProject) <<
感謝大家的耐心觀看,我是 [WangJie0822](http://www.wangjie0822.top) ,一個平平凡凡的程序猿,歡迎關(guān)注。
文章作者: WangJie0822
文章鏈接: http://www.wangjie0822.top/posts/bef2e009
版權(quán)聲明: 本博客所有文章除特別聲明外,均采用 CC BY-NC-SA 4.0 許可協(xié)議。轉(zhuǎn)載請注明來自 WangJie0822!