Android 中指纹识别的使用

前言

    最近,在查看  [SampleProject](https://github.com/WangJie0822/SampleProject) 这个项目的时候,就觉得吧,这个登录有点麻烦,总是要输密码,现在很多 **APP** 都是可以指纹登录的呀,这个必须支持一波;而且开发这么多年还没尝试过指纹识别,这可不行,学到老活到老嘛。

指纹登录流程

    **指纹登录** 不就是简单的调用 **指纹识别** 的 `API` 然后登录账号吗,这有什么可说的?可能有人会问了。然而并非如此,一开始我也很天真的以为就这么简单,但是在网上找了很多文章之后发现,大多数的文章都只是详细的说明了怎么去调用 **指纹识别** 的 `API`,至于怎么用于登录,怎么去实现 **指纹登录** 的业务确是很少提及,所以,这里我会先给大家讲清楚实现 **指纹登录** 的流程再去实现。

指纹识别在指纹登录中的作用

    登录是我们应用中的逻辑,我们把 **指纹识别** 穿插在其中是需要他做什么?

    实际上,我们可以把 **指纹登录** 简单的理解成 **不用密码登录**,那么为了能过 **不用密码登录**,我们肯定需要把用户登录需要的信息 `保存到本地`,那么我们首先考虑到的就是 **本地数据的安全问题**,在这里,**指纹识别** 就为我们本地数据提供了 `加密、解密` 的功能。

指纹登录相关流程

指纹登录流程
    如上图所示,在使用 **指纹登录** 功能前,我们需要先 **开启指纹登录**,这个步骤是为了获取登录所需要的数据,并进行加密存储,之后再 **指纹登录** 时,获取存储加密的数据,进行解密,然后进行登录。

指纹识别面临的问题

    在实现功能之前,我们需要知道,**Google** 从 `Android 6.0` 才开始支持 **指纹识别**,所以,如果你想兼顾 **6.0** 以下的机型,那么你可能需要自己去集成不同 **手机厂商** 的 `SDK`,当然了,现在 `6.0` 以下的手机已经很少了,而且我的项目只是一个自用的 **DEMO**,就不去做那些复杂的东西了,有需要的可以自行了解;

    其次,从 `Android 6.0` **Google** 新增了 `FingerprintManager` 用于指纹识别,后续又新增了 `FingerprintManagerCompat` 提供了一些兼容性操作,再到 `Android 9.0` 新增了 `BiometricPrompt` `API` 用于生物识别,并将 `FingerprintManager` 添加了 `@Deprecated` 标记,所以在实现时我们也需要考虑版本兼容问题。

指纹登录实现

    从上面,我们了解了 **指纹登录** 的流程以及 **指纹识别** 的发展及要注意的问题,接下来我们开始实现。

指纹识别的集成

    首先,我们将 **指纹识别** 功能集成进来。

    虽然 `Android 6.0` 就有了 **指纹识别** 的 `API`,但显然并不是所有手机都会支持,所以,我们首先来判断当前手机是否支持 **指纹识别**。
/** [Build.VERSION_CODES.M] 以上指纹管理对象 */
private val fingerprintManager: FingerprintManagerCompat by lazy {
    FingerprintManagerCompat.from(activity)
}

/** 检查指纹识别支持状态 */
fun checkBiometric(): Int {
    // 获取锁屏管理
    val km = context.getSystemService(KeyguardManager::class.java)
    return when {
        !fingerprintManager.isHardwareDetected -> {
            // 不支持指纹
            BiometricInterface.ERROR_HW_UNAVAILABLE
        }
        !km.isKeyguardSecure -> {
            // 未设置锁屏
            BiometricInterface.ERROR_NO_DEVICE_CREDENTIAL
        }
        !fingerprintManager.hasEnrolledFingerprints() -> {
            // 未注册有效指纹
            BiometricInterface.ERROR_NO_BIOMETRICS
        }
        else -> {
            // 支持指纹识别
            BiometricInterface.HW_AVAILABLE
        }
    }
}
    在确定手机支持 **指纹识别** 后,我们就可以调用 `API` 拉起指纹识别功能了。
/** [Build.VERSION_CODES.M] 以上指纹管理对象 */
private val fingerprintManager: FingerprintManagerCompat by lazy {
    FingerprintManagerCompat.from(activity)
}

/** [Build.VERSION_CODES.M] 以上拉起指纹认证 */
fun authenticateM() {
    fingerprintManager.authenticate(
        crypto, // 包装了 Cipher 对象的 FingerprintManagerCompat.CryptoObject 对象,用于加解密
        flags, // 可选 flag,建议为 0
        cancel, // CancellationSignal 对象,用于取消指纹认证,可空,但不建议为 null
        callback, // 认证回调接口
        handler // 回调所在 Handler,一般为 null
    )
}
    上面只是 `Android 6.0` 以上的简单的 **指纹认证** 代码,但是 `FingerprintManagerCompat` 并没有提供相关提示弹窗,所以,在这基础上,我们还需要加上相关弹窗逻辑。
/** [Build.VERSION_CODES.M] 以上拉起指纹认证 */
fun authenticateM() {
    val cancellationSignal = CancellationSignal()
    cancellationSignal.setOnCancelListener {
        // 取消回调
    }
    val dialog = BiometricDialog.create()
    dialog.setOnCancelListener {
        // 取消指纹认证
        cancellationSignal.cancel()
    }
    dialog.show()
    fingerprintManager.authenticate(
        crypto, // 包装了 Cipher 对象的 FingerprintManagerCompat.CryptoObject 对象,用于加解密
        flags, // 可选 flag,建议为 0
        cancellationSignal, // CancellationSignal 对象,用于取消指纹认证,可空,但不建议为 null
        object: FingerprintManagerCompat.AuthenticationCallback() {
            override fun onAuthenticationSucceeded(result: FingerprintManagerCompat.AuthenticationResult?) {
                // 认证成功
                dialog.dismiss()
            }
            override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence?) {
                // 认证提示
                dialog.setHint(helpString)
            }
            override fun onAuthenticationFailed() {
                // 认证失败
                dialog.dismiss()
            }
            override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
                // 认证异常
                dialog.dismiss()
            }
        }, // 认证回调接口
        null // 回调所在 Handler,一般为 null
    )
}
    这样,我们的认证功能就完成了,而在 `Android 9.0` 新增的 `BiometricPrompt` `API` 中已经提供相关提示弹窗,所以不需要我们自己手动实现弹窗。
