如何实现Android指纹登录

Posted 天兰之珠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何实现Android指纹登录相关的知识,希望对你有一定的参考价值。

一、概述

指纹识别通过指纹传感器采集信息,进行指纹图像的预处理,然后进行特征点提取,最后进行特征匹配。一般指纹识别的用途有:系统解锁、应用锁、支付认证、普通的登录认证。

  • 指纹识别两种场景

本地识别:在本地完成指纹的识别后,跟本地信息绑定登陆;

后台交互:在本地完成识别后,将数据传输到服务器;

  无论是本地还是与服务器交互,都需要对信息进行加密,通常来说,与本地交互的采用对称加密,与服务器交互则采用非对称加密,下面我们来简单介绍下对称加密和非对称加密

  • 对称加密、非对称加密和签名

在正式使用指纹识别功能之前,有必要先了解一下对称加密和非对称加密的相关内容

对称加密:

 采用单密钥密码系统的方法,同一密钥作为加密和解密的工具,通过密钥控制加密和解密的指令,算法规定如何加密和解密。优点是算法公开、加密解密速度快、效率高,缺点是发送前的双方保持统一密钥,如果泄露则不安全,通常由AES、DES加密算法等;

非对称加密:

 非对称加密算法需要两个密钥来进行加密和解密,这两个秘钥是公开密钥(简称公钥)和私有密钥(简称私钥),如果一方用公钥进行加密,接受方应用私钥进行解密,反之发送方用私钥进行加密,接收方用公钥进行解密,由于加密和解密使用的不是同一密钥,故称为非对称加密算法;与对称加密算法相比,非对称加密的安全性得到了很大的提升,但是效率上则低了很多,因为解密加密花费的时间更长了,所以适合数据量少的加密,通常有RSA,ECC加密算法等等
 

其中涉及到很多相关的加密算法可参考:java安全之加密技术_itheimach的专栏-CSDN博客_java安全加密

 Java使用Cipher类实现加密,包括DES,DES3,AES和RSA加密 - 蔡昭凯 - 博客园

Android保存私密信息-强大的keyStore(译) - 简书

 Android KeyStore + FingerprintManager 存储密码_lintcgirl的博客-CSDN博客_android keystore

二:指纹识别的兼容性和安全性问题

首先说兼容性,指纹识别的 API 是 Google 在 android 6.0 开放出来的。

在 Android 6.0 以下的系统上,某些手机厂商自行支持了指纹识别,如果我们的 APP 要兼容这些设备,就还要集成厂商的指纹识别的SDK,这是最大的兼容性问题。不过,现在 Android 6.0 以下的设备已经很少了,其中支持指纹识别的设备就更少了,不对其进行兼容,我认为也是可以的。

在Android 6.0 以上的系统上,由于厂商对 Android 系统和指纹识别模块的定制化普遍,导致会出现一些兼容性问题。这个没有什么好的办法,就需要开发者见招拆招了。已经踩过坑的开发者很多,大家可以到网上搜索相关的文章看。

然后说下安全性,由于已添加的指纹是存储在手机上的,Google API 验证指纹后仅仅返回 true 或者 false,我们是很难无条件相信这个识别结果的。比如说用户的手机 root 了或者是自定制设备,指纹识别是有可能被劫持进而返回有误的识别结果的。

好在这种情况发生的概率比较低。如果指纹识别的应用场景非交易非支付,仅仅是类似于 “启动 APP 进行指纹验证” 这样的情况的话,Google API 提供的指纹识别就够用了。

三:兼容安卓6.0、9.0的指纹识别

