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

Android Studio Java项目集成Play Integrity API及服务端验证问询

Play Integrity API集成与验证问题解决

已完成的操作

  • 将Firebase项目与Google Console应用关联
  • 在Google Console和Google Cloud中启用Play Integrity API

当前需求

  • 在Android Studio的Java项目中正确集成Play Integrity API
  • 实现Node.js服务端的请求验证逻辑

遇到的问题

测试发布版本时设备可正常访问,但发布应用时收到警告:此版本包含未受Google Play保护的工件,您的应用可能易遭篡改和重新分发,说明Play Integrity API未正确生效。

现有代码问题分析

你提供的代码存在多处安全和逻辑错误,这是导致警告的核心原因:

public class PlayIntegrityApi {
    private final String TAG = "PlayIntegrityApi";
    private final Context context;
    private long timeRequest;
    boolean isAppOk = true;

    public PlayIntegrityApi(Context context) {
        this.context = context;
    }

    public boolean IsLicensedApp() {

        byte[] strBytes = android.util.Base64.decode(PLAY_INTEGRITY_SECRET, android.util.Base64.DEFAULT);
        byte[] encoded = android.util.Base64.encode(
                strBytes, android.util.Base64.URL_SAFE | android.util.Base64.NO_PADDING | android.util.Base64.NO_WRAP);
        String nonce = new String(encoded);
        // create the NONCE  Base64-encoded, URL-safe, and non-wrapped String
        String myNonce = Base64.encodeToString(nonce.getBytes(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);

        // Create an instance of a manager.
        IntegrityManager myIntegrityManager = IntegrityManagerFactory.create(context);

        // Request the integrity token by providing a nonce.
        Task<IntegrityTokenResponse> myIntegrityTokenResponse = myIntegrityManager
                .requestIntegrityToken(IntegrityTokenRequest
                        .builder()
                        .setNonce(myNonce)
                        .setCloudProjectNumber(Long.parseLong(CLOUD_PROJECT_NUMBER))         // necessary only if sold outside Google Play
                        .build());

        // get the time to check against the decoded integrity token time
        timeRequest = Calendar.getInstance().getTimeInMillis();

        myIntegrityTokenResponse.addOnSuccessListener(new OnSuccessListener<IntegrityTokenResponse>() {
            @Override
            public void onSuccess(IntegrityTokenResponse myIntegrityTokenResponse) {
                try {
                    String token = myIntegrityTokenResponse.token();

                    DecodeIntegrityTokenRequest requestObj = new DecodeIntegrityTokenRequest();
                    requestObj.setIntegrityToken(token);

                    //Configure your credentials from the downloaded Json file from the resource
                    GoogleCredentials credentials = GoogleCredentials.fromStream(Objects.requireNonNull(getClass().getClassLoader()).getResourceAsStream("credentials.json"));
                    HttpRequestInitializer requestInitializer = new HttpCredentialsAdapter(credentials);

                    HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
                    JsonFactory JSON_FACTORY = new JacksonFactory();
                    GoogleClientRequestInitializer initializer = new PlayIntegrityRequestInitializer();

                    PlayIntegrity.Builder playIntegrity = new PlayIntegrity.Builder(HTTP_TRANSPORT, JSON_FACTORY, requestInitializer).setApplicationName(APPLICATION_NAME)
                            .setGoogleClientRequestInitializer(initializer);
                    PlayIntegrity play = playIntegrity.build();

                    // the DecodeIntegrityToken must be run on a parallel thread
                    Thread thread = new Thread(() -> {
                        try {
                            DecodeIntegrityTokenResponse response = play.v1().decodeIntegrityToken("com.mydoamin.package", requestObj).execute();
                            String licensingVerdict = response.getTokenPayloadExternal().getAccountDetails().getAppLicensingVerdict();
                            if (licensingVerdict.equalsIgnoreCase("LICENSED")) {
                                isAppOk = true;
                                Log.d(TAG, "LICENSED OK APP");
                                // Looks good! LICENSED app
                            } else {
                                Log.d(TAG, "LICENSED NOT OK APP");
                                // LICENSE NOT OK
                            }
                        } catch (Exception e) {
                            //  LICENSE error
                        }
                        isAppOk = false;
                    });

                    // execute the parallel thread
                    thread.start();

                } catch (Error | Exception e) {
                    // LICENSE error
                }
            }
        });
        return isAppOk;
    }
}
  1. 客户端直接解码token:把服务端专属的验证逻辑放在客户端,credentials.json这类敏感凭证暴露在客户端,极易被逆向破解,完全违背Play Integrity的安全设计。
  2. Nonce生成逻辑错误:重复Base64编码,且使用固定密钥生成nonce,无法防止重放攻击,nonce必须是单次请求唯一的随机值。
  3. 异步逻辑失效:IsLicensedApp()直接返回初始值true,但token请求和线程执行都是异步操作,返回值永远不反映真实验证结果。
  4. 异常处理缺失:所有异常被直接吞掉,无法定位集成过程中的问题。

Android端正确集成步骤

1. 添加依赖

在app模块的build.gradle中添加官方依赖:

dependencies {
    implementation 'com.google.android.play:integrity:1.2.0'
}

2. 客户端仅负责请求Token

客户端只需要生成随机nonce并请求Integrity Token,然后将Token发送到服务端验证,绝对不能在客户端解码Token:

public class PlayIntegrityHelper {
    private static final String TAG = "PlayIntegrityHelper";
    private final Context context;
    private final OnIntegrityTokenListener listener;