/** [Build.VERSION_CODES.Q] 以上拉起指纹认证 */
fun authenticateQ() {
    val cancellationSignal = CancellationSignal()
    cancellationSignal.setOnCancelListener {
        // 取消回调
    }
    // 生成认证对象
    val prompt = with(BiometricPrompt.Builder(activity)) {
        setTitle(title)
        setSubtitle(subTitle)
        setDescription(hint)
        setNegativeButton(negative, activity.mainExecutor, { dialog, _ ->
            // 取消回调
            dialog?.dismiss()
            cancellationSignal.cancel()
        })
        build()
    }
    prompt.authenticate(
        crypto, // 包装了 Cipher 对象的 BiometricPrompt.CryptoObject 对象,用于加解密
        cancellationSignal, // CancellationSignal 对象,用于取消指纹认证,不能为空
        executor, // 回调 Executor,不能为空,可使用 activity.mainExecutor
        callback // 认证回调
    )
}
    那么 `crypto` 怎么获取呢?这个就要结合业务场景来说了,因为 `Cipher` 对象在用于 **加密、解密** 时获取的方式是不同的。

开启指纹登录功能

    上文有说到过,**指纹登录** 功能要先提供 **开启指纹登录** 来保存登录需要的数据,因为 **玩Android** 没有单独提供相关的 `API`,所以这里我们就使用 **登录** 接口来验证密码的正确性;

    所以,首先弹窗提示,让用户输入密码,确认后调用 **登录接口** 验证密码正确性,确认密码正确后,将密码暂时缓存,拉起 **指纹认证**;

    指纹认证流程在上面已经说过了,这里我们重点介绍 `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()
    }
    // 根据别名获取密钥
    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` 对象后,调用指纹认证,不同版本 `CryptoObject` 的对象是不同的,直接新建对应对象,将 `Cipher` 对象传入即可
