参考文献:android developer biometric

截止发稿时需要的依赖

implementation 'androidx.biometric:biometric:1.2.0-alpha04'

修改gradle支持viewBinding

 buildFeatures {
     viewBinding true
 }

在manifest中添加权限

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

1 简化版(建议使用)

android是不能取到用户的指纹图像的.
rsa是非对称加密,因此只有公钥是无法解密数据的
密钥在互联网上传播是不安全的,因此直接采用android Cipher加解密数据后与服务器通信不太安全.你可以采用以下几个步骤:

1.1 在服务器注册消息密文(开通生物识别功能)

  • 1.首先android必须登录服务器获取token
  • 2.注册指纹前android根据token(有效的token)请求服务器返回一个RAS公钥(注意要在服务器上检查RAS有效期,一般为请求至验证成功设置为1分钟)
  • 3.android验证指纹成功后在onAuthenticationSucceeded方法中随机生成一个32字节的待加密数据(范围:大小写字母和数据)
  • 4.android端永久保存待加密数据(明文),除非用户再次注册后覆盖
  • 5.在onAuthenticationSucceeded方法使用服务器返回的RAS公钥加密数据,然后将加密后的数据发送至服务器,服务器永久保存待加密数据(明文),除非用户再次注册后覆盖

1.2 使用指纹登录

  • 1.需要使用生物识别登录服务器或在服务器上验证信息前,根据token(token不管是否过期)先从服务器获取RAS公钥(注意要在服务器上检查RAS有效期,一般为请求至验证成功设置为1分钟)
  • 2.生物识别验证成功后将永久保存待加密数据(明文)使用RSA公钥加密后将密文和token发送到服务器
  • 3.服务器在检查token时不管是否过期,只需要将使用当前token的用户和RAS挂钩
  • 4.服务器根据token找到RAS私钥后解密密文,比较数据是否相同,相同则予以通过
/**
 *    生物识别简化版
 *    https://developer.android.com/training/sign-in/biometric-auth
 *
 *    需要添加下面的依赖
 *    implementation 'androidx.biometric:biometric:1.2.0-alpha04'
 */
package cn.kuncb.photograph.activity.biometric;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.widget.Toast;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;

import cn.kuncb.photograph.R;
import cn.kuncb.photograph.databinding.ActivityBiometricEasyBinding;

public class BiometricEasyActivity extends AppCompatActivity implements View.OnClickListener {

    private ActivityBiometricEasyBinding mViewBind;
    //region 生物识别
    private BiometricPrompt mBiometricPrompt;
    private ActivityResultLauncher<Intent> mBiometricLauncher; //提示添加生物识别
    //endregion

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.mViewBind = ActivityBiometricEasyBinding.inflate(getLayoutInflater());
        setContentView(this.mViewBind.getRoot());
        //region TODO:生物识别回调
        this.mBiometricLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
            try {
                if (Activity.RESULT_OK == result.getResultCode()) { //设置生物识别成功
                    initBiometricPrompt();
                } else {
                    Toast.makeText(this, "请使用用户名或手机号与密码登录.", Toast.LENGTH_LONG).show();
                }
            } catch (Exception e) {
                Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
                e.printStackTrace();
            }
        });
        //endregion
        //region TODO:检查是否支持生物识别
        BiometricManager biometricManager = BiometricManager.from(this);
        switch (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
            case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:        //用户未注册生物信息
                do {
                    Toast.makeText(this, "您还未注册生物识别信息,请输入屏幕解锁密码后在系统中注册您的生物识别信息.", Toast.LENGTH_LONG).show();
                    final Intent intent = new Intent(Settings.ACTION_BIOMETRIC_ENROLL);
                    intent.putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, BiometricManager.Authenticators.BIOMETRIC_STRONG);
                    this.mBiometricLauncher.launch(intent);
                } while (false);
                break;
            case BiometricManager.BIOMETRIC_SUCCESS:                    //成功
                do {
                    initBiometricPrompt();
                } while (false);
                break;
            case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:         //硬件不支持
            case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE:      //无法进行身份验证,因为硬件不可用.请稍后再试
                break;
        }
        //endregion
    }

    //region TODO:生物识别
    private void initBiometricPrompt() {
        this.mBiometricPrompt = new BiometricPrompt(this, ContextCompat.getMainExecutor(this), new BiometricPrompt.AuthenticationCallback() {
            @Override
            public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { //验证时发生异常
                super.onAuthenticationError(errorCode, errString);
                Toast.makeText(BiometricEasyActivity.this, String.format("%s.%s", errString, "请使用用户名或手机号与密码登录."), Toast.LENGTH_LONG).show();
            }

            @Override
            public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { //验证成功
                super.onAuthenticationSucceeded(result);
                try {
                    /*
                    密钥在互联网上传播是不安全的,因此直接采用android Cipher加解密数据后与服务器通信不太安全.你可以采用以下几个步骤:
                    1.需要使用生物识别登录服务器或在服务器上验证信息前,先从服务器获取一个随机字符串和RAS公钥
                    2.生物识别验证成功后将随机字符串用RSA公钥加密后将密文发送到服务器
                    3.服务器使用自己保存的RAS私钥解密密文后,比较随机字符串是否相同,相同则予以通过
                    */
                } catch (Exception e) {
                    e.printStackTrace();
                    Toast.makeText(BiometricEasyActivity.this, e.getMessage(), Toast.LENGTH_LONG).show();
                }
            }

            @Override
            public void onAuthenticationFailed() { //验证指纹失败,如失败次数超过5次,则点击button时系统会提示稍后再试,我们不需要任何处理
                super.onAuthenticationFailed();
                Toast.makeText(BiometricEasyActivity.this, String.format("指纹验证失败.%s", "请使用用户名或手机号与密码登录."), Toast.LENGTH_LONG).show();
            }
        });

        this.mViewBind.btnBiometric.setEnabled(true);
        this.mViewBind.btnBiometric.setOnClickListener(this);
    }
    //endregion

    @Override
    public void onClick(View v) {
        try {
            switch (v.getId()) {
                case R.id.btnBiometric:
                    onBiometric();
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
        }
    }

    //region TODO:生物识别
    private void onBiometric() {
        BiometricPrompt.PromptInfo biometricPromptInfo = new BiometricPrompt.PromptInfo.Builder()
                .setTitle("指纹登录")
                .setSubtitle("使用您在Android系统中已经登记的指纹登录本系统")
                .setNegativeButtonText("取消")
                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
                .build();
        this.mBiometricPrompt.authenticate(biometricPromptInfo);
    }
    //endregion
}

