google内购
google内购
google支付需要在服务端记录并验证订单,防止伪造订单,这里记录一下服务端校验订单
Google Pay主要支付流程:
相关信息
- 手机端向Java服务端发起支付,生成预订单,给手机端返回生成的订单号
- 手机端向Google发起支付(传入java服务端生成的订单号)
- Google服务器将支付结果返回给手机端(因这边用到的是消耗型的产品,所以购买后必须要通知gp我已经消耗了这次交易)
- 手机端向Java服务端发送校验请求,校验通过后即可处理订单(服务端重试校验,发货,保证订单正常发货成功)
1. 创建服务账号
- 打开地址: https://console.cloud.google.com
- 选择项目或者创建一个新的项目,为了区分和维护推荐创建个新的项目
- 选中项目后例如(Google-Pay) 创建服务帐户
- 在“ 服务帐户详细信息”下 ,键入服务帐户的名称,ID和描述,然后单击“ 创建”
- 可选:在“ 服务帐户权限”下 ,选择要授予服务帐户的IAM角色,然后单击继续
- 可选:在“ 授予用户对此服务帐户的访问权限”下 ,添加允许使用和管理该服务帐户的用户或组。
- 单击管理密钥,创建密钥 ,然后单击创建推荐生成json格式密钥。(服务账号的验证方式可用,不过google最新api已弃用,所以这个可能有问题)
警告
服务账号的验证方式可用,不过google最新api已弃用,所以这个可能有问题

