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

