参考文献: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>