17、指纹认证与后台服务:在Service中使用指纹认证、前台服务与指纹、认证结果回调处理

说实话,很多开发者一提到指纹认证,第一反应就是「在Activity里弹个对话框」。这当然没错,但实际项目中,认证场景往往更复杂。比如你的App在后台下载文件,用户锁屏了,这时候想用指纹续个权——怎么办?

嗯,这就涉及到我们今天要聊的核心:在Service中使用指纹认证

为什么要在Service里做指纹认证?

我遇到过这样一个需求:用户正在后台播放加密视频,每隔30分钟需要重新验证指纹。如果每次都要切回Activity,体验就很割裂。说白了,有些认证场景是「后台任务」的一部分,不应该依赖前台Activity的生命周期。

常见的场景包括:

  • 后台下载加密文件,需要指纹授权
  • VPN或安全服务在后台运行,定期验证用户身份
  • 支付SDK在后台发起二次认证
  • 锁屏后,后台服务需要重新获取密钥

核心结论:指纹认证的BiometricPrompt本身不依赖Activity,它只需要一个FragmentActivityContext。但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把结果抛到合适的线程。

我习惯的做法是:

  1. 在Service里定义一个ResultCallback接口
  2. 认证成功时,通过Broadcast或Binder通知Service
  3. 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中指纹认证核心流程 前台Service 启动透明Activity BiometricPrompt 认证弹窗 ✅ 认证成功 → 发广播 ❌ 认证失败 → 发广播

避坑指南

我曾经踩过一个坑:在Service里直接new BiometricPrompt,传了个ApplicationContext进去。结果认证弹窗死活不弹,Logcat里报Context must be an Activity。后来查文档才发现,BiometricPrompt的构造函数要求的是FragmentActivityActivity,不能是Service或Application。

另一个坑是前台通知的优先级。如果你的通知优先级太高,用户锁屏时会看到一条「指纹认证服务运行中」的通知,体验很糟糕。我建议把优先级设为PRIORITY_LOWPRIORITY_MIN,这样通知会折叠起来,不会打扰用户。

小技巧:如果用户取消认证,记得在onAuthenticationError里把Service也停掉,或者降级为普通Service。否则前台通知会一直挂着,用户会觉得你的App有问题。

总结一下

在Service里做指纹认证,说白了就是「借壳认证」——用前台Service保活,用透明Activity弹窗,用Broadcast传结果。这个模式我在好几个项目里都用过,稳定可靠。

嗯,核心就三点:

  • 前台Service保证后台任务不被杀
  • 透明Activity承载BiometricPrompt
  • 广播或Binder把认证结果传回Service

你想想看,如果用户正在后台下载重要文件,突然弹个指纹框让他验证,验证完继续下载——整个过程用户都不用离开当前App,体验是不是好很多?

公众号:蓝海资料掘金营,微信deep3321