Note: For licensing in online apps and games, we recommend using the Play Integrity API.
Learn more.
为应用添加服务器端许可验证
使用集合让一切井井有条
根据您的偏好保存内容并对其进行分类。
验证用户是否已从 Google Play 商店购买或下载应用的合法副本时,最好在您控制的服务器上执行许可验证检查。
本指南介绍了完成服务器端许可验证的分步流程,并介绍了与执行此项检查相关的一些最佳做法。
流程概览
图 1 显示了应用、Google Play 和私有服务器之间如何传输信息:
- 应用向 Google Play 发出请求,询问特定用户是否已购买或下载应用的合法副本。
- Google Play 会通过向应用发送响应数据对象(对象类型为
ResponseData
)来做出响应。此对象是一条签名信息,表明用户是否已购买或下载应用的合法副本。
- 应用向您控制的私有服务器发出请求,以验证响应数据的内容。
- 服务器通过向应用发送状态来做出响应,以指示用户是否确实已购买或下载应用的合法副本。如果服务器提供“成功”消息,验证响应,然后向用户授予对需要许可的资源的访问权限。
由于响应数据由 Google Play 签署,然后在服务器上进行检查,因此无法在运行应用的设备上修改该对象。如果应用依赖服务器,并且仅向合法用户提供资源,则应用将受到更大程度的保护,使其免受未经授权用户的侵害。
以下部分提供了执行服务器端许可验证时应牢记的其他注意事项。
防范重播攻击
用户收到来自 Google Play 的用户许可状态响应之后,可以复制响应数据并多次使用,或者将其提供给其他用户,然后其他用户就可以伪造对应用的私有服务器的请求。此类操作称为重播攻击。
为了降低用户成功执行重播攻击的可能性,请在向应用服务器发送请求前采取以下措施:
检查响应数据中包含的时间戳,确保 Google Play 是在最近生成的响应。
对服务器请求执行速率限制,例如指数退避算法,以减少应用尝试向应用服务器发送相同响应数据的次数。
在私有服务器上验证 Google Play 响应数据的内容之前,请先向私有服务器发出基于身份验证的初始请求。在第一个请求中,将用户凭据发送到服务器,然后让服务器以 Nonce 或仅使用一次的数字做出响应。然后,您可以在发送到私有服务器的下一个请求中添加此 Nonce,请求获取许可验证数据。如需详细了解如何为 Nonce 选择合适的值,请参阅生成合适的 Nonce 值部分。
生成合适的 Nonce 值
使用以下一种技巧创建一个很难猜到的 Nonce 值:
- 根据用户 ID 生成哈希值。
- 为每个用户生成一个随机值。将此随机值作为指定用户属性的一部分存储在应用服务器上。
验证来自服务器的响应数据
查看应用服务器向应用发送的响应数据时,请确保许可验证库的响应不是伪造的。将应用服务器响应数据中包含的签名与应用在上一步中从 Google Play 收到的密钥进行比较,以验证此签名。
还有一点值得注意的是,许可验证库 (LVL) 专用的块是唯一签名的部分。因此,在应用服务器的响应数据中,应用只应该信任这一个部分。
本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。
最后更新时间 (UTC):2024-01-10。
[{
"type": "thumb-down",
"id": "missingTheInformationINeed",
"label":"没有我需要的信息"
},{
"type": "thumb-down",
"id": "tooComplicatedTooManySteps",
"label":"太复杂/步骤太多"
},{
"type": "thumb-down",
"id": "outOfDate",
"label":"内容需要更新"
},{
"type": "thumb-down",
"id": "translationIssue",
"label":"翻译问题"
},{
"type": "thumb-down",
"id": "samplesCodeIssue",
"label":"示例/代码问题"
},{
"type": "thumb-down",
"id": "otherDown",
"label":"其他"
}]
[{
"type": "thumb-up",
"id": "easyToUnderstand",
"label":"易于理解"
},{
"type": "thumb-up",
"id": "solvedMyProblem",
"label":"解决了我的问题"
},{
"type": "thumb-up",
"id": "otherUp",
"label":"其他"
}]
{"lastModified": "\u6700\u540e\u66f4\u65b0\u65f6\u95f4 (UTC)\uff1a2024-01-10\u3002"}
[[["易于理解","easyToUnderstand","thumb-up"],["解决了我的问题","solvedMyProblem","thumb-up"],["其他","otherUp","thumb-up"]],[["没有我需要的信息","missingTheInformationINeed","thumb-down"],["太复杂/步骤太多","tooComplicatedTooManySteps","thumb-down"],["内容需要更新","outOfDate","thumb-down"],["翻译问题","translationIssue","thumb-down"],["示例/代码问题","samplesCodeIssue","thumb-down"],["其他","otherDown","thumb-down"]],["最后更新时间 (UTC):2024-01-10。"]]