关于 rc-security
rc-security 是一项审查 Android 应用 RevenueCat 购买与权益安全的技能,指导检查验证模式、API 密钥、用户身份和服务端访问控制。
主要功能
- 01检查权益验证模式
- 02区分公开与秘密 API 密钥
- 03使用已认证 ID 识别用户
- 04在服务端检查权益
适用场景
- 01发布前审查 RevenueCat 配置
- 02排查 Android 源码中的秘密密钥
- 03为登录用户替换匿名 ID
- 04通过权益检查保护高级内容端点
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ revenuecat/ ai-toolkit --skill rc-security安装后在 Claude Code 中输入/rc-security确认已加载。
详细介绍
阶段 0:意图
告诉用户:“我将检查你在 Android 上的 RevenueCat 安全状况:验证模式、API 密钥、用户身份以及服务端访问决策。”
阶段 1:发现
确认 RevenueCat 已经处理了哪些内容,以便聚焦真正的缺口。
| 关注点 | 由谁处理 | 方式 |
|---|---|---|
| 针对 Google Play 的收据验证 | RevenueCat 后端 | 在 awaitPurchase() 返回前运行 |
| 购买令牌重复使用 | RevenueCat 后端 | 在服务端对令牌进行去重 |
| 伪造的购买令牌 | RevenueCat 后端 | 无法通过 Google Play 验证,不会授予权益 |
| 与 RevenueCat 之间的 HTTPS 传输 | SDK | 始终启用 |
| 网络故障时重试提交令牌 | SDK | 下次应用启动时重试 |
向项目提出以下问题:
- 是否设置了
EntitlementVerificationMode?如果没有,CustomerInfo响应会在未经签名检查的情况下被信任。 - SDK 配置使用的是公开 Android 密钥(以
goog_开头),还是有人误将 Secret 密钥粘贴到了客户端? - 是否通过
Purchases.logIn(yourUserId)识别真实用户,还是应用仍依赖匿名$RCAnonymousID:...? - 为高级内容提供服务的服务端端点是否在服务端验证权益,还是信任客户端发送的标记?
阶段 2:计划
在更改代码前,确定以下各项。
决策 A:权益验证模式
| 模式 | 验证失败时的处理方式 | 适用情况 |
|---|---|---|
DISABLED(默认) | 不进行签名 | 仅用于快速原型 |
INFORMATIONAL | 记录日志,仍授予访问权限 | 希望获取信号,又不想冒误拒真实用户的风险 |
ENFORCED | EntitlementInfo.isActive 返回 false | 接受因代理或 VPN 导致的一些误判,以换取严格的客户端保证 |
决策 B:权威信息来源在哪里?
即使启用了 ENFORCED,客户端也不是付费内容的权威信息来源。请决定每个高级端点采用以下哪种方式:
- 对每个请求调用 RevenueCat REST API,或
- 读取由 RevenueCat Webhook 驱动的本地
has_entitlement标记。
方式 2 在请求时成本更低,方式 1 则不会有缓存过期问题。请选择一种并记录下来,不要在没有理由的情况下为不同端点混用。
决策 C:用户身份
如果应用已投入生产,请计划使用你自己的已认证用户 ID 调用 Purchases.logIn("your_user_id")。匿名 ID 是按设备划分的标识符,不是凭据,而且同一设备上的不同用户会共享它。
阶段 3:执行
3.1 启用响应验证
将该模式添加到 PurchasesConfiguration:
PurchasesConfiguration.Builder(context, apiKey)
.entitlementVerificationMode(EntitlementVerificationMode.INFORMATIONAL)
.build()
检查权益时,读取验证结果:
when (customerInfo.entitlements.verification) {
VerificationResult.VERIFIED -> { /* response is authentic */ }
VerificationResult.FAILED -> { /* possible tampering, log and alert */ }
VerificationResult.NOT_REQUESTED -> { /* verification disabled */ }
VerificationResult.VERIFIED_ON_DEVICE -> { /* verified locally */ }
}
在遥测数据确认真实流量中的 FAILED 很少见后,升级到 ENFORCED:
.entitlementVerificationMode(EntitlementVerificationMode.ENFORCED)
在 ENFORCED 模式下,签名验证失败会将受影响权益的 isActive 设为 false。
3.2 将 API 密钥放在正确的位置
| 密钥 | 存放位置 | 可执行的操作 |
|---|---|---|
公开 Android SDK 密钥(goog_...) | 嵌入应用二进制文件 | 仅为发起调用的用户读取和购买 |
| Secret REST API 密钥 | 你的服务端、密钥管理器或环境变量 | 完整 REST API、管理操作、授予权益 |
Android 代码中只能出现公开密钥:
Purchases.configure(
PurchasesConfiguration.Builder(context, "goog_PUBLIC_android_sdk_key").build()
)
切勿将 Secret 密钥提交到应用代码仓库。发布前,在 Android 源代码树中搜索 Secret 密钥前缀,并确认没有匹配项。
3.3 识别真实用户
一旦获得已认证的用户 ID,就调用 logIn:
val result = Purchases.sharedInstance.awaitLogIn("your_user_id")
val customerInfo = result.customerInfo
不要将匿名 ID 用作凭据。它是按设备划分的标识符,无法保护共享设备上的购买历史。
3.4 在服务端而非客户端强制执行
即使启用了 ENFORCED 模式,每个为付费内容提供服务的服务端端点也都要在服务端检查权益:
def get_premium_content(user_id):
info = revenuecat.get_subscriber(user_id)
if not info.entitlements["pro"].is_active:
raise Forbidden()
return content
或者,读取由 RevenueCat Webhook 驱动的本地 has_pro 标记,并在每个请求中检查该标记。
阶段 4:验证
- Android 源码中只包含公开 SDK 密钥。未找到密钥。
- 已设置
EntitlementVerificationMode(不是DISABLED),并监控VerificationResult.FAILED遥测数据。 - 通过
Purchases.logIn识别真实用户。匿名 ID 仅用于登录前的流程。 - 服务器上的每个付费内容端点都通过 REST API 或由 webhook 驱动的标记验证权益。没有任何端点信任客户端标头。
常见错误
| 错误 | 造成的影响 | 修复方法 |
|---|---|---|
| 在应用中打包密钥 | 攻击者反编译 APK 后,可以调用管理 REST 端点 | 应用中使用公开密钥,密钥仅保存在服务器上 |
在生产环境中将模式保留为 DISABLED | 中间人可以伪造 CustomerInfo 响应 | 设置为 INFORMATIONAL 或 ENFORCED |
| 将匿名 ID 当作凭据 | 它不是机密,同一设备的用户会共享此 ID | 使用经过身份验证的用户 ID 调用 logIn |
在服务器上信任来自客户端的 customerInfo | 被篡改的客户端可以声称拥有任意权益 | 在服务器上通过 REST API 或由 webhook 驱动的数据库进行验证 |