2. Google Play后台关联服务账号并授权
- 进入Google Play 管理中心的API 权限页面
- 将 Google Play 开发者帐号关联到 Google Cloud 项目如(Google-Pay)
- 点击服务帐号下要向自己的服务帐号授予对 Cloud 项目的访问权限,这样它才能显示在Google Play管理后台
- 完成点击刷新,API 权限页面的“服务帐号”会自动刷新,您的服务帐号将随即列出。
- 点击授予api访问权和应用于哪个app(可以选择多个app),为服务帐号提供相关操作所需的权限。
注意
必须先将 Google Play 开发者帐号关联到 Google Cloud 项目,然后才能访问 Google Play Developer API。在大多数情况下,我们建议您为自己的 Google Play 开发者帐号新建一个专用的 Google Cloud 项目,不过您也可以关联现有项目。请注意,每个 Google Play 开发者帐号只能关联到一个 Google Cloud 项目。如果您的同一个 Google Play 开发者帐号中有多个应用,这些应用必须都共用同一个 Google Cloud 项目。
以上参考: https://blog.csdn.net/weixin_39222112/article/details/120068129
3. 获取凭证
由于要链接Google Play中的项目所以直接导入 androidpublisher,这里已经包含了部分鉴权相关的api,后续还需导入其他依赖包
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-androidpublisher</artifactId>
<version>v3-rev20211125-1.32.1</version>
</dependency>
3.1 如何查找maven依赖的版本:
- 简单的方法:直接在在该网址上查询即可 https://mvnrepository.com/ ,不过版本可能不是最新,选择一个使用最多的版本即可
- 稍微复杂一点(正式一点),访问开发者后台 https://console.cloud.google.com ,右侧导航栏API和服务->库,搜索androidpublisher,进入api库里面,查看教程和文档,链接跳转到官方文档中,在使用入门最下面客户端库, 请参阅 客户端库和代码示例 点解链接进入 适用于 Java 的 Google API 客户端库 即可跳转到官方的git仓库 https://github.com/googleapis/google-api-java-client-services/tree/main/clients/google-api-services-androidpublisher/v3
上面这个过程有点复杂,不过记录一下下次自己找东西就方便多了,外文文档一不留神就不清楚到哪去找东西了
3.2 获取凭证(客户端凭证)
相关信息
之前的GoogleCredential也会在后续被废弃,服务账号验证的方式不知道后续会怎样,所以还是要能找到最新的文档,接下来的方式是获取客户端凭证的流程
- 在刚才(进入api库里面,查看教程和文档,链接跳转到 官方文档 中)这一步中选择左侧导航栏: 使用入门
- 本页内容中: 使用 OAuth 客户端,点击链接OAuth,进入到 使用 OAuth 2.0 访问 Google API
- 本页内容最下面:客户端库-- 适用于 Java 的 Google API 客户端库 点击进入
- 本页内容中:已安装应用方式(服务账号的方式老是显示 GoogleCredential 被弃用 ):官方给的示例代码,修改获得自己的代码
直接使用文档中的代码,发现还需要导入很多的包,这些包去哪找?
根据文档页面最上面概览中介绍: 目的 :本文档介绍了如何使用 GoogleCredential 实用程序类对 Google 服务进行 OAuth 2.0 授权。如需了解我们提供的通用 OAuth 2.0 函数,请参阅 OAuth 2.0 和 Java 版 Google OAuth 客户端库 。
我们需要到 OAuth2.0和Java版Google OAuth客户端库这里面找 里面有个 com.google.api.client.auth.oauth2 (来自 google-oauth-client )
点击进入 google-oauth-client 使用到了里面的这些依赖
<!--最终使用版本 1.32.1-->
<dependency>
<groupId>com.google.oauth-client</groupId>
<artifactId>google-oauth-client-java6</artifactId>
<version>1.30.4</version>
</dependency>
<!--最终使用版本 1.32.1-->
<dependency>
<groupId>com.google.oauth-client</groupId>
<artifactId>google-oauth-client-jetty</artifactId>
<version>1.30.4</version>
</dependency>
<!--这个依赖文档中没有,不过确实用到了,我在其他文档中看到google-api-client-jackson2的2.0.0版本,不过那个有问题,所以还是按这个用吧 -->
<!--最终使用版本 1.30.4-->
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client-jackson2</artifactId>
<version>1.30.4</version>
</dependency>
完成代码(已安装应用客户端方式,需要用户授权):
public static Credential authorize(String clientSecretsJson, String user) throws GeneralSecurityException, IOException {
JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
InputStreamReader isr = new InputStreamReader(IOUtils.toInputStream(clientSecretsJson, "UTF-8"));
GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(jsonFactory, isr);
GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
transport, jsonFactory, clientSecrets,
Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER))
.build();
return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user");
}
警告
到了这一步,运行后显示java.lang.IllegalArgumentException
报错是在
GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(jsonFactory, isr);
查看源码才知道, load需要加载参数installed或者web
注意
上面下载的json文件是服务端的凭证,之前使用下面的代码是没问题的,不过GoogleCredential已弃用,服务账号的凭证与官方示例客户端验证不匹配,需要下载 OAuth 2.0 客户端 ID(凭证),一定要换
这里也贴一下之前的服务账号验证的方式
//这是之前的服务账号验证的方式,新的api中是被弃用的,但是官方文档中还是这么用的示例....,也许后续会更新吧
public static GoogleCredential authorizeServer() throws GeneralSecurityException, IOException {
HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(clientSecretsJson.getBytes());
//已弃用
GoogleCredential readJsonFile = GoogleCredential.fromStream(byteArrayInputStream, transport, jsonFactory)
.createScoped(Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER));
GoogleCredential credential = new GoogleCredential.Builder().setTransport(readJsonFile.getTransport())
.setJsonFactory(readJsonFile.getJsonFactory())
.setServiceAccountId(readJsonFile.getServiceAccountId())
.setServiceAccountScopes(readJsonFile.getServiceAccountScopes())
//.setServiceAccountUser("hospital-billing-manager@api-7965197382587815639-857758.iam.gserviceaccount.com")
.setServiceAccountPrivateKey(readJsonFile.getServiceAccountPrivateKey()).build();
return credential;
}
说实话到这一步我已经不想弄了,结果一看很清楚,过程无法言述,哎.......
这回写个main方法执行一下,果然不出我所料,还有问题
警告
这回是java.lang.NoSuchMethodError: org.eclipse.jetty.server.Connector.setHost
查看源码发现是 new LocalServerReceiver()出得问题,竟然没有 setHost方法, 不得不说就很强,头发掉很多,弄了半天我以为是什么写错了呢,后来决定换下依赖的版本,全都换成1.32.1,这回代码直接出错了, 这个JacksonFactory.getDefaultInstance();方法没有 了,所以有单独把google-api-client-jackson2换成1.30.4,这回可以了,代码没报错,运行也没报错,能弹出授权页面,但是授权页面显示:
警告
错误 400: redirect_uri_mismatch,这个问题是由于我的是测试项目,需要授权回调地址,所以redirect_uri得配制成自己的,得了,整个方法弄的还不如我自己写一个http请求来的快呢
//添加配置,开发者后台配置OAuth 2.0 客户端 ID
LocalServerReceiver localServerReceiver = new LocalServerReceiver.Builder().setHost("localhost").setPort(8092)
.setCallbackPath("/jingxc/google/google-callback").build();