指纹识别是在Android 6.0以后新增的功能,在使用的时候需要先判断手机的系统版本是否为安卓6.0以上

 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {}
  • AndroidManifest添加权限
 <!--AndroidP(9.0)时 生物识别权限-->
    <uses-permission android:name="android.permission.USE_BIOMETRIC" />
     <!--AndroidP(6.0)时 开启触摸传感器与身份认证的权限-->
    <uses-permission android:name="android.permission.USE_FINGERPRINT" />
  • 检查设别是否支持指纹识别

  /**
     * 检查设别是否支持指纹识别
     *
     * @return
     */
    public static SupportResult checkSupport(Context context) {
       //指纹系统服务
        FingerprintManager fingerprintManager = context.getSystemService(FingerprintManager.class);
        //判断硬件是否支持指纹
        if (!fingerprintManager.isHardwareDetected()) {
            return SupportResult.DEVICE_UNSUPPORTED;
        }
        KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
        //判断是否处于安全保护中(你的设备必须是使用屏幕锁保护的,这个屏幕锁可以是password,PIN或者图案都行)  判断 是否开启锁屏密码
        if (!keyguardManager.isKeyguardSecure()) {
            return SupportResult.SUPPORT_WITHOUT_KEYGUARD;
        }
        //设备支持且有指纹数据
        if (fingerprintManager.hasEnrolledFingerprints()) {
            return SupportResult.SUPPORT;
        }
        //设备支持指纹识别但是没有指纹数据
        return SupportResult.SUPPORT_WITHOUT_DATA;

    }

1、先判断硬件是否支持指纹识别

指纹识别肯定要求你的设备上有指纹识别的硬件,因此在运行时需要检查系统当中是不是有指纹识别的硬件

2、判断是否处于安全保护中

(你的设备必须是使用屏幕锁保护的,这个屏幕锁可以是password,PIN或者图案都行) 即判断 是否开启锁屏密码。如果未设置则指导用户跳转手机设置页面进行设置。

3、系统中是不是有注册的指纹 

在android 6.0中,普通app要想使用指纹识别功能的话,用户必须首先在setting中注册至少一个指纹才行,否则是不能使用的,如果未设置则指导用户跳转手机设置页面进行设置。

  • 跳转设置页面

根据不同的手机,跳转到指纹录入界面,如果没有检测到手机的品牌,就提醒用户手动去指纹录入

Android-引导用户指纹录入 - Android原创 - 博客园

根据6.0、9.0分别调用不同的API进行指纹识别

一:基于Android 6.0 实现指纹识别

方法authenticate

采用FingerprintManager类,进行指纹识别中最关键的方法authenticate,调起指纹识别扫描器进行指纹识别

FingerprintManager.authenticate(CryptoObject crypto, CancellationSignal cancel,
 int flags, AuthenticationCallback callback, Handler handler);

 我们来看一下这个接口:


上图是google的api文档中的描述,现在我们挨个解释一下这些参数都是什么: 

  • crypto这是一个加密类的对象

指纹扫描器会使用这个对象来判断认证结果的合法性。这个对象可以是null,但是这样的话,就意味这app无条件信任认证的结果,虽然从理论上这个过程可能被攻击,数据可以被篡改,这是app在这种情况下必须承担的风险。因此,建议这个参数不要置为null。这个类的实例化有点麻烦,主要使用javax的security接口实现,后面我的demo程序中会给出一个helper类,这个类封装内部实现的逻辑,开发者可以直接使用我的类简化实例化的过程。 

  •  cancel 这个是CancellationSignal类的一个对象

这个对象用来在指纹识别器扫描用户指纹的是时候取消当前的扫描操作,如果不取消的话,那么指纹扫描器会移植扫描直到超时(一般为30s,取决于具体的厂商实现),

不及时取消的话,指纹扫描器就会一直扫描,直至超时。这会造成两个问题:

(1) 耗电;

(2) 在超时时间内,用户将无法再次调起指纹识别。

同样,这个参数在 Android 6.0 是 @Nullable,在 Android 9.0 之后是 @NonNull ,由于上述的原因,不建议传 null 。
 

  • flags 标识位

根据上图的文档描述,这个位暂时应该为0,这个标志位应该是保留将来使用的。 

  • callback 这个是FingerprintManager.AuthenticationCallback类的对象

