Google支付和服务端验证
因为公司业务需求,需要使用google的登录和支付。google支付分为订阅和应用内购买两种,笔者使用的是应用内购买这种方式,这里将整个google支付和支付验证的流程记录下来。
导入google结算库
def billing_version = "4.0.0"
implementation "com.android.billingclient:billing-ktx:$billing_version"
接入支付
流程:
- 初始化链接到google支付服务,如果不能链接到说明设备环境有问题,要么是没有FQ,要么是google套件(google paly 、server)没有安装完整,国内手机都是阉割过的,所以需要重新安装google套件
- 查询上次未消费的商品,如果有未消费的商品通知服务器,然后消费掉。因为国外的支付环境和国内不一样,他们可以线上下单,然后到便利店去支付,所以有未消费的这种情况。
这时google支付的准备工作已完成,下面就可以发起支付了
- 使用google后台配置商品id进行支付
- 支付完成后通知服务器验证订单合法性并发货
- 客户端消费商品
下面咋们上代码
step1
初始化并连接到google服务
// init方法
public synchronized void init(Activity mActivity){
//创建BillingClient 对面,查询 消费 支付都会使用这个对象
this.mBillingClient = BillingClient.newBuilder(mActivity)
.setListener(new PurchasesUpdatedListener() {//设置支付回调,这里其实是商品状态发生变化时就会回调
@Override
public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
LogUtils.d("call onPurchasesUpdated");
if (billingResult.getResponseCode() == BillingResponseCode.OK && purchases != null) {//支付成功
for (Purchase purchase : purchases) {
if(purchase == null || purchase.getPurchaseState() != Purchase.PurchaseState.PURCHASED) continue;
OrderManager.getInstance().paySuccess(purchase);
//通知服务器支付成功,服务端验证后,消费商品
}
//TODO客户端同步回调支付成功
} else if (billingResult.getResponseCode() == BillingResponseCode.USER_CANCELED) {//支付取消
} else {//支付失败
}
}
})
.enablePendingPurchases()
.build();
//链接到google play
this.connectBillPay();
}
private void connectBillPay(){
mBillingClient.startConnection(new BillConnectListener());
}
class BillConnectListener implements BillingClientStateListener {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
//链接到google服务
payEnable = true;
queryPurchases();
}
}
@Override
public void onBillingServiceDisconnected() {
//未链接到google服务
payEnable = false;
connectBillPay();
}
}
setp2
查询已支付的商品,并通知服务器后消费(google的支付里面,没有消费的商品,不能再次购买)
private void queryPurchases(){
PurchasesResponseListener mPurchasesResponseListener = new PurchasesResponseListener() {
@Override
public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List<Purchase> purchasesResult) {
if(billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK || purchasesResult == null) return;
for (Purchase purchase : purchasesResult) {
if(purchase == null || purchase.getPurchaseState() != Purchase.PurchaseState.PURCHASED) continue;
OrderManager.getInstance().paySuccess(purchase);
//这里处理已经支付过的订单,通知服务器去验证
}
}
};
mBillingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP, mPurchasesResponseListener);
}
setp3
发起支付
/**
*
* @param cpOrder 你自己的订单号或者用户id,用于关联到对应的用户,发放道具时使用
* @param productId google后台配置产品ID
*/
public void pay(final String cpOrder, final String productId) {
if(mBillingClient == null || wrActivity.get() == null || !payEnable){
//TODO客户端同步回调支付失败,原因是为链接到google或者google的支付服务不能使用
return;
}
//查询商品详情
querySkuDetailsAsync(cpOrder, productId);
}
//查询商品详情
void querySkuDetailsAsync(final String cpOrder, final String productId){
List<String> skuList = new ArrayList<>();
skuList.add(productId);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
mBillingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult,
List<SkuDetails> skuDetailsList) {
if (skuDetailsList != null && billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK){
for(SkuDetails skuDetails : skuDetailsList){
if(productId.equals(skuDetails.getSku())){
//发起支付
launchBillingFlow(cpOrder, skuDetails);
}
}
}
}
});
}
//吊起google支付页面
void launchBillingFlow(String cpOrder, SkuDetails skuDetails){
mBillingClient.launchBillingFlow(
wrActivity.get(),
BillingFlowParams
.newBuilder()
.setSkuDetails(skuDetails)
.setObfuscatedAccountId(cpOrder)//这里本来的意思存放用户信息,类似于国内的透传参数,我这里传的我们的订单号。老版本使用DeveloperPayload字段,最新版本中这个字段已不可用了
.build()
);
}
服务器支付验证操作较为复杂,咋们在下面单独提出来做一个小节
setp5
消费商品
public void consumePurchase(final Purchase purchase){
if(mBillingClient == null || purchase == null || purchase.getPurchaseState() != Purchase.PurchaseState.PURCHASED) return;
LogUtils.d("消耗商品:\n商品id:" + purchase.getSkus() + "\n商品OrderId:" + purchase.getOrderId() + "\ntoken:" + purchase.getPurchaseToken());
LogUtils.d("消耗商品:" + purchase.getAccountIdentifiers().getObfuscatedAccountId());
ConsumeParams consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
ConsumeResponseListener listener = new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() == BillingResponseCode.ERROR) {
//消费失败将商品重新放入消费队列
OrderManager.getInstance().consumeFinal(purchase);
return;
}
LogUtils.d("消费成功");
}
};
mBillingClient.consumeAsync(consumeParams, listener);
}
服务端验证
做服务端验证前,需要做一下准备工作
- 创建api项目这个和登录用的项目不是同一个
- 开启Google Play Android Developer API
- 设置oauth同意屏幕(就是拉起开发者授权账号登录时的登录页面)
- 创建web应用的oauth客户端ID
- google play开发者后台,API权限菜单中关联刚刚创建的项目,一个google play账号只需要也只能关联一个api项目就行了,这个项目可以查询关联账号中的所有应用的订单
- 拉起授权页面,使用google开发者账号给项目授权,得到code
- 通过code,拿到refreshToken,这个token只有第一次才会返回需要永久储存(这个refreshtoken很重要,需要保存下来),如果弄丢,只有重新创建一个oauth客户端ID,然后重复步骤6,7,拿到新的refreshtoken
- 刷新refreshToken, 得到accessToken,通过accesstoken就可以去查询订单状态了,这里的accessToken一般只有5分钟左右,5分钟后需要重新用refreshToken换取新的accessToken
下面咋们上操作截图
setp1
创建api项目google api console
![](https://upload-images.jianshu.io/upload_images/3418275-44f98dae8e3e3a38.png)
setp2
开启Google Play Android Developer API
![](https://upload-images.jianshu.io/upload_images/3418275-dfa27a8f79bb3aa0.png)
![](https://upload-images.jianshu.io/upload_images/3418275-726c8ef52f2a619c.png)
搜索“Google Play Android Developer API”
![](https://upload-images.jianshu.io/upload_images/3418275-dc262e86de6d8013.png)
开启“Google Play Android Developer API”
![](https://upload-images.jianshu.io/upload_images/3418275-4922947d7afb2ff3.png)
setp3
开启同意屏幕
![](https://upload-images.jianshu.io/upload_images/3418275-67f7e3ade6d57579.png)
这里填上必填项就行了,这个授权同意屏幕,请求code时拉起来给咋们开发人员开的,填啥都无所谓
setp4
创建oauth2客户端id
![](https://upload-images.jianshu.io/upload_images/3418275-2ac60d63b6d07404.png)
创建页面和创建成功后的修改页面可以获取到clientId和clientSecret
![](https://upload-images.jianshu.io/upload_images/3418275-6c5b1c05990e215f.png)
到这里api项目就已经创建好了
setp5
google play后台关联api项目
![](https://upload-images.jianshu.io/upload_images/3418275-c69f91d041e60713.png)
setp6
获取code
地址:https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/androidpublisher&response_type=code&access_type=offline&redirect_uri={填写的重定向地址}&client_id={创建的clientId}
将上面的{XX}替换成创建api项目时填写的重定向地址,和clientId,然后将连接放到浏览器中打开,就会吊起授权界面,使用你的开发者账号授权登录
请求方式:浏览器中打开
![](https://upload-images.jianshu.io/upload_images/3418275-3bdd397f8150c6cd.png)
![](https://upload-images.jianshu.io/upload_images/3418275-2010cf29811cc52d.png)
这里可以看到,重定向地址上有两个参数code和scope,我们只需要code就行了,这里的code是urlencode后的,使用时需要decode
setp7
使用code换取refreshToken
地址:https://accounts.google.com/o/oauth2/token
请求方式:post
参数:grant_type=authorization_code
code=获取到的code(需要看看code中是否有%号,如果有需要urldecode)
client_id=创建api项目是的clientId(客户端ID)
client_secret=创建api项目时的clientSecret(客户端密钥)
redirect_uri=创建api项目时的重定向地址
![](https://upload-images.jianshu.io/upload_images/3418275-49e8e52764816468.png)
这里就获取到refreshToken了,重点重点重点,refreshToken保存下来,它只会在第一次请求中返回,后续用在发一样的请求不会返回refreshtoken,如果不慎弄丢了,需要去重新创建一个WebClientId
setp8
使用refreshToken获取accessToken
地址:https://accounts.google.com/o/oauth2/token
请求方式:post
参数:grant_type=refresh_token
refresh_token=刚刚获取到的refreshToken
client_id=创建api项目是的clientId(客户端ID)
client_secret=创建api项目时的clientSecret(客户端密钥)
![](https://upload-images.jianshu.io/upload_images/3418275-521fbaf32cef7340.png)
setp9
查询订单状态
https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}?access_token={access_token}
packageName:app包名,必须是创建登录api项目时,创建android客户端Id使用包名
productId:对应购买商品的商品ID
token:购买成功后Purchase对象的getPurchaseToken()
access_token:上面咋们获取到的accessToken
请求方式:get
返回值:
{
"purchaseTimeMillis": "1623980699933",//购买产品的时间,自纪元(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": "SDK2106180944530041",//上面客户支付时的透传字段,google指导是用来存放用户信息的,不能过长,否则客户端不能支付
"obfuscatedExternalProfileId": "",
"regionCode": "HK"
}
到这里整个支付验证流程就已经走完了,这里总结哈笔者这次试用过程中走过的一些坑:
- google应用必须要在封闭测试状态下,并审核通过的应用才能支付,文档说的是内部测试就可以了,笔者每次都弄到封闭测试状态下才可以支付。
- 在firebase中创建了项目,会自动同步到google api后台,不用再去单独创建登录使用的项目
- 登录使用的api项目和查询支付使用的api项目是两个不同的项目相互不干扰,查询支付的api项目一个google play账号对应一个项目,这个google play账号中所有的应用,都可以通过这个查询支付的api项目去查询
- 获取code授权api项目时,要使用google play后台的开发者账号授权
关于RefreshToken过期问题
- api项目-同意屏幕,发布状态为测试(有效期7天)
- RefreshToken 6个月都未使用,这个要维护accessToken的有效性,应该可以不必考虑
- 授权账号改密码了(笔者未测试,修改开发者账号密码是否会导致过期)
- 授权超过50个刷新令牌,最先的刷新令牌就会失效(这里50个应该够用了,除了测试时,可能会授权多个)
- 取消了授权
- 属于具有有效会话控制策略的 Google Cloud Platform 组织