Google Pay 服务端订单验证与安全策略
1. Google Pay服务端验证的核心逻辑当用户通过Google Pay完成支付后客户端会获取到一个支付凭证purchase token这个token就是服务端验证的关键。我遇到过不少开发者直接把token存到数据库就完事了这其实存在很大风险。正确的做法是立即向Google服务器发起验证请求获取完整的订单信息。验证过程主要分为两个阶段本地RSA签名验证先用Google提供的公钥验证token的签名防止伪造请求Google API验证通过Android Publisher API查询订单详情这里有个容易踩坑的地方有些开发者只做API验证不做本地签名验证这在网络延迟高的时候会有安全隐患。我建议两个验证都要做顺序上可以先做本地验证再做API验证。2. 双重校验机制的具体实现2.1 本地RSA签名验证Google Pay的支付凭证包含三个关键字段signtureContent原始数据signture签名值packageName应用包名验证代码如下Java示例public boolean verifyPurchase(String signedData, String signature, String publicKey) { try { Signature sig Signature.getInstance(SHA1withRSA); sig.initVerify(KeyFactory.getInstance(RSA) .generatePublic(new X509EncodedKeySpec(Base64.decode(publicKey)))); sig.update(signedData.getBytes()); return sig.verify(Base64.decode(signature)); } catch (Exception e) { log.error(签名验证失败, e); return false; } }这里有个实用技巧不同包名对应的公钥可能不同建议用Map存储包名和公钥的对应关系MapString, String packageToPublicKey Map.of( com.example.app1, MIIBIjANBgk..., com.example.app2, MIIBIjANBgk... );2.2 Google API验证配置首先需要配置Service Account在Google Cloud Console创建服务账号下载JSON格式的私钥文件在Play Console关联API项目Java服务端集成Android Publisher API的依赖dependency groupIdcom.google.apis/groupId artifactIdgoogle-api-services-androidpublisher/artifactId versionv3-rev20220301-1.32.1/version /dependency初始化代码示例GoogleCredential credential GoogleCredential.fromStream( new FileInputStream(service-account.json)) .createScoped(Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER)); AndroidPublisher publisher new AndroidPublisher.Builder( GoogleNetHttpTransport.newTrustedTransport(), JacksonFactory.getDefaultInstance(), credential) .build();3. 订单状态解析与业务处理通过API获取的订单信息包含以下关键字段{ purchaseState: 0, consumptionState: 0, orderId: GPA.1234-5678-9012-34567, purchaseTimeMillis: 1640995200000, productId: premium_monthly, acknowledgementState: 0 }3.1 状态码解析purchaseState0已支付1已取消2待处理consumptionState0未消耗可退款1已消耗acknowledgementState0未确认1已确认3.2 业务逻辑处理建议我建议采用这样的处理流程检查purchaseState是否为0已支付检查consumptionState是否为0未消耗在数据库中记录订单信息发放商品/服务调用消费API标记为已消耗消费API调用示例AndroidPublisher.Purchases.Products.Consume consume publisher.purchases().products() .consume(packageName, productId, token); consume.execute();4. 防重复消费与并发控制4.1 分布式锁实现高并发场景下可能出现重复消费问题我的解决方案是用Redis分布式锁public boolean tryLock(String orderId) { return redisTemplate.opsForValue() .setIfAbsent(lock: orderId, 1, 30, TimeUnit.SECONDS); } public void unlock(String orderId) { redisTemplate.delete(lock: orderId); }处理流程if (!tryLock(orderId)) { throw new BusinessException(订单处理中请勿重复操作); } try { // 处理订单逻辑 } finally { unlock(orderId); }4.2 数据库唯一索引在订单表添加唯一索引ALTER TABLE google_pay_orders ADD UNIQUE INDEX idx_order_id (order_id);这样即使锁失效数据库层面也能防止重复记录。5. 异常处理与容错策略5.1 网络超时处理Google API调用建议配置合理的超时时间HttpRequestInitializer httpRequestInitializer request - { credential.initialize(request); request.setConnectTimeout(5 * 1000); // 5秒连接超时 request.setReadTimeout(10 * 1000); // 10秒读取超时 };5.2 令牌刷新机制access_token过期时需要自动刷新if (credential.getExpiresInSeconds() 60) { credential.refreshToken(); }5.3 重试策略对于可重试的异常如网络超时建议实现指数退避重试int retry 0; while (retry 3) { try { // 调用API break; } catch (IOException e) { Thread.sleep((long) Math.pow(2, retry) * 1000); retry; } }6. 性能优化实践6.1 缓存验证结果已验证的订单可以缓存一段时间// 缓存1小时 redisTemplate.opsForValue().set( order: orderId, orderInfo, 1, TimeUnit.HOURS);6.2 批量查询接口如果有批量验证需求可以使用purchases.products.batchGet接口AndroidPublisher.Purchases.Products.BatchGet batchGet publisher.purchases().products() .batchGet(packageName, tokenList);6.3 异步处理非关键路径可以异步处理Async public void asyncConsumeOrder(String orderId) { // 消费逻辑 }7. 安全加固措施7.1 请求参数校验必须校验包名是否匹配商品ID是否有效价格是否一致if (!validProducts.contains(productId)) { throw new SecurityException(非法商品ID); }7.2 敏感信息脱敏日志中的敏感信息需要脱敏logger.info(验证订单: orderId{}, product{}, mask(orderId), productId);7.3 限流防护防止暴力破解RateLimiter(value 10, key #ip) // 每秒10次 public OrderResult verifyOrder(String ip, OrderRequest request) { // 验证逻辑 }8. 监控与报警8.1 关键指标监控建议监控验证成功率API响应时间异常订单比例google_pay_verify_duration_seconds_bucket{le0.5} 1234 google_pay_verify_errors_total{typenetwork} 58.2 业务告警规则需要设置告警连续验证失败异常订单突增平均响应时间超标8.3 日志分析日志中需要包含足够的信息MDC.put(orderId, orderId); logger.info(开始验证Google Pay订单);这些经验都是我经过多个项目实践总结出来的特别是在处理高并发场景时分布式锁和缓存机制真的能解决大问题。还有一次因为没做本地签名验证导致被恶意请求刷了不少虚拟商品这个教训让我深刻认识到双重验证的重要性。