2 完成版

  • 存在的问题是使用指纹加解密后密钥与服务器之间的通信不太安全.
  • 要注意处理user not authenticated异常
/**
 *    生物识别完整版
 *    https://developer.android.com/training/sign-in/biometric-auth
 *
 *    需要添加下面的依赖
 *    implementation 'androidx.biometric:biometric:1.2.0-alpha04'
 */
package cn.kuncb.photograph.activity.biometric;

import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.security.keystore.UserNotAuthenticatedException;
import android.view.View;
import android.widget.Toast;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

import cn.kuncb.photograph.R;
import cn.kuncb.photograph.databinding.ActivityBiometricBinding;

public class BiometricActivity extends AppCompatActivity implements View.OnClickListener {

    ActivityBiometricBinding mViewBind;
    //region 指纹识别
    private static final String KEY_STORE_ALIAS = "KEY_STORE_ALIAS";

    private BiometricPrompt mBiometricPrompt;
    private BiometricPrompt.PromptInfo mBiometricPromptInfo;
    private SecretKey mSecretKey;
    private Cipher mCipher;
    private ActivityResultLauncher<Intent> mConfirmDeviceCredentialLauncher; //确认设备凭据,处理Cipher.init抛出的UserNotAuthenticatedException,异常消息:user not authenticated
    private ActivityResultLauncher<Intent> mBiometricLauncher; //提示添加生物识别
    //endregion

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.mViewBind = ActivityBiometricBinding.inflate(getLayoutInflater());
        setContentView(this.mViewBind.getRoot());
        //region TODO:生物识别回调
        this.mBiometricLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
            try {
                if (Activity.RESULT_OK == result.getResultCode()) { //设置生物识别成功
                    initBiometricPrompt();
                } else {
                    Toast.makeText(this, "请使用用户名或手机号与密码登录.", Toast.LENGTH_LONG).show();
                }
            } catch (Exception e) {
                Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
                e.printStackTrace();
            }
        });
        this.mConfirmDeviceCredentialLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
            try {
                if (Activity.RESULT_OK == result.getResultCode()) {
                    this.mCipher.init(Cipher.ENCRYPT_MODE, this.mSecretKey); //密钥要求30秒验证完成,否则密钥失效
                    this.mBiometricPrompt.authenticate(this.mBiometricPromptInfo, new BiometricPrompt.CryptoObject(this.mCipher));
                } else {
                    Toast.makeText(this, "请使用用户名或手机号与密码登录.", Toast.LENGTH_LONG).show();
                }
            } catch (Exception e) {
                Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
                e.printStackTrace();
            }
        });
        //endregion
        //region TODO:检查是否支持生物识别
        BiometricManager biometricManager = BiometricManager.from(this);
        switch (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
            case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:        //用户未注册生物信息
                do {
                    Toast.makeText(this, "您还未注册生物识别信息,请输入屏幕解锁密码后在系统中注册您的生物识别信息.", Toast.LENGTH_LONG).show();
                    final Intent intent = new Intent(Settings.ACTION_BIOMETRIC_ENROLL);
                    intent.putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, BiometricManager.Authenticators.BIOMETRIC_STRONG);
                    this.mBiometricLauncher.launch(intent);
                } while (false);
                break;
            case BiometricManager.BIOMETRIC_SUCCESS:                    //成功
                do {
                    initBiometricPrompt();
                } while (false);
                break;
            case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:         //硬件不支持
            case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE:      //无法进行身份验证,因为硬件不可用.请稍后再试
                break;
        }
        //endregion
    }

    //region TODO:生物识别
    private void initBiometricPrompt() {
        this.mBiometricPrompt = new BiometricPrompt(this, ContextCompat.getMainExecutor(this), new BiometricPrompt.AuthenticationCallback() {
            @Override
            public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
                super.onAuthenticationError(errorCode, errString);
                Toast.makeText(BiometricActivity.this, String.format("%s.%s", errString, "请使用用户名或手机号与密码登录."), Toast.LENGTH_LONG).show();
            }

            @Override
            public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
                super.onAuthenticationSucceeded(result);
                try {
                    byte[] data = "要加密的信息".getBytes(StandardCharsets.UTF_8);
                    Cipher cipher = result.getCryptoObject().getCipher();
                    byte[] encode = cipher.doFinal(data);
                } catch (Exception e) {
                    e.printStackTrace();
                    Toast.makeText(BiometricActivity.this, e.getMessage(), Toast.LENGTH_LONG).show();
                }
            }

            @Override
            public void onAuthenticationFailed() {
                super.onAuthenticationFailed();
                Toast.makeText(BiometricActivity.this, String.format("指纹验证失败.%s", "请使用用户名或手机号与密码登录."), Toast.LENGTH_LONG).show();
            }
        });

        this.mViewBind.btnBiometric.setEnabled(true);
        this.mViewBind.btnBiometric.setOnClickListener(this);
    }
    //endregion


    @Override
    public void onClick(View v) {
        try {
            switch (v.getId()) {
                case R.id.btnBiometric:
                    onBiometric();
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
        }
    }

    //region TODO:生物识别
    private void generateSecretKey() throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
        KeyGenParameterSpec.Builder keyGenParameterSpecBuilder = new KeyGenParameterSpec.Builder(
                KEY_STORE_ALIAS,
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                .setKeySize(256)
                .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
                .setUserAuthenticationRequired(true)
                // Invalidate the keys if the user has registered a new biometric
                // credential, such as a new fingerprint. Can call this method only
                // on Android 7.0 (API level 24) or higher. The variable
                // "invalidatedByBiometricEnrollment" is true by default.
                .setInvalidatedByBiometricEnrollment(true);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
            keyGenParameterSpecBuilder.setUserAuthenticationParameters(30, KeyProperties.AUTH_BIOMETRIC_STRONG); //设置在成功对用户进行身份验证后授权使用此密钥的持续时间(秒)和授权类型
        else
            keyGenParameterSpecBuilder.setUserAuthenticationValidityDurationSeconds(30);

        KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");

        keyGenerator.init(keyGenParameterSpecBuilder.build());
        SecretKey secretKey = keyGenerator.generateKey();
    }

    private SecretKey getSecretKey() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException, UnrecoverableKeyException {
        KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        // Before the keystore can be accessed, it must be loaded.
        keyStore.load(null);
        return ((SecretKey) keyStore.getKey(KEY_STORE_ALIAS, null));
    }

    private Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
        return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
                + KeyProperties.BLOCK_MODE_CBC + "/"
                + KeyProperties.ENCRYPTION_PADDING_PKCS7);
    }

    private void onBiometric()
            throws NoSuchPaddingException, NoSuchAlgorithmException, UnrecoverableKeyException, CertificateException, KeyStoreException, IOException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchProviderException {
        generateSecretKey();
        this.mCipher = getCipher();
        this.mSecretKey = getSecretKey();
        this.mBiometricPromptInfo = new BiometricPrompt.PromptInfo.Builder()
                .setTitle("指纹登录")
                .setSubtitle("使用您在Android系统中已经登记的指纹登录本系统")
                .setNegativeButtonText("取消")
                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
                .build();
        try {
            this.mCipher.init(Cipher.ENCRYPT_MODE, this.mSecretKey); //这里可能会报user not authenticated异常
            this.mBiometricPrompt.authenticate(this.mBiometricPromptInfo, new BiometricPrompt.CryptoObject(this.mCipher));
        } catch (UserNotAuthenticatedException e) {
            KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
            Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(null, null); //创建设备凭据
            this.mConfirmDeviceCredentialLauncher.launch(intent);
        }
    }
    //endregion
}

3 Lyaout

两个版本的布局都是一样的

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"">

    <Button
        android:id="@+id/btnBiometric"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:enabled="false"
        android:text="生物识别" />
</androidx.appcompat.widget.LinearLayoutCompat>
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