You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android使用SafetyNet检测Root设备重启后触发CANCELLED错误如何解决

问题描述

我需要在应用的登录流程启动前完成设备Root状态检测,参考相关技术方案后,我选择使用SafetyNet实现该功能,调整后的代码如下:

public class RootedDeviceCheckWithSafetyNetAttestation {
    public static AtomicInteger sendSafetyNetRequest(Activity context) {

        AtomicInteger rootCheck = new AtomicInteger();

        if(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS) {
            Log.e(TAG, "The SafetyNet Attestation API is available");

            // TODO(开发者): 调整nonce生成逻辑,加入自定义的一次性值,建议从远端服务器获取
            String nonceData = "Safety Net Sample: " + System.currentTimeMillis();
            ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
            Random mRandom = new SecureRandom();

            byte[] bytes = new byte[24];
            mRandom.nextBytes(bytes);
            try {
                byteStream.write(bytes);
                byteStream.write(nonceData.getBytes());
            } catch (IOException e) {
                e.printStackTrace();
            }

            byte[] nonce = byteStream.toByteArray();

            SafetyNetClient client = SafetyNet.getClient(context);
            Task<SafetyNetApi.AttestationResponse> task = client.attest(nonce, MY-API-KEY);

            task.addOnSuccessListener(context, attestationResponse -> {
                /*
                       TODO(开发者): 将该结果和nonce一起发送到自己的服务端进行校验
                       也可以在本地解析JwsResult确认API返回了响应,先检查是否存在error字段,再考虑指数退避重试
                       注意:不要仅依赖本地客户端侧的校验结果,必须在远端服务端校验响应保障安全
                      */

                String jwsResult = attestationResponse.getJwsResult();

                Log.e(TAG, "Success! SafetyNet result:\n" + jwsResult + "\n");

                if (jwsResult == null) {
                    Log.e(TAG, "jwsResult Null");
                }
                final String[] jwtParts = jwsResult.split("\\.");

                if (jwtParts.length == 3) {
                    String decodedPayload = new String(Base64.decode(jwtParts[1], Base64.DEFAULT));
                    try {
                        JSONObject jsonObject = new JSONObject(decodedPayload);
                        //Toast.makeText(context, "ctsProfileMatch:" + jsonObject.getString("ctsProfileMatch")+","+"basicIntegrity:" + jsonObject.getString("basicIntegrity"), Toast.LENGTH_SHORT).show();
                        if((jsonObject.getString("ctsProfileMatch").equals("false")) && (jsonObject.getString("basicIntegrity").equals("false"))){
                            rootCheck.set(4);
                        }
                        else{
                            rootCheck.set(1);
                        }
                    } catch (JSONException e) {
                        e.printStackTrace();
                        rootCheck.set(2);
                    }
                    //Toast.makeText(context, decodedPayload, Toast.LENGTH_SHORT).show();
                    //Log.e(TAG, "decodedPayload :     " + decodedPayload);
                }
            });

            task.addOnFailureListener(context, e -> {
                // 与服务通信时发生错误
                String mResult = null;
                rootCheck.set(5);

                if (e instanceof ApiException) {
                    // Google Play服务API错误,包含额外错误信息
                    ApiException apiException = (ApiException) e;

                    Log.e(TAG, "Error: " +
                            CommonStatusCodes.getStatusCodeString(apiException.getStatusCode()) + ": " +
                            apiException.getStatusMessage());
                    Toast.makeText(context, "Error: " +
                            CommonStatusCodes.getStatusCodeString(apiException.getStatusCode()) + ": " +
                            apiException.getStatusMessage(), Toast.LENGTH_LONG).show();
                } else {
                    // 其他未知类型错误
                    Log.e(TAG, "ERROR! " + e.getMessage());
                    Toast.makeText(context, "ERROR! " + e.getMessage(), Toast.LENGTH_LONG).show();
                }
            });

        } else {
            Log.e(TAG, "提示用户更新Google Play服务");
            rootCheck.set(3);
        }
        return rootCheck;
    }
}

我在模拟器或者Root设备上测试时,rootCheck变量可以按预期被赋值为4。
但在真机上测试时发现:首次登录时Root检测功能正常,但将应用杀后台后重新打开,代码就会弹出“Error: CANCELLED: null”的Toast提示,rootCheck变量被赋值为5,说明请求进入了task.addOnFailureListener的if (e instanceof ApiException)分支,该问题在Root和未Root的真机上都稳定复现。
请问该问题的诱因是什么,该如何解决?


问题诱因

  • 你在调用addOnSuccessListener和addOnFailureListener时传入了当前Activity实例作为生命周期所有者。当应用被后台杀死后重新打开,旧的Activity实例已经被系统销毁,Google Play服务的Task框架会自动取消所有绑定了已销毁生命周期所有者的待执行请求,直接触发失败回调返回CANCELLED状态码。
  • 部分真机的Google Play服务对短时间内重复发起的SafetyNet请求有频率限制,首次请求成功后短时间内再次发起请求,可能被服务端直接拒绝返回取消状态。
  • 你当前使用的SafetyNet Attestation API已经正式废弃,官方已停止维护,部分新机型和新版本Google Play服务对旧API的适配存在兼容性问题,也会导致请求被取消。

解决方案

1. 调整监听器绑定逻辑

移除监听器的Activity生命周期绑定,改用无生命周期参数的监听器重载方法,避免请求被自动取消。
修改前:

task.addOnSuccessListener(context, attestationResponse -> { ... })
task.addOnFailureListener(context, e -> { ... })

修改后:

task.addOnSuccessListener(attestationResponse -> { ... })
task.addOnFailureListener(e -> { ... })

如果担心内存泄漏,可以在Activity的onDestroy生命周期中手动调用task.cancel(true)取消未完成的请求。

2. 增加请求重试机制

捕获到状态码为CANCELLED的ApiException时,增加指数退避重试逻辑,最多重试2-3次,避开服务端频率限制。

3. 修复异步结果读取逻辑

你当前用同步方法返回AtomicInteger的写法存在逻辑漏洞,SafetyNet请求是异步执行的,方法返回时回调大概率还未触发,拿到的会是初始值0。建议改用接口回调的方式传递检测结果,示例如下:

public interface RootCheckCallback {
    void onCheckResult(int resultCode);
}

public static void sendSafetyNetRequest(Activity context, RootCheckCallback callback) {
    // 原有逻辑
    // 在回调的各个分支调用 callback.onCheckResult(对应code)即可
}

4. 替换为Play Integrity API

SafetyNet Attestation API已在2024年完全停用,官方建议替换为Play Integrity API,兼容性、稳定性和安全性都高于旧API,可彻底规避旧API的适配问题。


内容的提问来源于stack exchange,提问作者Wicaledon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 19:48:01