1. 前言

指纹登录是一种很常见的登录办法,特别在各种金融类APP中,指纹登录的功用已成为标配。为了丰富咱们的登录办法,进步咱们的用户体会,降低咱们的短信本钱,咱们最近在货拉拉专送司机Android端上线了指纹登录功用。Google从Android 6.0开端,供给了敞开的指纹辨认相关API,咱们在依据指纹验证的根底功用上完结登录的事务场景,期望经过本次的共享能让你的app快速完结指纹登录能力。

2. 指纹辨认

从Android 6.0(Android M Api23)开端,Android 体系支撑指纹辨认功用,指纹辨认的API主要是FingerprintManager中心类。FingerprintManager供给了根底的指纹辨认功用,完结的办法主要有:判别体系是否支撑指纹,体系是否录入过指纹,建议指纹验证,撤销验证和验证成果回调。可是需要注意的是,FingerprintManager在Android 9.0(Android P Api28)做了 @Deprecated 标记,将被弃用。Android 9.0今后Google官方不再引荐运用FingerprintManager接口,引荐运用以BiometricPrompt为中心的新API代替。BiometricPrompt支撑设备供给的生物辨认,包括指纹、虹膜、面部等。可是现在来看,虹膜和面部等生物辨认的Api尚未彻底敞开,大部分设备仅支撑指纹辨认,不过在指纹辨认上进行了一致,比方要求运用一致的指纹辨认UI,不允许开发者自定义了。下面介绍FingerprintManager和BiometricPrompt这两种接口的运用办法。

2.1 FingerprintManager

FingerprintManager运用步骤:

① 恳求指纹权限

<!-- AndroidP(6.0)时 敞开接触传感器与身份认证的权限 -->
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>

② 判别是否支撑指纹辨认

  • isHardwareDetected() 判别是否有硬件支撑
  • isKeyguardSecure() 判别是否设置锁屏,因为一个手机最少要有两种登录办法
  • hasEnrolledFingerprints() 判别体系中是否增加至少一个指纹
/**
 *  判别是否支撑指纹辨认
 */
public static boolean supportFingerprint(Context context) {
    if (VERSION.SDK_INT < 23) {
        // 体系版别过低,不支撑指纹功用
        return false;
    } else {
        KeyguardManager km = context.getSystemService(KeyguardManager.class);
        FingerprintManagerCompat fm = FingerprintManagerCompat.from(context);
        if (!fm.isHardwareDetected()) {
            // 手机不支撑指纹功用
            return false;
        } else if (!km.isKeyguardSecure()) {
            // 未设置锁屏,请先设置锁屏并增加一个指纹
            return false;
        } else if (!fm.hasEnrolledFingerprints()) {
            // 至少需要在体系设置中增加一个指纹
            return false;
        }
    }
    return true;
}

③ 敞开指纹验证

@RequiresApi(api = Build.VERSION_CODES.M)
public void authenticate(FingerprintManager.CryptoObject cryptoObject, 
              CancellationSignal cancellationSignal,
              int flag,
              FingerprintManager.AuthenticationCallback authenticationCallback, 
              Handler handler) {
    if (fingerprintManager != null) {
      fingerprintManager.authenticate(cryptoObject, cancellationSignal, flag, authenticationCallback, handler);
    }
}

authenticate()这是指纹辨认中最中心的办法,用于拉起指纹辨认扫描器进行指纹辨认。解释一下这个办法参数:

  • Int flags:可选标志,暂无用处,传0即可

  • Handler handler:这个参数作用是告知体系运用这个Handler的Looper处理指纹辨认的Message。默许便是交给主线程的Looper处理,传null即可。

  • FingerprintManagerCompat.CryptoObject crypto:这是一个暗码目标的包装类,当时支撑Signature和Cipher形式的暗码目标加密,作用是指纹扫描器会运用这个目标判别指纹认证成果的合法性。能够传null,可是不建议传null,下面会解释。

  • CancellationSignal cancel:这个目标的作用便是用来撤销指纹扫描器的扫描操作。比方在用户点击辨认框上的“撤销”按钮或许切换到暗码登录后,就要及时撤销扫描器的扫描操作。不及时撤销的话,指纹扫描器就会一向扫描,直至超时。这会造成两个问题:

    • 耗电。
    • 在超时时间内,用户将无法再次调起指纹辨认。
  • FingerprintManagerCompat.AuthenticationCallback callback:指纹辨认成果的回调接口,其中声明了几个办法:

    • onAuthenticationFailed():指纹验证失利回调办法;
    • onAuthenticationSucceeded(AuthenticationResult result):这个接口会在认证成功之后回调;
    • onAuthenticationHelp(int helpMsgId, CharSequence helpString):指纹验证过错时提示协助,比方说指纹过错,咱们将显现在界面上 让用户知道状况;
    • onAuthenticationError(int errMsgId, CharSequence errString):当呈现过错的时分回调此函数,比方多次测验都失利了的时分;

