Android 6.0指纹识别App开发案例
在android 6.0中google终于给android系统加上了指纹识别的支持,这个功能在iphone上早就已经实现了,并且在很多厂商的定制的rom中也都自己内部实现这个功能了,这个功能来的有点晚啊。在google全新发布的nexus设备:nexus 5x和nexus 6p中都携带了一颗指纹识别芯片在设备的背面,如下图(图片来自网络):
笔者手中的设备就是图上的那台黑色的nexus 5x,话说这台机器很是好看呢!手感超棒!
废话不多说,下面我出一个指纹识别的demo app,并且详细说明怎么开发一个基于google api的指纹识别app。demo的源码在我的github上:
https://github.com/createchance/androidfingerprintdemo
android m中的指纹识别接口
这个是首先需要关注的问题,在实际动手开始写app之前需要知道最新的平台为我们提供了那些指纹识别的接口。所有的指纹识别接口全部在android.hardware.fingerprint这个包下,这个包中的类不是很多,如下:
api doc链接地址:
大家最好fq自己看下。
上面的图中,我们看到这个包中总共有4个类,下面我们简要介绍一下他们:
1.fingerprintmanager:主要用来协调管理和访问指纹识别硬件设备
2.fingerprintmanager.authenticationcallback这个一个callback接口,当指纹认证后系统会回调这个接口通知app认证的结果是什么
3.fingerprintmanager.authenticationresult这是一个表示认证结果的类,会在回调接口中以参数给出
4.fingerprintmanager.cryptoobject这是一个加密的对象类,用来保证认证的安全性,这是一个重点,下面我们会分析。
好了,到这里我们简要知道了android 6.0给出的指纹识别的接口不是很多,可以说是简短干练。
动手开发一个指纹识别app
现在,我们要动手写一个利用上面接口的指纹识别app,这个app界面很简单,就一个activity,这个activity上会激活指纹识别,然后提示用户按下指纹,并且会将认证的结果显示出来。
开始
在开始之前,我们需要知道使用指纹识别硬件的基本步骤:
1.在androidmanifest.xml中申明如下权限:
<uses-permission android:name="android.permission.use_fingerprint"/>
2.获得fingerprintmanager的对象引用
3.在运行是检查设备指纹识别的兼容性,比如是否有指纹识别设备等。下面我们详细说一下上面的步骤:
申明权限
这一步比较简单,只要在androidmanifest.xml中添加上面说到的权限就可以了。
获得fingerprintmanager对象引用
这是app开发中获得系统服务对象的常用方式,如下:
// using the android support library v4 fingerprintmanager = fingerprintmanagercompat.from(this); // using api level 23: fingerprintmanager = (fingerprintmanager)getsystemservice(context.fingerprint_service);
上面给出两种方式,第一种是通过v4支持包获得兼容的对象引用,这是google推行的做法;还有就是直接使用api 23 framework中的接口获得对象引用。
检查运行条件
要使得我们的指纹识别app能够正常运行,有一些条件是必须满足的。
1). api level 23
指纹识别api是在api level 23也就是android 6.0中加入的,因此我们的app必须运行在这个系统版本之上。因此google推荐使用 android support library v4包来获得fingerprintmanagercompat对象,因为在获得的时候这个包会检查当前系统平台的版本。
2). 硬件
指纹识别肯定要求你的设备上有指纹识别的硬件,因此在运行时需要检查系统当中是不是有指纹识别的硬件:
if (!fingerprintmanager.ishardwaredetected()) { // no fingerprint sensor is detected, show dialog to tell user. alertdialog.builder builder = new alertdialog.builder(this); builder.settitle(r.string.no_sensor_dialog_title); builder.setmessage(r.string.no_sensor_dialog_message); builder.seticon(android.r.drawable.stat_sys_warning); builder.setcancelable(false); builder.setnegativebutton(r.string.cancel_btn_dialog, new dialoginterface.onclicklistener() { @override public void onclick(dialoginterface dialog, int which) { finish(); } }); // show this dialog. builder.create().show(); }
调用上面的接口接可以知道系统中是不是有一个这样的硬件,如果没有的话,那就需要做一些合适的事情,比如提示用户当前系统中没有指纹识别硬件等。
3). 当前设备必须是处于安全保护中的
这个条件的意思是,你的设备必须是使用屏幕锁保护的,这个屏幕锁可以是password,pin或者图案都行。为什么是这样呢?因为google原生的逻辑就是:想要使用指纹识别的话,必须首先使能屏幕锁才行,这个和android 5.0中的smart lock逻辑是一样的,这是因为google认为目前的指纹识别技术还是有不足之处,安全性还是不能和传统的方式比较的。
我们可以使用下面的代码检查当前设备是不是处于安全保护中的:
keyguardmanager keyguardmanager =(keyguardmanager)getsystemservice(context.keyguard_service); if (keyguardmanager.iskeyguardsecure()) { // this device is secure. }
我们使用keyguardmanager的iskeyguardsecure接口就能知道。
4). 系统中是不是有注册的指纹
在android 6.0中,普通app要想使用指纹识别功能的话,用户必须首先在setting中注册至少一个指纹才行,否则是不能使用的。所以这里我们需要检查当前系统中是不是已经有注册的指纹信息了:
if (!fingerprintmanager.hasenrolledfingerprints()) { // no fingerprint image has been enrolled. alertdialog.builder builder = new alertdialog.builder(this); builder.settitle(r.string.no_fingerprint_enrolled_dialog_title); builder.setmessage(r.string.no_fingerprint_enrolled_dialog_message); builder.seticon(android.r.drawable.stat_sys_warning); builder.setcancelable(false); builder.setnegativebutton(r.string.cancel_btn_dialog, new dialoginterface.onclicklistener() { @override public void onclick(dialoginterface dialog, int which) { finish(); } }); // show this dialog builder.create().show(); }
如果用户还没有注册一个指纹的话,那么我们的app可以提示用户:如果想要使用指纹是功能,请再setting中注册一个你的指纹。这里需要啰嗦一句,如果你做过bluetooth或者其他设备开发的话,那么你知道你可以通过发送一个intent来启动bluetooth开启的界面,只要是声明了蓝牙的管理权限。但是,到目前位置google任然没有开放让普通app启动指纹注册界面的权限,这一点我们可以从setting的androidmanifest中看到:
<activity android:name=".fingerprint.fingerprintsettings" android:exported="false"/> <activity android:name=".fingerprint.fingerprintenrollonboard" android:exported="false"/> <activity android:name=".fingerprint.fingerprintenrollfindsensor" android:exported="false"/> <activity android:name=".fingerprint.fingerprintenrollenrolling" android:exported="false"/> <activity android:name=".fingerprint.fingerprintenrollfinish" android:exported="false"/> <activity android:name=".fingerprint.fingerprintenrollintroduction" android:exported="false" /> <activity android:name=".fingerprint.setupfingerprintenrollonboard" android:exported="false"/> <activity android:name=".fingerprint.setupfingerprintenrollfindsensor" android:exported="false"/> <activity android:name=".fingerprint.setupfingerprintenrollenrolling" android:exported="false"/> <activity android:name=".fingerprint.setupfingerprintenrollfinish" android:exported="false"/> <activity android:name=".fingerprint.setupfingerprintenrollintroduction" android:exported="true" android:permission="android.permission.manage_fingerprint" android:theme="@style/setupwizarddisableappstartingtheme"> <intent-filter> <action android:name="android.settings.fingerprint_setup" /> <category android:name="android.intent.category.default" /> </intent-filter> </activity>
大部分的fingerprint设置界面都没有exporte,只有setupfingerprintenrollintroduction,但是这个界面需要android.permission.manage_fingerprint这个权限,并且这个权限只能是系统app使用,这就直接防止第三方app启动这个界面了。(不知道日后google会不会开放这个权限。。。。。)
一个好的app,应该在运行时都检查一下上面的条件,防止app出现意外的错误。
扫描用户按下的指纹
要开始扫描用户按下的指纹是很简单的,只要调用fingerprintmanager的authenticate方法即可,那么现在我们来看一下这个接口:
上图是google的api文档中的描述,现在我们挨个解释一下这些参数都是什么:
1. crypto这是一个加密类的对象,指纹扫描器会使用这个对象来判断认证结果的合法性。这个对象可以是null,但是这样的话,就意味这app无条件信任认证的结果,虽然从理论上这个过程可能被攻击,数据可以被篡改,这是app在这种情况下必须承担的风险。因此,建议这个参数不要置为null。这个类的实例化有点麻烦,主要使用javax的security接口实现,后面我的demo程序中会给出一个helper类,这个类封装内部实现的逻辑,开发者可以直接使用我的类简化实例化的过程。
2. cancel 这个是cancellationsignal类的一个对象,这个对象用来在指纹识别器扫描用户指纹的是时候取消当前的扫描操作,如果不取消的话,那么指纹扫描器会移植扫描直到超时(一般为30s,取决于具体的厂商实现),这样的话就会比较耗电。建议这个参数不要置为null。
3. flags 标识位,根据上图的文档描述,这个位暂时应该为0,这个标志位应该是保留将来使用的。
4. callback 这个是fingerprintmanager.authenticationcallback类的对象,这个是这个接口中除了第一个参数之外最重要的参数了。当系统完成了指纹认证过程(失败或者成功都会)后,会回调这个对象中的接口,通知app认证的结果。这个参数不能为null。
5. 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来实现上面说道的保护功能。
为了理解这些类之间是怎么协同工作的,这里我给出一个用于实例化cryptoobject对象的包装类代码,我们先看下这个代码是怎么实现的,然后再解释一下为什么是这样。
public class cryptoobjecthelper { // this can be key name you want. should be unique for the app. static final string key_name = "com.createchance.android.sample.fingerprint_authentication_key"; // we always use this keystore on android. static final string keystore_name = "androidkeystore"; // should be no need to change these values. static final string key_algorithm = keyproperties.key_algorithm_aes; static final string block_mode = keyproperties.block_mode_cbc; static final string encryption_padding = keyproperties.encryption_padding_pkcs7; static final string transformation = key_algorithm + "/" + block_mode + "/" + encryption_padding; final keystore _keystore; public cryptoobjecthelper() throws exception { _keystore = keystore.getinstance(keystore_name); _keystore.load(null); } public fingerprintmanagercompat.cryptoobject buildcryptoobject() throws exception { cipher cipher = createcipher(true); return new fingerprintmanagercompat.cryptoobject(cipher); } cipher createcipher(boolean retry) throws exception { key key = getkey(); cipher cipher = cipher.getinstance(transformation); try { cipher.init(cipher.encrypt_mode | cipher.decrypt_mode, key); } catch(keypermanentlyinvalidatedexception e) { _keystore.deleteentry(key_name); if(retry) { createcipher(false); } else { throw new exception("could not create the cipher for fingerprint authentication.", e); } } return cipher; } key getkey() throws exception { key secretkey; if(!_keystore.iskeyentry(key_name)) { createkey(); } secretkey = _keystore.getkey(key_name, null); return secretkey; } void createkey() throws exception { keygenerator keygen = keygenerator.getinstance(key_algorithm, keystore_name); keygenparameterspec keygenspec = new keygenparameterspec.builder(key_name, keyproperties.purpose_encrypt | keyproperties.purpose_decrypt) .setblockmodes(block_mode) .setencryptionpaddings(encryption_padding) .setuserauthenticationrequired(true) .build(); keygen.init(keygenspec); keygen.generatekey(); } }
上面的类会针对每个cryptoobject对象都会新建一个cipher对象,并且会使用由应用生成的key。这个key的名字是使用key_name变量定义的,这个名字应该是保证唯一的,建议使用域名区别。getkey方法会尝试使用android keystore的api来解析一个key(名字就是上面我们定义的),如果key不存在的话,那就调用createkey方法新建一个key。
cipher变量的实例化是通过调用cipher.getinstance方法获得的,这个方法接受一个transformation参数,这个参数制定了数据怎么加密和解密。然后调用cipher.init方法就会使用应用的key来完成cipher对象的实例化工作。
这里需要强调一点,在以下情况下,android会认为当前key是无效的:
1. 一个新的指纹image已经注册到系统中
2. 当前设备中的曾经注册过的指纹现在不存在了,可能是被全部删除了
3. 用户关闭了屏幕锁功能
4. 用户改变了屏幕锁的方式
当上面的情况发生的时候,cipher.init方法都会抛出keypermanentlyinvalidatedexception的异常,上面我的代码中捕获了这个异常,并且删除了当前无效的key,然后根据参数尝试再次创建。
上面的代码中使用了android的keygenerator来创建一个key并且把它存储在设备中。keygenerator类会创建一个key,但是需要一些原始数据才能创建key,这些原始的信息是通过keygenparameterspec类的对象来提供的。keygenerator类对象的实例化是使用它的工厂方法getinstance进行的,从上面的代码中我们可以看到这里使用的aes(advanced encryption standard )加密算法的,aes会将数据分成几个组,然后针对几个组进行加密。
接下来,keygenparameterspec的实例化是使用它的builder方法,keygenparameterspec.builder封装了以下重要的信息:
1. key的名字
2. key必须在加密和解密的时候是有效的
3. 上面代码中block_mode被设置为cipher block chaining也就是keyproperties.block_mode_cbc,这意味着每一个被aes切分的数据块都与之前的数据块进行了异或运算了,这样的目的就是为了建立每个数据块之间的依赖关系。
4. cryptoobjecthelper类使用了pksc7(public key cryptography standard #7)的方式去产生用于填充aes数据块的字节,这样就是要保证每个数据块的大小是等同的(因为需要异或计算还有方面算法进行数据处理,详细可以查看aes的算法原理)。
5. setuserauthenticationrequired(true)调用意味着在使用key之前用户的身份需要被认证。
每次keygenparameterspec创建的时候,他都被用来初始化keygenerator,这个对象会产生存储在设备上的key。
怎么使用cryptoobjecthelper呢?
下面我们看一下怎么使用cryptoobjecthelper这个类,我们直接看代码就知道了:
cryptoobjecthelper cryptoobjecthelper = new cryptoobjecthelper(); fingerprintmanager.authenticate(cryptoobjecthelper.buildcryptoobject(), 0, cancellationsignal, myauthcallback, null);
使用是比较简单的,首先new一个cryptoobjecthelper对象,然后调用buildcryptoobject方法就能得到cryptoobject对象了。
处理用户的指纹认证结果
前面我们分析authenticate接口的时候说道,调用这个接口的时候必须提供fingerprintmanager.authenticationcallback类的对象,这个对象会在指纹认证结束之后系统回调以通知app认证的结果的。在android 6.0中,指纹的扫描和认证都是在另外一个进程中完成(指纹系统服务)的,因此底层什么时候能够完成认证我们app是不能假设的。因此,我们只能采取异步的操作方式,也就是当系统底层完成的时候主动通知我们,通知的方式就是通过回调我们自己实现的fingerprintmanager.authenticationcallback类,这个类中定义了一些回调方法以供我们进行必要的处理:
下面我们简要介绍一下这些接口的含义:
1. onauthenticationerror(int errorcode, icharsequence errstring) 这个接口会再系统指纹认证出现不可恢复的错误的时候才会调用,并且参数errorcode就给出了错误码,标识了错误的原因。这个时候app能做的只能是提示用户重新尝试一遍。
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),因此app需要区别对待。
下面给出我的代码中实现的callback子类:
package com.createchance.fingerprintdemo; import android.os.handler; import android.support.v4.hardware.fingerprint.fingerprintmanagercompat; import javax.crypto.badpaddingexception; import javax.crypto.illegalblocksizeexception; /** * created by baniel on 7/21/16. */ public class myauthcallback extends fingerprintmanagercompat.authenticationcallback { private handler handler = null; public myauthcallback(handler handler) { super(); this.handler = handler; } @override public void onauthenticationerror(int errmsgid, charsequence errstring) { super.onauthenticationerror(errmsgid, errstring); if (handler != null) { handler.obtainmessage(mainactivity.msg_auth_error, errmsgid, 0).sendtotarget(); } } @override public void onauthenticationhelp(int helpmsgid, charsequence helpstring) { super.onauthenticationhelp(helpmsgid, helpstring); if (handler != null) { handler.obtainmessage(mainactivity.msg_auth_help, helpmsgid, 0).sendtotarget(); } } @override public void onauthenticationsucceeded(fingerprintmanagercompat.authenticationresult result) { super.onauthenticationsucceeded(result); try { result.getcryptoobject().getcipher().dofinal(); if (handler != null) { handler.obtainmessage(mainactivity.msg_auth_success).sendtotarget(); } } catch (illegalblocksizeexception e) { e.printstacktrace(); } catch (badpaddingexception e) { e.printstacktrace(); } } @override public void onauthenticationfailed() { super.onauthenticationfailed(); if (handler != null) { handler.obtainmessage(mainactivity.msg_auth_failed).sendtotarget(); } } }
这个子类实现很简单,主要的实现方式就是将消息抛给主界面的handler来处理:
handler = new handler() { @override public void handlemessage(message msg) { super.handlemessage(msg); log.d(tag, "msg: " + msg.what + " ,arg1: " + msg.arg1); switch (msg.what) { case msg_auth_success: setresultinfo(r.string.fingerprint_success); mcancelbtn.setenabled(false); mstartbtn.setenabled(true); cancellationsignal = null; break; case msg_auth_failed: setresultinfo(r.string.fingerprint_not_recognized); mcancelbtn.setenabled(false); mstartbtn.setenabled(true); cancellationsignal = null; break; case msg_auth_error: handleerrorcode(msg.arg1); break; case msg_auth_help: handlehelpcode(msg.arg1); break; } } };
这里分别处理四中回调,并且针对错误码调用handleerrorcode方法处理:
private void handleerrorcode(int code) { switch (code) { case fingerprintmanager.fingerprint_error_canceled: setresultinfo(r.string.errorcanceled_warning); break; case fingerprintmanager.fingerprint_error_hw_unavailable: setresultinfo(r.string.errorhwunavailable_warning); break; case fingerprintmanager.fingerprint_error_lockout: setresultinfo(r.string.errorlockout_warning); break; case fingerprintmanager.fingerprint_error_no_space: setresultinfo(r.string.errornospace_warning); break; case fingerprintmanager.fingerprint_error_timeout: setresultinfo(r.string.errortimeout_warning); break; case fingerprintmanager.fingerprint_error_unable_to_process: setresultinfo(r.string.errorunabletoprocess_warning); break; } }
很简单,就是针对不同的错误码,设置界面上不同的显示文字,以提示用户。这里大家可以很据自己的需要修改逻辑。
针对帮助码调用handlehelpcode方法处理:
private void handlehelpcode(int code) { switch (code) { case fingerprintmanager.fingerprint_acquired_good: setresultinfo(r.string.acquiredgood_warning); break; case fingerprintmanager.fingerprint_acquired_imager_dirty: setresultinfo(r.string.acquiredimagedirty_warning); break; case fingerprintmanager.fingerprint_acquired_insufficient: setresultinfo(r.string.acquiredinsufficient_warning); break; case fingerprintmanager.fingerprint_acquired_partial: setresultinfo(r.string.acquiredpartial_warning); break; case fingerprintmanager.fingerprint_acquired_too_fast: setresultinfo(r.string.acquiredtoofast_warning); break; case fingerprintmanager.fingerprint_acquired_too_slow: setresultinfo(r.string.acquiredtoslow_warning); break; } }
这里的处理和handleerrorcode是一样的。
总结
这里我们总计一下,android 6.0上的指纹识别开发的几个要点:
1. 建议使用android support library v4 compatibility api,不要使用直接framework中的api。
2. 在使用指纹硬件之前一定要检查上面提到的几个检查条件
3. 根据google的建议最好使用google提供的指纹是被icon来标示你的指纹识别界面:
这个做的目的就是为了很明确地提示用户这是一个指纹识别操作,就像人们看到蓝牙的那个小标识就知道这是蓝牙操作一样。当然,这只是google的一个实践性的建议,并非强制。
4. app需要及时通知用户当前的操作以及操作的结果,比如需要明确告诉用户当前正在扫描指纹,请把你的指纹放在传感器上等。
5. 最后需要注意的就是android support library v4中的fingerprintmanager类名字是fingerprintmanagercompat,并且他们的authenticate方法参数顺序不一样,flags和cancel的位置在两个类中是不一样的,这一点需要注意(个人觉得,这会不会是google的失误呢???嘿嘿。。。。。)
demo运行效果截图(运行于nexus 5x)
初始状态
扫描状态
扫描失败(出现可以恢复的错误,这里是手指移动太快)
认证失败
认证成功
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。