这个是这个接口中除了第一个参数之外最重要的参数了。当系统完成了指纹认证过程(失败或者成功都会)后,会回调这个对象中的接口,通知app认证的结果。这个参数不能为NULL。 

  • handler 这是Handler类的对象

如果这个参数不为null的话,那么FingerprintManager将会使用这个handler中的looper来处理来自指纹识别硬件的消息。通常来讲,开发这不用提供这个参数,可以直接置为null,因为FingerprintManager会默认使用app的main looper来处理。

取消指纹扫描

上面我们提到了取消指纹扫描的操作,这个操作是很常见的。这个时候可以使用CancellationSignal这个类的cancel方法实现: 

 这个方法专门用于发送一个取消的命令给特定的监听器,让其取消当前操作。 
因此,app可以在需要的时候调用cancel方法来取消指纹扫描操作

创建CryptoObject类对象

上面我们分析FingerprintManager的authenticate方法的时候,看到这个方法的第一个参数就是CryptoObject类的对象,现在我们看一下这个对象怎么去实例化。 
我们知道,指纹识别的结果可靠性是非常重要的,我们肯定不希望认证的过程被一个第三方以某种形式攻击,因为我们引入指纹认证的目的就是要提高安全性。但是,从理论角度来说,指纹认证的过程是可能被第三方的中间件恶意攻击的,常见的攻击的手段就是拦截和篡改指纹识别器提供的结果。这里我们可以提供CryptoObject对象给authenticate方法来避免这种形式的攻击。
 

FingerprintManager.CryptoObject

是基于Java加密API的一个包装类,并且被FingerprintManager用来保证认证结果的完整性。通常来讲,用来加密指纹扫描结果的机制就是一个Javax.Crypto.Cipher对象。Cipher对象本身会使用由应用调用Android keystore的API产生一个key来实现上面说道的保护功能。 

创建密钥
创建密钥要涉及到两个类:KeyStore(俗称密钥商店) 和 KeyGenerator(密钥发电机)

 三个类:
        KeyGenerator产生密钥
        KeyStore存放获取密钥
        Cipher,是一个按照一定的加密规则,将数据进行加密后的一个对象

KeyStore 是用于存储、获取密钥(Key)的容器,获取 KeyStore的方法如下:

try {
    mKeyStore = KeyStore.getInstance("AndroidKeyStore");
} catch (KeyStoreException e) {
    throw new RuntimeException("Failed to get an instance of KeyStore", e);
}