因而一个指纹辨认事情次序是这样:开端辨认 –> (onAuthenticationHelp/onAuthenticationFailed)[0个或多个] –> onAuthenticationSucceeded/onAuthenticationError。

撤销辨认

/**
 * 停止辨认
 */
public void cancelListening() {
    if (cancellationSignal != null) {
        cancellationSignal.cancel();
    }
}

2.2 BiometricPrompt

BiometricPrompt和FingerprintManager差异如下:

① 引入依赖

implementation "androidx.biometric:biometric:1.1.0"

② 恳求指纹权限

<!--AndroidP(9.0)时 生物辨认权限-->
<uses-permission android:name="android.permission.USE_BIOMETRIC" />

③ 敞开指纹验证

BiometricPrompt.PromptInfo promptInfo=new BiometricPrompt.PromptInfo.Builder()
    .setTitle("指纹验证")
    .setDescription("用户指纹验证")
    .setNegativeButtonText("撤销")
    .build();
getprompt().authenticate(promptInfo);
private BiometricPrompt getprompt() {
    Executor executor = ContextCompat.getMainExecutor(this);
    BiometricPrompt.AuthenticationCallback callback=new BiometricPrompt.AuthenticationCallback() {
        // 指纹验证成功
        @Override
        public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
        }
        // 指纹验证失利
        @Override
        public void onAuthenticationFailed() {
        }
        // 指纹验证过错
        @Override
        public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
        }
    }
    BiometricPrompt biometricPrompt = new BiometricPrompt(this,executor,callback);
    return biometricPrompt;
}

④ 作用图

BiometricPrompt指纹辨认有体系的辨认弹窗,不能自定义UI,以下是在honor x8的显现作用:

货拉拉专送司机Android指纹认证登录实践与总结

以上介绍了指纹辨认的根底用法,可是细心看下辨认的成果回调,只是回来辨认成功/失利。这就存在两个问题:

  • 无法跟账号关联起来,满意不了指纹登录功用的需求;
  • 假如手机被Root,或许经过hook重写authenticate办法,在authenticate办法中直接调用AuthenticationCallback.onAuthenticationSucceeded办法,这样就能够随意修正辨认成果,这显然是不安全的。

因而在运用authenticate()办法时,若对第一个CryptoObject参数运用办法不正确,则存在指纹验证被绕过的风险,所以咱们需要将指纹辨认与KeyStore结合起来运用。

2.3 AndroidKeystore

AndroidKeystore体系是一个密钥库办理体系,能够在一个安全的容器中(如:借助于体系芯片中供给的可信履行环境 TEE)存储加密密钥,在咱们的加密密钥进入 Keystore 之后,能够在不必导出密钥的前提下完结加密操作。

别的,咱们能够结合身份验证体系(如指纹验证)来完结只需在用户完结身份验证之后才干运用密钥,即能够让运用指定密钥的授权运用办法,一旦生成或导入密钥,授权办法将无法更改,而且之后每次需要运用密钥时,都会由 Android Keystore 库强制履行授权。

下面看下如何结合 Keystore 和 Fingerprint 来完结数据的加密和解密:

首要咱们需要经过 Keystore 库来生成(用于后续对数据进行加密的)密钥,这里采用 AES 加密算法,而且设置这个密钥为强制授权拜访办法。

① 新建一个KeyStore密钥库,用于寄存密钥;

KeyStore keystore = KeyStore.getInstance(KEYSTORE_NAME);
keystore.load(null);

② 获取KeyGenerator密钥生成东西,生成密钥;

在生成的进程中设置了要求用户身份验证的选项,后续步骤中的加密和解密步骤咱们能够经过指纹辨认来完结用户身份验证并经过相应的密钥进行加解密。

/**
 * 获取KeyGenerator密钥生成东西,生成密钥
 */
@RequiresApi(api = Build.VERSION_CODES.M)
public void createKey() throws Exception {
    KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_NAME);
    KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(DEFAULT_KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setUserAuthenticationRequired(true)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);
    keyGenerator.init(builder.build());
    keyGenerator.generateKey();
}

③ 经过密钥初始化Cipher目标,生成加密目标CryptoObject;

/**
 * 经过密钥初始化Cipher目标,生成加密目标CryptoObject
 */
public Cipher createCipher() throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, NoSuchPaddingException, InvalidKeyException {
    SecretKey key = (SecretKey) keyStore.getKey(DEFAULT_KEY_NAME, null);
    Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7);
    cipher.init(Cipher.ENCRYPT_MODE, key);
    return cipher;
}

