
前阵子有个朋友找到我说他们应用在某个安卓市场上被人换了图标重新上架包里的广告SDK被替换还多塞了一个恶意下载模块。更讽刺的是用户不但没发现破绽反而跑来问他们“你们App什么时候改版了”——因为那个盗版包装得实在太像了。这个故事听起来像段子却是很多 Android 团队每天都在面对的现实。移动应用攻击早就不是安全研究员PPT里的演示而是一条完整的黑色产业链重打包、内存注入、自动化批量刷量、模拟器跑号、改机工具每一种都能直接影响收入和品牌信任。传统那种在客户端里写死几个校验、混淆一下、再上个加固就以为安全的思路已经被证明扛不住了。所以当 Play Integrity API 出现的时候我身边不少做移动安全的人都觉得这件事的思路终于对了把“这设备可不可信、这个包是不是从正规渠道来、安装完整性是否被破坏”的判断从客户端挪到云端去完成让服务生态来背书。对企业级应用来说这已经是当前对抗篡改、重打包、批量化作弊最重要的一道信任防线也是我在多个项目里踩过坑、验证过、真正能放到线上的方案。这篇内容我会把 Play Integrity API 的底层机制、接入架构、服务端校验逻辑、企业落地时的策略设计和真实排查经验完整讲透适合移动端负责人、客户端开发和安全工程师参考。不说废话直接进入正题。1. 先搞清楚Play Integrity API 到底在防什么1.1 你面对的威胁重打包、注入与批量作弊在讨论技术方案之前先把威胁模型讲清楚因为很多团队接入 Play Integrity API 之后依然不知道怎么设定判定策略根源就是没有想明白自己在防什么。Android 应用的攻击大致分三类。第一类是重打包。攻击者把官方包拆开加入自己的逻辑替换签名再重新打包分发。这类攻击最容易出现在非官方应用市场和论坛帖子里用户往往从搜索广告或者陌生人分享的链接中招。重打包之后App 里的支付跳转、广告SDK、甚至登录凭证都可能被劫持。过去靠比对包体积、校验代码签名确实能拦截一部分但攻击者连代码都能改这些静态校验在他们眼里只是时间问题。第二类是运行时注入和 Hook。攻击者通过调试工具挂载到进程里直接改写方法返回值、绕过校验逻辑。我见过最典型的场景是客户端做了“是否已付费”的判断攻击者用注入工具把这个判断函数返回值一改付费墙就直接消失了。这种攻击最大的特点是本地自检根本拦不住因为你自检的代码本身也运行在同一个被控制的内存环境里。第三类是批量化作弊。模拟器批量开号、设备农场群控、改机工具伪造设备信息这类攻击集中在积分、签到、新人礼、秒杀这类营销场景。单看每个请求都很正常但后台一看设备指纹高度相似、行为模式完全一致就是典型的批量脚本。传统设备指纹方案可以被改机工具绕过因为那些设备参数都在客户端本地读取。Play Integrity API 针对的就是这三类问题中的共同核心它帮你判断“当前运行环境是否可信、当前安装来源是否正规、设备是否通过了完整性和强完整性校验”并且这个判断由云端发起、由生态根证书背书攻击者在客户端本地再怎么能改也改不了服务端的结论。1.2 三类判定设备、应用与账号Play Integrity API 最核心的部分是它每次返回的 Integrity Token完整性令牌解码之后包含三个维度的判定结果。第一维是设备完整性即 Device Integrity。它用来判断设备本身是否可信返回值一般有这几个档位MEETS_STRONG_INTEGRITY设备通过了最高级别的完整性校验通常由硬件支持的密钥库提供证明是最可信的档位。MEETS_DEVICE_INTEGRITY设备通过了基础完整性校验但没有强完整性那么严格。MEETS_BASIC_INTEGRITY设备通过了一些基本检查但可能是 root 过的设备或存在某种风险。MEETS_VIRTUAL_INTEGRITY设备是模拟器不能提供任何完整性保证。第二维是应用完整性即 App Integrity。它判断当前安装的包是否来自 Google Play 且未被篡改核心返回字段是PLAY_RECOGNIZED包通过 Google Play 识别安装来源可信。UNRECOGNIZED_VERSION包未被识别这通常意味着安装包不是从 Google Play 正规渠道来的或者已经被重打包。UNEVALUATED由于某些原因没有评估。第三维是账号许可即 Account Integrity。它判断当前设备上登录的 Play 账号是否具备合法授权返回 LICENSED 或 UNLICENSED。这三个维度是独立返回的组合起来就能形成一套非常清晰的信任图谱。比如一个设备如果返回 MEETS_DEVICE_INTEGRITY PLAY_RECOGNIZED LICENSED那基本可以认为用户是在一台正常设备上从正规渠道安装的官方版本。如果返回 MEETS_BASIC_INTEGRITY UNRECOGNIZED_VERSION那大概率就是一台 root 过的设备上装了一个来路不明的包。1.3 为什么说这是信任模型的革命我在开篇用了“革命性”这个词可能有人觉得夸张。但做过客户端安全的人应该理解过去我们所有的本地自检本质上都是不可靠的因为判断逻辑和被判断的对象运行在同一个环境中攻击者打破了进程边界之后所有本地校验都可以被模拟和绕过。Play Integrity API 的革命性在于它把安全决策从客户端转移到了服务端。客户端只是发起请求真正做判断的是基础设施侧的信任根。它把“我的客户端是否可信”变成了“整个设备与安装链路是否可信”这是一个完全不同层面的信任模型。而且这个方案是生态级别的不需要自己维护公私钥、不需要自己搭验证服务器也不需要自己分发信任凭证。它直接利用 Google Play 生态已有的信任链对企业来说节约了大量安全基础设施建设成本。当然它也不是万能的后面章节我会提它的局限性和降级策略。2. 接入前的准备工作账号、指纹与依赖2.1 前置条件Play Console 与云项目的绑定接入 Play Integrity API 的第一步不是写代码而是把应用和云端项目正确关联起来。这一步的坑非常多我见过不少团队代码写完了、联调却怎么都过不了最后发现是 Play Console 的关联关系没配好。你需要准备这几样东西一个 Play Console 账号并且应用已经发布到内部测试轨道或更高版本轨道。注意仅存在于草稿状态的应用可能无法正常使用完整性检测。一个 Google Cloud 项目用于接收 Play 服务返回的判定结果。一个服务账号后续服务端解密和验证令牌时需要用到它的私钥。应用包名和签名 SHA-256 指纹这个必须和当前发布的签名一致。在 Play Console 的应用配置页面里找到“应用完整性”相关的设置入口将你的云项目绑定到当前应用。绑定之后Play 服务才会把完整性判定结果投递到你的云项目下。日常开发中如果换了电脑签名或者上传了新的签名密钥一定要回 Play Console 同步更新指纹否则测试阶段会发现所有请求都返回异常结果。2.2 客户端依赖与初始化客户端的接入方式非常简单。Gradle 里加上 Play Integrity 相关的依赖然后在应用初始化阶段拿到 IntegrityManager 实例。// Gradle 依赖示例具体版本以官方文档当前发布为准 dependencies { implementation(com.google.android.play:integrity:1.4.0) }初始化代码val integrityManager IntegrityManagerFactory.create(applicationContext)这里有个重要的设计点不需要你在初始化时做任何登录或者获取用户态的逻辑Play Integrity API 是由系统服务直接响应请求的所以调用方式是标准异步模式。你只需要关心返回的 IntegrityToken 字符串后续怎么处理它才是关键。3. 客户端请求与服务端校验完整数据链路3.1 nonce 的设计抗重放的关键很多团队在接入 Play Integrity API 时把注意力放在客户端请求上却忽略了 nonce 的设计。这个字段的重要性怎么强调都不过分它是防止攻击者重放合法结果、绕过服务端校验的核心。nonce 是一段随机字符串客户端在发起完整性请求之前生成发起请求时传进去服务端收到返回结果后校验这个 nonce 是否和自己之前下发给客户端的值一致。这保证了每次校验请求都是独立的攻击者即使截获了一个合法的校验结果也无法在另一个请求中重用。nonce 的生成需要满足几个要求每次请求都必须不一样不能复用。建议至少 16 字节128 位随机数用安全随机数生成器生成。服务端必须校验 nonce 的匹配性校验完成后及时销毁。我自己在生成时习惯用 SecureRandom 生成 16 字节再 Base64 编码。这样做的好处是兼容性好服务端不需要处理特殊字符。private fun generateNonce(): String { val bytes ByteArray(16) SecureRandom().nextBytes(bytes) return Base64.encodeToString(bytes, Base64.NO_WRAP) }注意不能让客户端自己决定一个固定的 nonce否则攻击者可以先自己拿到一个合法响应再用同样 nonce 伪造后续请求。3.2 请求时机与缓存策略我一直觉得Play Integrity API 的接入方案里最容易被忽略的是“什么时候请求”。有些团队图省事在应用启动时请求一次拿到结果之后存起来用一整天。这个方案的问题是攻击者完全可以在首次请求之后就对客户端动手脚之后一整天的校验结果都是失效的。正确思路是把完整性校验嵌到业务的关键路径里。登录、支付、领取奖励、签到、参与活动之前发起一次完整性请求拿到结果再放行。只有这样才能保证“每次关键操作发生时设备状态都是可信的”。当然敏感操作每次都校验会带来延迟所以需要缓存策略。我的经验是区分校验等级普通操作可以用一个 5 到 10 分钟的短期缓存比如用户浏览首页、查看列表没必要每次都卡一个网络请求。但涉及资金操作和账号安全操作必须实时校验不能用缓存。请求发出去之后需要设置合理的超时时间。网络不稳定或者 GMS 服务响应慢时不能让用户一直等下去。我的经验是超时控制在 10 秒左右并且把超时视为校验失败走当前场景的降级逻辑比如阻止高风险操作但允许浏览类操作继续。val request IntegrityTokenRequest.builder() .setNonce(nonce) .build() integrityManager.requestIntegrityToken(request) .addOnSuccessListener { response - val token response.token() // 把 token 交给服务端进行最终校验 } .addOnFailureListener { e - // 本次校验失败走降级逻辑 }3.3 服务端验签与判定逻辑这一步是最容易出事的环节。很多客户端团队会把 IntegrityToken 拿到手之后本地解析一下字段然后决定是否放行。这种做法的危害在于攻击者根本不需要破解你的逻辑只需要 Hook 掉本地校验函数让一切都按放行结果走就行。所以我的态度很明确Play Integrity 的校验结果必须在服务端验证客户端永远只把它当成一个“待验证的凭证”。服务端要做的事情分为三步。第一步验签确认返回的令牌确实由 Google 服务签发而不是攻击者伪造的字符串。第二步解密并解析出字段拿到设备完整性、应用完整性、账号许可三类判定结果。第三步校验 nonce 和时效性确认这一次请求的业务上下文和令牌是绑定的。// 伪代码核心校验流程示意 public IntegrityResponse verifyIntegrityToken(String token, String expectedNonce) { // 1. 用 Google 发布的证书验证 JWS 签名 // 2. 解密载荷得到 integrity verdict 字段 // 3. 校验 nonce 是否与本次业务请求下发的一致 // 4. 校验请求时间是否在合理时间窗内 // 5. 返回判定结果给业务层 }服务端的判定逻辑不是简单判断“是否通过”而是需要根据业务场景设定阈值。比如一个论坛应用要求 PLAY_RECOGNIZED 和 MEETS_DEVICE_INTEGRITY 就足够但一个金融应用风控要求就必须上升到 MEETS_STRONG_INTEGRITY否则就要触发风险提示或者人工验证。具体阈值设计在下一章展开。4. 企业落地中的策略设计与阈值设定4.1 不同业务场景下的判定阈值Play Integrity API 返回的是原始判定真正决定它价值的是业务侧如何消费这些判定结果。我把常见企业应用分为三类它们的策略差异很大。第一类是内容型应用比如资讯、社区、视频类。这类应用最怕的是重打包带来的内容替换和隐私劫持对实时性要求不高。门槛可以设为 PLAY_RECOGNIZED设备状态只做参考。因为内容型应用用户广泛设备差异大过于严格的设备校验会误伤大量真实用户。第二类是交易型应用比如电商、支付、金融类。这类应用直接和资金挂钩攻击者重打包一次就可能批量盗刷或者篡改支付流程。门槛必须设为 MEETS_DEVICE_INTEGRITY 或 MEETS_STRONG_INTEGRITY同时要求 PLAY_RECOGNIZED 和 LICENSED。有一次我们做电商活动攻击者用重打包后的客户端去抢占秒杀名额服务端发现大量请求的 appIntegrity 不在 PLAY_RECOGNIZED 状态直接拦掉之后活动数据立刻恢复了正常。第三类是游戏或积分激励类应用核心风险是模拟器和批量养号。这里对 MEETS_VIRTUAL_INTEGRITY 要特别敏感因为模拟器环境下无法保证任何完整性可以默认拒绝模拟器参与排行、结算、对战、奖励领取等场景。但要注意测试同学日常使用模拟器跑功能所以内部测试包要单独处理不要一刀切。4.2 降级策略没有 GMS 的设备怎么办Play Integrity API 依赖 Google Play 服务但并不是所有 Android 设备都有 GMS 认证。国内大量设备出厂不带 GMS或者用户自行移除/停用了 Play 服务这类设备发起完整性请求往往直接失败。这里不能把失败一律当风险拦掉否则会损失大量正常用户。我的建议是设计三层降级。设备支持 Play 服务并成功返回结果正常消费判定字段。设备支持 Play 服务但返回失败根据失败码区分是网络原因、版本原因还是服务异常临时放行但打上风险标签后续用其他风控手段补查。设备不支持 Play 服务直接走独立风控通道比如结合设备指纹、行为分析、短信验证码等人工复核手段。实际运营中无 GMS 设备的用户群里可能混着攻击者但也确实有很多普通用户重点在于这类用户不能被一刀切封禁而是进入更高等级的风控检查池。4.3 与现有安全体系的关系接入 Play Integrity API 之后很多团队会误以为可以撤掉加固和自研风控了。这是个大坑。Play Integrity 解决的是信任根问题它告诉你设备是否可信、包是否来源正规但它不会告诉你用户行为是否恶意、请求序列是否是脚本。这两者需要配合。我的经验是Play Integrity 应该作为整个风控体系里的“一号信任因子”加在风控决策的最前端。它先过滤掉一批明显的重打包、模拟器和不可信设备请求剩下的通过请求再交给后端的行为风控和业务风控去分析。加固、混淆、RASP 这些手段也不是多余的它们提高了攻击者的本地分析成本让 Play Integrity 的绕过更难实现。四者是叠加关系不是替代关系。5. 常见问题与排查实录5.1 返回结果不符合预期的排查思路我梳理了几个接入和上线过程中最高频的问题大家可以对照排查。现象可能原因排查方式所有请求都返回 UNEVALUATED应用未在 Play Console 正确发布或云项目绑定关系缺失检查 Play Console 应用完整性设置确认云项目已绑定测试包总是 UNRECOGNIZED_VERSION测试包没有从 Google Play 渠道安装可能是直接侧载的 debug 包通过 Play Console 内部测试轨道安装测试包模拟器上返回 MEETS_VIRTUAL_INTEGRITY这是模拟器的正常判定不代表异常业务策略按模拟器拦截或降级服务端验签失败云项目的服务账号私钥配置错误或服务器时间偏差过大检查 JWS 签名使用的证书和密钥校准服务器时间签名更新后大量请求失败Play Console 中的签名指纹和当前包签名不一致更新 Play Console 的签名指纹等待生效5.2 性能、超时与用户体验接入 Play Integrity API 之后最直观的变化就是关键操作变慢了。这个请求要走一次网络链路还要等待服务端评估耗时通常在 200ms 到 1s 之间。如果设计不当用户会明显感知到卡顿。我见过一个项目把校验放在点击支付按钮的同步路径里结果用户一顿操作之后转圈转了好几秒线上投诉立刻飙升。正确的做法是提前预取和异步化。用户在某个页面停留时后台提前生成 nonce 并缓存完整性 token真正触发敏感操作时直接拿已经返回的结果提交服务端校验。如果缓存过期则临时走一次同步请求但要做好加载态提示和超时兜底。还有一点容易被忽视请求生命周期管理。Activity 或 Fragment 的回调里发起请求后如果页面已经销毁回调可能触发空指针。一定要在 onDestroy 里取消请求或者用应用级别的生命周期管理避免在无效上下文中处理回调。5.3 Play Integrity 与加固/风控的互动关系很多团队反馈上了 Play Integrity 之后本地加固好像“没用了”。其实不是没用而是各自负责的层面不同。加固和混淆解决的是代码静态分析难度阻止攻击者快速读懂你的逻辑。Play Integrity 解决的是运行环境和安装来源可信度。如果一个包被篡改到面目全非Play Integrity 的 appIntegrity 直接不给 PLAY_RECOGNIZED攻击者连正常业务逻辑都进不去。所以我的建议是不要在两者之间做取舍。主力 App 该加固加固该上 Play Integrity 上 Play Integrity两者都上之后再叠加服务端行为风控这个组合是目前企业级 Android 应用最务实的防护方案。6. 避坑指南与监控告警6.1 我踩过的最贵的几个坑第一个坑是本地校验。早期 Pilot 的时候我们图省事在客户端把 token 解析完直接判断结果上线一周就有攻击者用 Hook 工具把判断结果全部改成放行。当时复盘问题不在于 Play Integrity 本身而在于我们错误地把它放在了客户端可信边界内。第二个坑是 nonce 复用。有一次服务端同学为了“优化性能”允许同一个 nonce 在短时间内重复使用结果攻击者拿到一个合法响应一顿重放直接突破了签到限制。后来改成每个请求独立 nonce并且服务端加了幂等校验这个问题才彻底解决。第三个坑是测试环境混乱。团队里用模拟器测功能发现结果不对就有人在代码里写“如果是模拟器就放行”的逻辑。这个逻辑悄悄进了生产等于给攻击者开了一扇大窗。后来我们强制区分构建类型调试包可以用宽松策略生产包必须全量校验杜绝测试逻辑泄漏到线上。6.2 灰度发布与线上监控指标接入 Play Integrity API 不是一次性上线而是需要灰度演进。我推荐的分阶段策略是先只记录不拦截把判定结果的分布情况统计出来。观察几天之后确认正常用户的判定分布符合预期再对高风险结果做告警最后逐步把拦截策略放开。线上必须监控几个核心指标PLAY_RECOGNIZED 比例、MEETS_DEVICE_INTEGRITY 比例、MEETS_STRONG_INTEGRITY 比例、UNEVALUATED 比例、服务端验签失败率。这些指标发生剧烈波动时通常意味着三种情况签名更新未同步、Play 服务大面积异常、或者出现了新的绕过方式。提前设置好告警比事后看用户投诉舒服得多。6.3 后续扩展思路Play Integrity API 能做的远不止“判断设备是否可信”。把它的结果和业务数据打通之后可以做很多精细化的运营策略。比如新用户注册时对 UNRECOGNIZED_VERSION 的包自动要求手机号二次验证营销活动里对非 PLAY_RECOGNIZED 的请求直接不发放权益金融类操作中要求 MEETS_STRONG_INTEGRITY 才允许修改安全设置。另外它还能辅助做版本更新策略。如果某天下载渠道被恶意植入了篡改包服务端能把 UNRECOGNIZED_VERSION 的请求引导到应用内升级页主动把用户导回正规渠道而不是被动等待用户被引流走。最后再分享一点个人体会安全是一个持续对抗的过程Play Integrity API 解决的问题只是整个链路里最基础的信任确认。它确实是一次信任模型的革命但革命之后的每一天还是要靠服务端策略迭代和线上监控去守住阵地。我见过太多团队把安全寄托在某一个单点上最后都是要付出代价的。把 Play Integrity 用好、和现有的风控体系联动起来这条路才是长期靠谱的。