产生密钥

    /**
     * @des 根据当前指纹库创建一个密钥
     */
    void createKey() {
      
 // 创建KeyGenerator对象
            mKeyGenerator = KeyGenerator.getInstance(
                    KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
                //生成密钥参数  1.别名,这个名字可以是任意的  2.设置意图,是加密还是解密
                KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(KEYSTORE_ALIAS,
                        KeyProperties.PURPOSE_ENCRYPT |
                                KeyProperties.PURPOSE_DECRYPT)
                        //保证了只有指定的block模式下可以加密,解密数据,如果使用其它的block模式,将会被拒绝
                        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                        // 设置需要用户验证
                        .setUserAuthenticationRequired(true)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    builder.setInvalidatedByBiometricEnrollment(true);
                }
                //始化KeyGenerator
                mKeyGenerator.init(builder.build());
                //生成了SecretKey(加密密钥成功)
                mKeyGenerator.generateKey();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

创建并初始化 Cipher 对象

Cipher 对象是一个按照一定的加密规则,将数据进行加密后的一个对象。调用指纹识别功能需要使用到这个对象。创建 Cipher 对象很简单,如同下面代码那样:

 /**
     * @des 创建cipher (Cipher类提供了加密和解密的功能) AES/CBC/PKCS7Padding
     */
    public Cipher createCipher() {
        try {
            //getInstance(String transformation)  返回实现指定转换的 Cipher 对象。
            // 参数按"算法/模式/填充模式"
            return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
                    + KeyProperties.BLOCK_MODE_CBC + "/"
                    + KeyProperties.ENCRYPTION_PADDING_PKCS7);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

然后使用刚才创建好的密钥,初始化 Cipher 对象:

  /**
     * @des 初始化Cipher ,根据KeyPermanentlyInvalidatedExceptiony异常判断指纹库是否发生了变化
     */
    public boolean initCipher(Cipher cipher) {
        try {
            keyStore.load(null);
            //获取密钥
            SecretKey key = (SecretKey) keyStore.getKey(KEYSTORE_ALIAS, null);
            if (cipher == null) {
                cipher = createCipher();
            }
            //ENCRYPT_MODE 加密模式,key为密钥    进行加密
            cipher.init(Cipher.ENCRYPT_MODE, key);
            return false;
        } catch (KeyPermanentlyInvalidatedException | UnrecoverableKeyException e) {
            //该密钥已被永久无效 |未能获得有关私钥
            //指纹库是否发生了变化,这里会抛KeyPermanentlyInvalidatedException
            return true;
        } catch (KeyStoreException | CertificateException | IOException
                | NoSuchAlgorithmException | InvalidKeyException e) {
            //密钥库异常|
//            throw new RuntimeException("Failed to init Cipher", e);
            e.printStackTrace();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return true;
        }
    }

这里需要强调一点,在以下情况下,android会认为当前key是无效的: 
1. 一个新的指纹image已经注册到系统中 
2. 当前设备中的曾经注册过的指纹现在不存在了,可能是被全部删除了 
3. 用户关闭了屏幕锁功能 
4. 用户改变了屏幕锁的方式 
当上面的情况发生的时候,Cipher.init方法都会抛出KeyPermanentlyInvalidatedException的异常

处理用户的指纹认证结果

前面我们分析authenticate接口的时候说道,调用这个接口的时候必须提供FingerprintManager.AuthenticationCallback类的对象,这个对象会在指纹认证结束之后系统回调以通知app认证的结果的。在android 6.0中,指纹的扫描和认证都是在另外一个进程中完成(指纹系统服务)的,因此底层什么时候能够完成认证我们app是不能假设的。因此,我们只能采取异步的操作方式,也就是当系统底层完成的时候主动通知我们,通知的方式就是通过回调我们自己实现的FingerprintManager.AuthenticationCallback类,这个类中定义了一些回调方法以供我们进行必要的处理: 

 下面我们简要介绍一下这些接口的含义: 
1. OnAuthenticationError(int errorCode, ICharSequence errString)

这个接口会再系统指纹认证出现不可恢复的错误的时候才会调用,并且参数errorCode就给出了错误码,标识了错误的原因。测试了下一般是OnAuthenticationFailed出现5次,会进入OnAuthenticationError回调中,接下来再次进行识别时会一直回调这个方法,这个时候app能做的只能是提示用户重新尝试一遍。即指纹传感器会关闭一段时间,在下次调用authenticate时,会出现禁用期(时间依厂商不同30s,1分都有)

 
2. OnAuthenticationFailed()

这个接口会在系统指纹认证失败的情况的下才会回调。注意这里的认证失败和上面的认证错误是不一样的,虽然结果都是不能认证。认证失败是指所有的信息都采集完整,并且没有任何异常,但是这个指纹和之前注册的指纹是不相符的;但是认证错误是指在采集或者认证的过程中出现了错误,比如指纹传感器工作异常等。也就是说认证失败是一个可以预期的正常情况,而认证错误是不可预期的异常情况。 


3. OnAuthenticationHelp(int helpMsgId, ICharSequence helpString)

上面的认证失败是认证过程中的一个异常情况,我们说那种情况是因为出现了不可恢复的错误,而我们这里的OnAuthenticationHelp方法是出现了可以恢复=复的异常才会调用的。什么是可以恢复的异常呢?一个常见的例子就是:手指移动太快,当我们把手指放到传感器上的时候,如果我们很快地将手指移走的话,那么指纹传感器可能只采集了部分的信息,因此认证会失败。但是这个错误是可以恢复的,因此只要提示用户再次按下指纹,并且不要太快移走就可以解决。 


4. OnAuthenticationSucceeded(FingerprintManagerCompati.AuthenticationResult result)

这个接口会在认证成功之后回调。我们可以在这个方法中提示用户认证成功。这里需要说明一下,如果我们上面在调用authenticate的时候,我们的CryptoObject不是null的话,那么我们在这个方法中可以通过AuthenticationResult来获得Cypher对象然后调用它的doFinal方法。doFinal方法会检查结果是不是会拦截或者篡改过,如果是的话会抛出一个异常。当我们发现这些异常的时候都应该将认证当做是失败来来处理,为了安全建议大家都这么做。

 关于上面的接口还有2点需要补充一下: 
1. 上面我们说道OnAuthenticationError 和 OnAuthenticationHelp方法中会有错误或者帮助码以提示为什么认证不成功。Android系统定义了几个错误和帮助码在FingerprintManager类中,如下

 我们的callback类实现的时候最好需要处理这些错误和帮助码。 具体大家可以详细了解下。

2. 当指纹扫描器正在工作的时候,如果我们取消本次操作的话,系统也会回OnAuthenticationError方法的,只是这个时候的错误码是FingerprintManager.FINGERPRINT_ERROR_CANCELED(值为5、指纹操作以取消),如果要区别判断,则大家可以去获取对呀的值进行区别判断。 

基于Android 9.0 实现指纹识别

在AndroidP(9.0)时候,官方不再推荐使用FingerprintManager,标记为@Deprecated,开放BiometricPrompt新的API

AndroidManifest添加权限

<uses-permission android:name="android.permission.USE_BIOMETRIC" />

Android 9.0及以上的指纹认证实现,相比Android 6.0 指纹识别框可以自定义,而9.0不允许开发者自定义指纹识别框 

 this.mBiometricPrompt = new BiometricPrompt
                .Builder(activity)
                .setTitle("我是指纹弹出窗的标题")
                .setDescription("我是它的详细信息")
                .setNegativeButton("按钮名称 一般是取消",
                        activity.getMainExecutor(), new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialogInterface, int i) {
                                mSelfCanceled = true;
                                mFingerCallback.onCancel();
                                mCancellationSignal.cancel();
                            }
                        })
                .build();