④ 调用authenticate() 办法发动指纹传感器并开端监听

CryptoObjectHelper cryptoObjectHelper = new CryptoObjectHelper();
if (cancellationSignal == null) {
    cancellationSignal = new CancellationSignal();
}
fingerprintManager.authenticate(
    cryptoObjectHelper.buildCryptoObject(), 
    0,
   cancellationSignal, 
    myAuthCallback, 
    null);

⑤ 在回调的类中监听指纹辨认的成果

在指纹验证经往后回调 onAuthenticationSucceeded 办法,而且在输入参数 AuthenticationResult result 中携带加密算法目标,经过此加密算法目标完结加密工作;

@Override
public void onSucceeded(FingerprintManager.AuthenticationResult result) {
    try {
        Cipher cipher = result.getCryptoObject().getCipher();
        byte[] bytes = cipher.doFinal(pwd.getBytes());
        aCache.put("pwdEncode", Base64.encodeToString(bytes,Base64.URL_SAFE));
        byte[] iv = cipher.getIV();
        aCache.put("iv", Base64.encodeToString(iv,Base64.URL_SAFE));
    }catch (Exception e){
        e.printStackTrace();
    }
}

接下来咱们经过KeyStore和Fingerprint结合完结数据加密和解密的办法,来完结指纹登录功用。

3. 指纹登录

登录功用是咱们运用中的逻辑,咱们如何把指纹辨认穿插在其中呢?实际上,咱们能够把指纹登录简单的了解成不需要用暗码登录,那么为了能过不必暗码登录,咱们需要把用户登录需要的信息保存到本地,那么就需要考虑到本地数据的安全问题,在这里,指纹辨认就为咱们本地数据供给了加密/解密的功用。

3.1 敞开指纹登录

在运用指纹登录功用前,咱们需要先敞开指纹登录,这个步骤是为了获取登录所需要的数据,并进行加密存储,具体进程如下图所示:

货拉拉专送司机Android指纹认证登录实践与总结

  • 敞开指纹登录:敞开指纹登录的机遇,一般都是先用其他办法登录了App,然后再敞开指纹登录。
  • 比对体系存在的指纹:指纹数据一直保存在手机本地,不会上传到远程。Android供给的指纹认证API只回来成功或许失利的认证成果。
  • 指纹验证成功:私钥保存在Keystore,只需指纹认证成功后才干运用,公钥上传到服务端,供指纹登录的解密、验签运用。

3.2 运用指纹登录

经过上面敞开指纹登录之后,在运用指纹登录时,获取存储加密的数据,上传给服务端进行解密验签,然后进行登录。

货拉拉专送司机Android指纹认证登录实践与总结
  • 登录授权码:指纹登录的恳求或许被人截获,尽管无法解密,可是能够重复恳求服务器获取token,在指纹登录流程中加入授权码而且运用一次就失效,能够防止攻击者绕过指纹认证。
  • 签名数据:主要是用客户端私钥签名用户信息和授权码,在敞开指纹登录时指定必须采用指纹授权的办法才干运用私钥,这样确保签名是有认证过的用户建议的;服务端假如验证签名经过,则代表数据在传输进程中没有被篡改,登录是在可信的客户端建议的。

4. 兼容性

咱们经过指纹辨认完结了指纹登录的功用,但还需要关注和处理其中的兼容性和安全性问题。下面首要说兼容性:

4.1 版别兼容

指纹辨认的API是Google在Android 6.0之后敞开出来的。在Android 6.0以下的体系上,某些手机厂商自行支撑了指纹辨认,假如咱们的App要兼容这些设备,就需要集成厂商的指纹辨认SDK,这是最大的兼容性问题。不过,现在Android 6.0以下的设备现已很少了,其中支撑指纹辨认的设备就更少了,不对其进行兼容,也是能够的。

其次,从Android 6.0新增了FingerprintManager用于指纹辨认,后续又新增了FingerprintManagerCompat供给了一些兼容操作,再到Android 9.0新增了BiometricManager API用于生物辨认,并将FingerprintManager增加了@Deprecated标记,所以在完结时咱们也需要注意这些版别兼容问题。

比方在调用FingerprintManagerCompat的isHardwareDetected()和hasEnrolledFingerprints()办法时回来的成果,或许与调用FingerprintManager的isHardwareDetected()和hasEnrolledFingerprints()办法的成果不一致,建议判别契合指纹条件时两种都判别一次。

4.2 UI兼容

FingerprintManager是开发者自定义UI,因而不存在UI兼容性问题;BiometricPrompt运用的是体系弹窗,开发者不能够自定义弹窗款式,仅能够设置弹窗上的相关案牍及按钮点击事情。因而不同厂商对体系Dialog款式的不同完结,会导致指纹验证流程中呈现不同的交互。