/** 指纹认证回调成功 */
override fun onAuthenticationSucceeded(result: AuthenticationResult?) {
    // 认证成功,获取 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())
}
    这样 **开启指纹登录** 就完成了。

需要注意的有三点:

  1. 加密时,Cipher 对象使用 cipher.init(Cipher.ENCRYPT_MODE, key) 进行初始化;
  2. 指纹认证成功后,使用回调返回 Cipher 对象对数据进行加密;
  3. 指纹认证成功后,要将 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()
    }
    // 根据别名获取密钥
    val key = keyStore.getKey(keyAlias, null)
    val cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
                                    + KeyProperties.BLOCK_MODE_CBC + "/"
                                    + KeyProperties.ENCRYPTION_PADDING_PKCS7)
    // 获取开启指纹登录时保存的加密向量数据
    val ivBytes = get(IV_BYTES).toHexByteArray()
    val iv = IvParameterSpec(ivBytes)
    // 使用指纹登录,使用 Cipher.DECRYPT_MODE 和 iv 进行初始化
    cipher.init(Cipher.DECRYPT_MODE, key, iv)
    return cipher
}
    和上面开启一样,拉起指纹认证。
/** 指纹认证回调成功 */
override fun onAuthenticationSucceeded(result: AuthenticationResult?) {
    // 认证成功,获取 Cipher 对象
    val cipher = result?.cryptoObject?.cipher ?: throw RuntimeException("cipher is null!")
    // 使用 cipher 对登录信息进行解密
    val logintInfo = cipher.doFinal(get(encryptInfo).toHexByteArray()
    // 使用 loginInfo 进行登录
    login(loginInfo)
}
    这样就完成了 **指纹登录**。

需要注意的有两点:

  1. 解密时,Cipher 对象使用 cipher.init(Cipher.DECRYPT_MODE, key, iv) 进行初始化;
  2. 指纹认证成功后,使用回调返回 Cipher 对象对数据进行解密。

总结

    看完全文,你学会怎么集成 **指纹登录** 功能了吗?我们需要牢记的是,**指纹登录** 就是一个类似于 **记住密码** 的功能,在这个过程中使用到了 **指纹认证** 来对登录信息进行加密解密,使用的都是 `Cipher` 对象,而用于加密和解密时 `Cipher` 对象的初始化方式有所不同,解密时需要使用到加密时生成的 `IvParameterSpec` 加密向量,而由我们初始化出来的 `Cipher` 对象是无法直接使用的,需要使用 **指纹认证** 处理之后才能用于加密解密。

    想要我的源码吗?想要的话可以全部给你,去找吧!我把所有源码都放在那里!>> [SampleProject](https://github.com/WangJie0822/SampleProject) <<

    感谢大家的耐心观看,我是 [WangJie0822](http://www.wangjie0822.top) ,一个平平凡凡的程序猿,欢迎关注。

文章作者: WangJie0822
文章链接: http://www.wangjie0822.top/posts/bef2e009
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 WangJie0822

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,874评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,151评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,270评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,137评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,116评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,935评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,261评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,895评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,342评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,854评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,978评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,609评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,181评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,182评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,402评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,376评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,677评论 2 344

推荐阅读更多精彩内容

  • 指纹识别相关api FingerprintManagaerCompat 指纹识别的核心包装类 初始化方法 用于判断...
    好多个胖子阅读 2,121评论 1 10
  • 网上有一堆做指纹识别的demo,但是仅限于识别这一步,其实对于一个项目来说,牵扯到的东西可能更多:从无指纹识别版本...
    瑜小贤阅读 149评论 0 0
  • 指纹识别-Android @(Android进阶资料)[Android, 学习, 读书笔记, Markdown]指...
    辰曦小雨阅读 1,591评论 3 6
  • 最近项目需要使用到指纹识别的功能,查阅了相关资料后,整理成此文。 指纹识别是在Android 6.0之后新增的功能...
    湫水长天阅读 3,720评论 2 46
  • 一、 指纹识别接口从Android 6.0开始,Android系统加上了对指纹识别的支持。所有指纹识别的接口都在...
    Qi0907阅读 1,416评论 0 1