整个可以运行的代码是:运行会有堆栈溢出,暂时没时间管这个,先将就着往下
package com.jxc.server.service.impl;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.store.FileDataStoreFactory;
import com.google.api.services.androidpublisher.AndroidPublisherScopes;
import com.jxc.server.service.GooglePayService;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.GeneralSecurityException;
import java.util.Collections;
@Service
public class GooglePayServiceImpl implements GooglePayService {
private static final java.io.File DATA_STORE_DIR =
new java.io.File(System.getProperty("user.home"), ".googlepay/pay_sample");
public static Credential authorize() throws GeneralSecurityException, IOException {
JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
//InputStreamReader isr = new InputStreamReader(IOUtils.toInputStream(clientSecretsJson, "UTF-8"));
InputStreamReader isr = new InputStreamReader(GooglePayServiceImpl.class.getClassLoader().getResourceAsStream("./client_secrets.json"));
GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(jsonFactory, isr);
FileDataStoreFactory dataStoreFactory = new FileDataStoreFactory(DATA_STORE_DIR);
GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
transport, jsonFactory, clientSecrets,
Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER))
.build();
LocalServerReceiver localServerReceiver = new LocalServerReceiver.Builder().setHost("localhost").setPort(8092).setCallbackPath("/jingxc/google/google-callback").build();
return new AuthorizationCodeInstalledApp(flow, localServerReceiver).authorize("user");
}
public static void main(String[] args) {
try {
Credential credential = authorize();
System.out.println(credential);
} catch (Exception e) {
e.printStackTrace();
}
}
}
相关信息
到这就可以显示跳转页面,但和我想要的不一样啊,我想用服务账号似的直接获取权限,我找了好多地方,也没有看到服务凭证还有那些新的不被弃用的方法,实在是找不出来了,不找了,我也问了gpt也是给出不建议用服务账号的方式
现在两种方式都尝试了一下,觉得怎样合适就怎样来吧
顺便这里说下之后会专门写一篇不用官方api的方式,感觉使用api反而比较乱
若果想消除服务账号验证的弃用标记,可以低版本的maven,我试了一下最高使用1.28.0之后就有弃用标记了
<!--本文写时官网版本是2.0.0 -->
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
<version>1.28.0</version>
</dependency>
提示
特别说明:在这我还特意去请教了一下gpt,我问的是google登陆的授权,道理一样,不同的授权就是scope参数不同