    public interface OnIntegrityTokenListener {
        void onTokenReceived(String token);
        void onError(Exception e);
    }

    public PlayIntegrityHelper(Context context, OnIntegrityTokenListener listener) {
        this.context = context;
        this.listener = listener;
    }

    public void requestIntegrityToken() {
        // 生成16字节随机nonce,防止重放攻击
        String nonce = generateRandomNonce();
        
        IntegrityManager integrityManager = IntegrityManagerFactory.create(context);
        IntegrityTokenRequest request = IntegrityTokenRequest.builder()
                .setNonce(nonce)
                // 仅当应用在Google Play外分发时才需要设置此参数,否则省略
                // .setCloudProjectNumber(YOUR_CLOUD_PROJECT_NUMBER)
                .build();

        integrityManager.requestIntegrityToken(request)
                .addOnSuccessListener(response -> listener.onTokenReceived(response.token()))
                .addOnFailureListener(listener::onError);
    }

    private String generateRandomNonce() {
        byte[] randomBytes = new byte[16];
        new SecureRandom().nextBytes(randomBytes);
        return Base64.encodeToString(randomBytes, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP);
    }
}

3. 闪屏页调用示例

// 在闪屏页中发起Token请求
new PlayIntegrityHelper(this, new PlayIntegrityHelper.OnIntegrityTokenListener() {
    @Override
    public void onTokenReceived(String token) {
        // 将Token发送到你的Node.js服务端进行验证
        sendTokenToServer(token);
    }

    @Override
    public void onError(Exception e) {
        Log.e(TAG, "请求Integrity Token失败", e);
        // 处理错误,如提示用户检查网络或重启应用
    }
}).requestIntegrityToken();

Node.js服务端验证步骤

1. 安装依赖

npm install googleapis express

2. 服务端验证代码

将Google Cloud下载的credentials.json放在服务端安全目录下,禁止暴露给客户端:

const { google } = require('googleapis');
const express = require('express');
const app = express();
app.use(express.json());

// 初始化Play Integrity客户端
const auth = new google.auth.GoogleAuth({
    keyFile: './credentials.json', // 你的服务端凭证路径
    scopes: ['https://www.googleapis.com/auth/playintegrity']
});

// 验证Integrity Token的核心函数
async function verifyIntegrityToken(packageName, token) {
    try {
        const client = await auth.getClient();
        google.options({ auth: client });

        const playIntegrity = google.playintegrity('v1');
        const response = await playIntegrity.v1.decodeIntegrityToken({
            packageName: packageName,
            requestBody: { integrityToken: token }
        });

        const payload = response.data.tokenPayloadExternal;
        const licensingVerdict = payload.accountDetails.appLicensingVerdict;
        
        // 可选:额外验证应用完整性、设备完整性
        const appRecognition = payload.appIntegrity.appRecognitionVerdict;
        const deviceVerdict = payload.deviceIntegrity.deviceRecognitionVerdict;

        if (licensingVerdict === 'LICENSED') {
            return {
                valid: true,
                message: '应用已授权',
                appIntegrity: appRecognition,
                deviceIntegrity: deviceVerdict
            };
        } else {
            return {
                valid: false,
                message: '应用未授权',
                verdict: licensingVerdict
            };
        }
    } catch (error) {
        return { valid: false, message: '验证失败', error: error.message };
    }
}

// 暴露验证接口
app.post('/verify-integrity', async (req, res) => {
    const { packageName, token } = req.body;
    if (!packageName || !token) {
        return res.status(400).json({ error: '缺少必要参数' });
    }

    const result = await verifyIntegrityToken(packageName, token);
    res.json(result);
});

// 启动服务
app.listen(3000, () => {
    console.log('验证服务运行在端口3000');
});

关键安全注意事项

  • Token解码必须在服务端完成:服务端凭证是核心敏感信息,绝对不能出现在客户端代码中。
  • Nonce必须随机唯一:每次请求生成新的nonce,避免攻击者重放旧Token。
  • 验证完整维度:除了许可证状态,还应验证应用是否被篡改、设备是否合规,提升整体安全性。
  • 异步逻辑正确处理:客户端请求Token是异步操作,必须通过回调或协程处理结果,不能同步返回值。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 22:07:06