17、指纹认证与后台服务:在Service中使用指纹认证、前台服务与指纹、认证结果回调处理
说实话,很多开发者一提到指纹认证,第一反应就是「在Activity里弹个对话框」。这当然没错,但实际项目中,认证场景往往更复杂。比如你的App在后台下载文件,用户锁屏了,这时候想用指纹续个权——怎么办?
嗯,这就涉及到我们今天要聊的核心:在Service中使用指纹认证。
为什么要在Service里做指纹认证?
我遇到过这样一个需求:用户正在后台播放加密视频,每隔30分钟需要重新验证指纹。如果每次都要切回Activity,体验就很割裂。说白了,有些认证场景是「后台任务」的一部分,不应该依赖前台Activity的生命周期。
常见的场景包括:
- 后台下载加密文件,需要指纹授权
- VPN或安全服务在后台运行,定期验证用户身份
- 支付SDK在后台发起二次认证
- 锁屏后,后台服务需要重新获取密钥
核心结论:指纹认证的BiometricPrompt本身不依赖Activity,它只需要一个FragmentActivity或Context。但Service不是Activity的子类,所以我们需要一些「曲线救国」的方式。
方案一:前台Service + 悬浮认证窗口
我个人最推荐的方式,是把Service提升为前台Service,然后通过一个透明的Activity来承载BiometricPrompt。你可能会问:「这不还是Activity吗?」没错,但这个Activity只负责弹认证框,认证完就finish掉,用户几乎感知不到它的存在。
来看代码:
// 前台Service
public class FgBiometricService extends Service {
private static final int NOTIFICATION_ID = 1001;
private static final int REQUEST_CODE = 100;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 启动前台通知
startForeground(NOTIFICATION_ID, buildNotification());
// 启动透明Activity进行指纹认证
Intent authIntent = new Intent(this, BiometricAuthActivity.class);
authIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(authIntent);
return START_STICKY;
}
private Notification buildNotification() {
// 构建一个低优先级的通知
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("指纹认证服务运行中")
.setSmallIcon(android.R.drawable.ic_lock_lock)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build();
}
}
然后这个BiometricAuthActivity长这样:
public class BiometricAuthActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置透明主题,用户看不到界面
setContentView(R.layout.activity_transparent);
showBiometricPrompt();
}
private void showBiometricPrompt() {
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("后台服务验证")
.setSubtitle("请验证指纹以继续后台操作")
.setNegativeButtonText("取消")
.build();
BiometricPrompt biometricPrompt = new BiometricPrompt(this,
ContextCompat.getMainExecutor(this),
new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(
BiometricPrompt.AuthenticationResult result) {
// 认证成功,通知Service继续工作
Intent intent = new Intent("ACTION_AUTH_SUCCESS");
sendBroadcast(intent);
finish();
}
@Override
public void onAuthenticationError(int errorCode,
CharSequence errString) {
// 认证失败或取消
Intent intent = new Intent("ACTION_AUTH_FAILED");
intent.putExtra("error", errString);
sendBroadcast(intent);
finish();
}
});
biometricPrompt.authenticate(promptInfo);
}
}
我的经验:这个透明Activity的主题一定要设置成Theme.Translucent.NoTitleBar,否则会闪一下白屏。我曾经因为这个被QA提了bug,后来改成透明主题就解决了。
方案二:直接使用BiometricManager + 回调
如果你不想启动Activity,也可以直接用BiometricManager来判断设备是否支持指纹,然后自己管理认证流程。但说实话,BiometricPrompt的UI是系统级的,你绕不开它。所以这个方案只适合「检查是否支持」的场景,不适合真正的认证。
// 在Service中检查是否支持
BiometricManager manager = BiometricManager.from(this);
switch (manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
case BiometricManager.BIOMETRIC_SUCCESS:
Log.d("Service", "指纹可用");
break;
case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
Log.e("Service", "没有指纹硬件");
break;
case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
Log.e("Service", "没有录入指纹");
break;
}
认证结果回调处理
这里有个容易被忽略的点:回调的线程。BiometricPrompt的callback默认在主线程执行,但你的Service可能需要在子线程处理后续逻辑。我建议在回调里用Handler或LiveData把结果抛到合适的线程。
我习惯的做法是:
- 在Service里定义一个
ResultCallback接口 - 认证成功时,通过Broadcast或Binder通知Service
- Service收到通知后,在后台线程执行后续操作
// Service中的回调接口
public interface AuthResultListener {
void onSuccess(CryptoObject cryptoObject);
void onError(int errorCode, String message);
}
// 在BiometricAuthActivity中通过Binder注册监听
// 或者更简单:用LocalBroadcastManager
LocalBroadcastManager.getInstance(this)
.registerReceiver(authReceiver,
new IntentFilter("ACTION_AUTH_SUCCESS"));
注意:千万不要在Service的onStartCommand里直接调用biometricPrompt.authenticate(),因为Service没有UI上下文,系统会直接报错。必须通过Activity或Dialog来承载。
知识体系图
避坑指南
我曾经踩过一个坑:在Service里直接new BiometricPrompt,传了个ApplicationContext进去。结果认证弹窗死活不弹,Logcat里报Context must be an Activity。后来查文档才发现,BiometricPrompt的构造函数要求的是FragmentActivity或Activity,不能是Service或Application。
另一个坑是前台通知的优先级。如果你的通知优先级太高,用户锁屏时会看到一条「指纹认证服务运行中」的通知,体验很糟糕。我建议把优先级设为PRIORITY_LOW或PRIORITY_MIN,这样通知会折叠起来,不会打扰用户。
小技巧:如果用户取消认证,记得在onAuthenticationError里把Service也停掉,或者降级为普通Service。否则前台通知会一直挂着,用户会觉得你的App有问题。
总结一下
在Service里做指纹认证,说白了就是「借壳认证」——用前台Service保活,用透明Activity弹窗,用Broadcast传结果。这个模式我在好几个项目里都用过,稳定可靠。
嗯,核心就三点:
- 前台Service保证后台任务不被杀
- 透明Activity承载BiometricPrompt
- 广播或Binder把认证结果传回Service
你想想看,如果用户正在后台下载重要文件,突然弹个指纹框让他验证,验证完继续下载——整个过程用户都不用离开当前App,体验是不是好很多?