指纹识别关键方法 authenticate

BiometricPrompt.authenticate(@NonNull CryptoObject crypto,
            @NonNull CancellationSignal cancel,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull AuthenticationCallback callback)

参数和6.0的差不多,

  • Executor executor (补充,是)

这个参数是 Android 9.0 Api BiometricPrompt.authenticate() 中的参数,是 @NonNull 的,作用与上个参数 Handler handler 类似,用来分发指纹识别的回调事件。

当通过主线程进行分发时,可通过 Context#getMainExecutor() 传参;

当通过共享线程池进行分发时,可通过 AsyncTask#THREAD_POOL_EXECUTOR 传参。

本文相关的事例代码可自行进行下载:GitHub - WCaiZhu/FingerprintRecognition: 有关指纹识别的工具包,适配6.0和9.0以上的安卓设备

可查看更多有关指纹的文章:

Android6.0指纹解锁demo_小白的究极进化-CSDN博客

指纹登录 - 徐继收 - 博客园

Android开发学习—指纹识别系统的原理与使用_程序病毒小队的博客-CSDN博客Android 指纹识别,提升APP用户体验,从这里开始_牛角尖-CSDN博客

以上是关于如何实现Android指纹登录的主要内容,如果未能解决你的问题,请参考以下文章

android 指纹和人脸登录

网站如何通过指纹登录?

使用oauth 2.0中的指纹登录

Android指纹API加解密

android:当用户触摸片段外部时,我如何关闭片段?

如何从颤动中正确调用android本机代码(android/IOS)?