4. 查询订单信息
查询订单信息,上面的准备工作已经基本完成,可以直接完成代码了,伪代码如下:
public static void checkOrder() throws GeneralSecurityException, IOException {
// 参数详细说明:
String signtureData = "安卓上报的订单数据";
String signture = "安卓上报的签名";
String publicKey = "订单数据验签公钥";
JSONObject parseObject = JSON.parseObject(signtureData);
String productId = parseObject.getString("productId");//在谷歌后台定义的商品id
String packageName = parseObject.getString("packageName");//安卓apk包名
String purchaseToken = parseObject.getString("purchaseToken");//安卓上报的token
int purchaseState = parseObject.getIntValue("purchaseState");//订单状态
if (purchaseState != 0) {
//TODO 订单未完成付款
return;
}
Credential credential = authorizeServer();
HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
AndroidPublisher publisher = new AndroidPublisher.Builder(transport, JacksonFactory.getDefaultInstance(),
credential).setApplicationName("uu_oversea_pay").build();
AndroidPublisher.Purchases.Products products = publisher.purchases().products();
AndroidPublisher.Purchases.Subscriptions subscribes = publisher.purchases().subscriptions();
boolean doCheck = RSA.doCheck(signtureData, signture, publicKey, "RSA1", true);
// https://developers.google.com/android-publisher/api-ref/purchases/products/get
AndroidPublisher.Purchases.Products.Get product = products.get(packageName, productId, purchaseToken);
AndroidPublisher.Purchases.Subscriptions.Get subscribe = subscribes.get(packageName, productId,
purchaseToken);
// 获取订单信息
// https://developers.google.com/android-publisher/api-ref/purchases/products
// 通过consumptionState, purchaseState可以判断订单的状态
String purchaseOrderId = "";
int payType = 0;
if (0 == payType) {
ProductPurchase purchase = product.execute();
purchaseOrderId = purchase.getOrderId();
purchaseState = purchase.getPurchaseState();
if (purchaseState != 0) {
//TODO 订单未付款
return;
}
} else {
SubscriptionPurchase purchase = subscribe.execute();
Long expiryTimeMillis = purchase.getExpiryTimeMillis();
long now = System.currentTimeMillis() / 1000;
if (now > expiryTimeMillis) {
//TODO 订单已过订阅期限
return;
}
purchaseOrderId = purchase.getOrderId();
}
//TODO 更改订单状态
}
返回的数据信息:
{
"purchaseTimeMillis": "1682575200000",//购买产品的时间,自纪元(1970 年 1 月 1 日)以来的毫秒数。
"purchaseState": 0,//订单的购买状态。可能的值为:0. 已购买 1. 已取消 2. 待定
"consumptionState": 0,//产品的消费状态。可能的值为: 0. 尚未消耗 1. 已消耗
"developerPayload": "",
"orderId": "GPA.3398-6726-1036-80298",//google订单号
"purchaseType": 0,
"acknowledgementState": 0,
"kind": "androidpublisher#productPurchase",
"obfuscatedExternalAccountId": "SDK2204180944530041",//上面客户支付时的透传字段,google指导是用来存放用户信息的,不能过长,否则客户端不能支付
"obfuscatedExternalProfileId": "",
"regionCode": "HK"
}
5. 退款数据查询
退款信息查询,该功能是补充功能,正常来说用不到,不过可以作为当webhook信息接收出现问题时作为补充选项
public void googleRefundOrder() throws GeneralSecurityException, IOException {
GoogleCredential credential = authorizeServer();
String packageName = "安卓apk的包名";
HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
JacksonFactory defaultInstance = JacksonFactory.getDefaultInstance();
AndroidPublisher service = new AndroidPublisher.Builder(transport, defaultInstance, credential)
.setApplicationName("uu_oversea_pay").build();
// 获取google list对象
AndroidPublisher.Purchases.Voidedpurchases.List voidPurchaseList = service.purchases().voidedpurchases()
.list(packageName);
// 设置查询参数
voidPurchaseList.setStartTime(getDaysBeforeUnixTimeStampMinute(-24 * 60));//一天
// 执行查询
VoidedPurchasesListResponse response = voidPurchaseList.execute();
List<VoidedPurchase> voidedPurchases = response.getVoidedPurchases();
if (voidedPurchases == null) {
//TODO 没有退款
return;
}
// 获取分页tokenPagination
TokenPagination tokenPagination = response.getTokenPagination();
while (tokenPagination != null) {
// 设置查询token 重新执行查询 查询下一页
voidPurchaseList.setToken(tokenPagination.getNextPageToken());
VoidedPurchasesListResponse newResponse = voidPurchaseList.execute();
voidedPurchases.addAll(newResponse.getVoidedPurchases());
tokenPagination = newResponse.getTokenPagination();
}
for (VoidedPurchase voidedPurchase : voidedPurchases) {
String orderId = voidedPurchase.getOrderId();
//TODO 处理相关退款账号和设备
}
}
6. webhook
提示
要接收 Google Play 发送的退款通知,需要在 Google Play Developer Console 中配置并验证一个 Webhook URL。Webhook URL 是一个接收 POST 请求的 URL,当用户退款时,Google Play 会向这个 URL 发送退款通知。以下是配置 Webhook URL 的步骤:
- 打开 Google Play Developer Console,进入你的应用程序的管理页面。
- 在左侧导航菜单中,选择“商店设置”>“开发者帐户”。
- 单击“创建新的 API 密钥”,并按照指示生成 API 密钥。
- 在左侧导航菜单中,选择“商店设置”>“API 访问”。
- 单击“创建新的 Webhook”,输入 Webhook URL,选择要接收的通知类型(例如,购买、退款、续订等),然后单击“创建 Webhook”。
- 如果你的 Webhook URL 使用了 SSL 加密,则需要上传 SSL 证书。
- 单击“保存”。
注意
为了验证 Webhook URL 的有效性,Google Play 会向该 URL 发送一个验证请求。需要在 Webhook URL 中编写代码来处理验证请求,并返回一个特定格式的响应。验证请求的详细信息可以在 Google Play Developer Console 中找到。
完成以上步骤后,当有用户退款时,Google Play 会向你配置的 Webhook URL 发送一个 POST 请求,请求包含有关退款的详细信息。你可以使用这些信息来更新你的应用程序状态,并根据需要执行其他相关操作。
7. 订阅的订单
public void googleSubscribeOrder(byte[] body) throws IOException, GeneralSecurityException {
JSONObject paramJson = null;
String paramStr = new String(body, "utf-8");
if (StringUtils.isNotBlank(paramStr)) {
paramJson = JSON.parseObject(URLDecoder.decode(paramStr, "utf-8"));
JSONObject msgJson = paramJson.getJSONObject("message");
String data = msgJson.getString("data");
String developerNotificationStr = new String(Base64.getDecoder().decode(data), "UTF-8");
JSONObject developerNotificationJson = JSON.parseObject(developerNotificationStr);
String packageName = developerNotificationJson.getString("packageName");
JSONObject subscriptionNotificationJson = developerNotificationJson
.getJSONObject("subscriptionNotification");
String purchaseToken = subscriptionNotificationJson.getString("purchaseToken");
String subscriptionId = subscriptionNotificationJson.getString("subscriptionId");
/**
* notificationType int 通知的类型。它可以具有以下值: (1)
* SUBSCRIPTION_RECOVERED - 从帐号保留状态恢复了订阅。 (2)
* SUBSCRIPTION_RENEWED - 续订了处于活动状态的订阅。 (3)
* SUBSCRIPTION_CANCELED - 自愿或非自愿地取消了订阅。如果是自愿取消,在用户取消时发送。 (4)
* SUBSCRIPTION_PURCHASED - 购买了新的订阅。 (5) SUBSCRIPTION_ON_HOLD
* - 订阅已进入帐号保留状态(如已启用)。 (6) SUBSCRIPTION_IN_GRACE_PERIOD -
* 订阅已进入宽限期(如已启用)。 (7) SUBSCRIPTION_RESTARTED -
* 用户已通过“Play”>“帐号”>“订阅”重新激活其订阅(需要选择使用订阅恢复功能)。 (8)
* SUBSCRIPTION_PRICE_CHANGE_CONFIRMED - 用户已成功确认订阅价格变动。 (9)
* SUBSCRIPTION_DEFERRED - 订阅的续订时间点已延期。 (10) SUBSCRIPTION_PAUSED
* - 订阅已暂停。 (11) SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED -
* 订阅暂停计划已更改。 (12) SUBSCRIPTION_REVOKED - 用户在有效时间结束前已撤消订阅。 (13)
* SUBSCRIPTION_EXPIRED - 订阅已过期。
*/
int notificationType = subscriptionNotificationJson.getIntValue("notificationType");
if (2 == notificationType) {
GoogleCredential credential = authorizeServer();
HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
AndroidPublisher publisher = new AndroidPublisher.Builder(transport,
JacksonFactory.getDefaultInstance(), credential).setApplicationName("uu_oversea_pay")
.build();
AndroidPublisher.Purchases.Subscriptions subscribes = publisher.purchases().subscriptions();
AndroidPublisher.Purchases.Subscriptions.Get subscribe = subscribes.get(packageName, subscriptionId,
purchaseToken);
SubscriptionPurchase purchase = subscribe.execute();
Long expiryTimeMillis = purchase.getExpiryTimeMillis();
long now = System.currentTimeMillis() / 1000;
if (now > expiryTimeMillis) {
//已过订阅期限
return;
}
String purchaseOrderId = purchase.getOrderId();
//TODO 续订
}
}
}
google支付相关的问题就介绍到这...
后记:我会在写一篇curl的流程介绍,那样不受api版本限制,比较自由灵活
- 本文作者: 景兴春
- 本文链接: https://www.jingxc.top/back/payment/java-google.html
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!