Android指纹识别功能的接入

在指纹识别手机没有大规模普及之前,许多app提供了使用手势密码进行快速登录或解锁的功能。但是在指纹识别大规模普及的今天,安卓的许多app还是仅支持收拾密码登陆功能,一方面是出于安全性的考量,另一方面是出于对适配兼容的担忧。一但用户进行了Root的操作,那么指纹识别的结果将变得不可信。

最近在寻找市面上常用的密码本软件的时候,发现大部分指纹密码软件支持通过生物识别解锁app。也许是指纹识别的兼容支持变好了?查阅资料发现,安卓对指纹识别的功能进行过封装,使用FingerprintManagerCompat可以轻松对指纹识别进行对接操作。身边8台不同品牌和版本的测试机,都可以正常的使用指纹验证功能。

但是由于Android统一封装指纹功能是在Android6.0后才加入的,这里也只对6.0以上的版本进行指纹识别功能适配。

那么来看一下指纹识别该如何去实现和接入吧。这里为了方便接入,讲指纹识别的全部功能,封装到了一个Dialog中,当打开次dialog时,就可以进行指纹识别了。


首先在AndroidManifest.xml中添加权限。Android在9.0时,开始不推荐使用FingerprintManager,开放了新的API,BiometricPrompt
但是我们仍可以使用FingerprintManagerCompat对指纹识别进行接入。

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

接下来编写一个简单的指纹验证Dialog布局,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFFFFF"
android:orientation="vertical">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:src="@drawable/fingerprint"/>

<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:textSize="16sp"
android:textColor="#000000"
android:text="请验证指纹解锁" />

<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#339CA6B1" />

<TextView
android:id="@+id/tv_cancel"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:text="取消"
android:textColor="#177FFF"
android:textSize="16sp" />
</LinearLayout>

接下来可以编写Dialog了,全部的代码放在最后,这里只截取关键的部分来说明。

这里继承使用DialogFragment,因为DialogFragment拥有和Fragment一样的生命周期管理能力,便于资源的处理和释放。

首先获取实例对象,并判断设备是否支持指纹识别功能和是否开启了(录入了)指纹。如果系统版本在6.0以下,就直接返回不支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FingerprintManagerCompat mCompat = FingerprintManagerCompat.from(getContext());

//判断硬件是否支持指纹
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false;
} else {
return mCompat != null && mCompat.isHardwareDetected();
}

//判断用户是否开启指纹识别
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false;
} else {
return mCompat != null && mCompat.hasEnrolledFingerprints();
}

之后就可以开始验证了,看一下验证方法authenticate需要的参数:

  • crypto: 需要传入一个加密对象,用于对指纹识别结果进行加密,如果传null,表示无条件相信返回的结果。推荐使用加密,避免指纹识别结果被篡改
  • flags: 标志位,默认传0即可
  • cancel: 用于处理取消指纹识别后的行为
  • callback: 提供了指纹识别完毕后的回调方法,包括成功、失败、异常错误
  • handler: 默认会使用主线程的Handler,传null即可

其中特殊强调一下第一个参数,crypto的作用是,验证系统返回的指纹识别结果是否被修改过,这里使用了非对称加密,具体的内容可以参考之前的“Android AES加解密”那篇文章,里面对加密相关的类型和参数有详细的讲解。

具体的逻辑也是大同小异,先生成KeyStore,然后通过KeyStore生成Cipher对象,最后通过FingerprintManagerCompat.CryptoObject生成CryptoObject。

最后将参数传入方法,就可以实现指纹识别功能了。全部代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
public class FingerprintDialog extends DialogFragment {

private static final String DEFAULT_KEY_NAME = "default_key";
private FingerprintManagerCompat mCompat;
private CancellationSignal mCancellationSignal;
private FingerprintManagerCompat.CryptoObject cryptoObject = null;
private FingerPrintResult mResult;

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_finger_test, container, false);
mCompat = FingerprintManagerCompat.from(getContext());

TextView tvCancel = view.findViewById(R.id.tv_cancel);
tvCancel.setOnClickListener(v -> dismiss());

init();
return view;
}

private void init() {
if (!hasDeviceAllow()) {
Toast.makeText(getContext(), "暂不支持此设备", Toast.LENGTH_SHORT).show();
dismiss();
return;
}
if (!hasOpenFingerprints()) {
Toast.makeText(getContext(), "此设备未设置指纹", Toast.LENGTH_SHORT).show();
dismiss();
return;
}
initKey();
authenticate();
}

public boolean hasDeviceAllow() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false;
} else {
return mCompat != null && mCompat.isHardwareDetected();
}
}

public boolean hasOpenFingerprints() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false;
} else {
return mCompat != null && mCompat.hasEnrolledFingerprints();
}
}

private void initKey() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
}
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
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();
initCipher(keyStore);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

@TargetApi(23)
private void initCipher(KeyStore keyStore) {
try {
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);
cryptoObject = new FingerprintManagerCompat.CryptoObject(cipher);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public void authenticate() {
mCancellationSignal = new CancellationSignal();
mCompat.authenticate(cryptoObject, 0, mCancellationSignal, new FingerprintManagerCompat.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
super.onAuthenticationError(errMsgId, errString);
Toast.makeText(getContext(), "指纹识别出现错误", Toast.LENGTH_SHORT).show();
dismiss();
}

@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
Toast.makeText(getContext(), "指纹识别成功", Toast.LENGTH_SHORT).show();
//处理成功之后的回调
mResult.onSuccess();
dismiss();
}

@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
Toast.makeText(getContext(), "指纹识别失败", Toast.LENGTH_SHORT).show();
}
}, null);
}

public interface FingerPrintResult{
void onSuccess();
}

public void setResult(FingerPrintResult mResult) {
this.mResult = mResult;
}
}
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×