5. 安全性

然后说下安全性,因为已增加的指纹是存储在手机上的,指纹辨认API验证后只是回来true或false,假如用户的手机root了,指纹辨认是有或许被劫持而回来过错的辨认成果。处理这个问题的办法是在调用authicate办法前,经过AndroidKeyStore生成秘钥,并用这把秘钥生成crypto目标传入authicate办法,在AuthenticationCallback.onAuthenticationSucceeded办法中取出crypto目标,假如没有经过指纹认证,crypto目标是无法正常运用的,这在上文中已有详细阐明。

运用Google API,只需验证的指纹是手机体系指纹列表里存在的,就能验证经过,Google API是没有供给指纹仅有ID的,所以想要依据本机上的指纹ID来区别不同手指无法做到,也就无法完结指纹和账号绑定。这样会呈现下面这种现象,你注册指纹登录后,又在体系中录入了新的指纹,下次能够用新指纹登录你的账号。

货拉拉专送司机Android指纹认证登录实践与总结

在一些低版别中指纹认证成功后,依据FingerprintManager.AuthenticationResult目标能够经过反射办法获取到指纹id,可是经过阅读Android源码时,会发现大部分版别中获取指纹信息的办法被@UnsupportedAppUsage注解,表明该办法不支撑App去调用,也无法经过反射的办法来获取。

在上文敞开指纹登录阶段,App会生成一对非对称密钥,用于后续服务端认证客户端身份,这个私钥被TEE安全密钥加密存储在手机中,现在没有破解之法,可是在密钥的生成进程中或许被替换,那存储和运用再怎么安全也杯水车薪了。其次,服务端无法承认上传的公钥是否是客户端生成的,也便是无法验证公钥的来历,无法验证客户端身份。

针对上述的一些安全性问题,在国内腾讯和阿里都选择和手机厂商协作,分别供给了SOTER和IFAA两套方案,两者都要求手机厂商在产线上生成一对非对称密钥,这个密钥称为根密钥,私钥写入手机TEE,即便拿到了手机当时也没有破解之法,私钥也就不会泄漏;公钥上传到认证服务器,认证的时分用于验证手机APP上传的数据签名。针对Android密钥生成或许被阻拦的问题,让手机厂商装个补丁,替换掉不安全的中间环节,一起让手机厂商修正能在指纹辨认成功后能拿到指纹ID。

下面介绍下SOTER的密钥信赖原理,除了厂商内置的根密钥,SOTER又搞了运用密钥和事务密钥,咱们来看下运用密钥和事务密钥的发生进程。

运用密钥发生进程:运用密钥在运用初次装置的时分生成,运用密钥的私钥保存在手机端,并运用厂商内置的私钥签名,然后发送到腾讯的认证服务器签名,此刻运用密钥的公钥会保存到运用的后台。

货拉拉专送司机Android指纹认证登录实践与总结

事务密钥的发生进程:在运用登录的指纹认证注册时,运用会再生成一个事务密钥,事务密钥的私钥仍是保存在手机端,事务密钥运用手机端的运用密钥进行签名,验证只需要在运用的后台验证就能够,此刻和认证服务器就没有关系了。

货拉拉专送司机Android指纹认证登录实践与总结

咱们经过信赖厂商内置根密钥来信赖运用密钥,最终承认登录时运用的事务密钥是安全的,这样也能够让服务端承认上传的公钥是否可靠,经过与厂商协作,SOTER处理上述的安全问题。IFAA的运用进程和SOTER差不多,别的国外还有一个FIDO联盟,也是处理这些问题。总的来说,在对安全性要求相对较低的场景(如解锁),运用Android原生指纹接口配合AndroidKeystore完结;在对安全性要求较高的场景(如支付),选择采用第三方处理方案,兼容性问题少,安全性高。

6.上线作用

专送司机端Android指纹登录上线后在Honor 8X设备上运转的部分截图如下:

货拉拉专送司机Android指纹认证登录实践与总结
货拉拉专送司机Android指纹认证登录实践与总结
货拉拉专送司机Android指纹认证登录实践与总结
货拉拉专送司机Android指纹认证登录实践与总结

7. 总结

货拉拉专送司机端Android指纹登录上线以来,依据咱们的数据计算,有很多司机注册和运用了指纹登录,这种方便快捷的登录办法更受司机喜爱,一起也节约了咱们在短信验证码登录方面20%的本钱。咱们期望经过共享开发指纹登录进程中遇到的问题和处理方案,能够让更多的开发者从中受益,少走弯路,一起也欢迎读者和咱们评论交流。