2020-4-25更新
google提供了更方便的服务账号(Service Account)的方案,具体方式如下:
1. 去google developer api后台(https://console.developers.google.com/)
1. 创建 project
2. 进入创建好的project,找到 Google Play Android Developer API,并启用
3. 在project中,创建服务账号(service account)
4. 在service account中创建密钥,并下载json格式的密钥文件
2. 去google play console(https://play.google.com/apps/publish/)
1. 点击左下角的设置=>API权限,选择关联的项目
2. 给服务账号授予财务角色的权限
3. 等待24小时生效
注意:
1. 一定要先关联项目,再创建内购商品,否则无法校验。
如果之前已经创建了商品,只能重新创建。注意,用过的product_id即使删掉也无法再用。
python库:
pip install google-api-python-client
2015-4-3更新
1. 将原有分散的代码封装为通用库 ggpay,代码在: https://github.com/dantezhu/ggpay,也可以直接 pip install ggpay 进行安装,使用方法见 examples 的get_token。
最近在google play上线的应用内支付被人刷了,用户模拟发起了大量的支付请求,并且全部成功支付。搞得我最近茶饭不思。。今天总算是解决了,和大家分享一下。
我们客户端的支付实现步骤是:
1. app端调用google支付
2. 支付成功后,调用 自己服务器的发货接口,当然发货接口是做了签名校验的。
之所以在app端调用发货,是因为google貌似没有提供服务器端直接回调url的地方,所以才给了恶意用户模拟google返回的机会。
一开始我以为是我们自己的发货接口密钥被破解了,但是后来经过app上报,发现客户端是真实的走过了所有的google支付流程,即google的支付sdk真的返回了成功。
由于不清楚是因为google的密钥泄漏还是攻击者用别的方法实现,所以客户端这边已经没有办法确认是安全的了。
好在google是提供了查询订单的接口的: http://developer.android.com/google/play/billing/gp-purchase-status-api.html
实现的流程在文档中已经写的很清楚了,我这里就不赘述了。
判断的方法也很简单:
1. 判断是否购买成功
2. 判断返回 developerPayload 是否与传入的值一致。最好传入订单号,以防止重放攻击。
实现代码如下:
# -*- coding: utf-8 -*-
import requests
import datetime
from .vals import logger
class GooglePurchaseChecker(object):
"""
google的支付查询
"""
client_id = None
client_secret = None
refresh_token = None
access_token = None
access_token_create_time = None
access_token_expire_time = None
def __init__(self, client_id, client_secret, refresh_token):
self.client_id = client_id
self.client_secret = client_secret
self.refresh_token = refresh_token
def get_new_access_token(self):
"""
通过refresh_token获取access token
"""
base_url = 'https://accounts.google.com/o/oauth2/token'
data = dict(
grant_type='refresh_token',
client_id=self.client_id,
client_secret=self.client_secret,
refresh_token=self.refresh_token,
)
try:
rsp = requests.post(base_url, data=data)
jdata = rsp.json()
if 'access_token' in jdata:
self.access_token = jdata['access_token']
self.access_token_create_time = datetime.datetime.now()
self.access_token_expire_time = self.access_token_create_time + datetime.timedelta(
seconds=jdata['expires_in'] * 2 / 3
)
return True
else:
logger.error('no access_token: %s', rsp)
return False
except:
logger.error('fail', exc_info=True)
return False
def should_get_new_access_token(self):
"""
判断是否要重新获取access_token
"""
if not self.access_token:
return True
now = datetime.datetime.now()
if now >= self.access_token_expire_time:
return True
return False
def check_purchase(self, bill_id, package_name, product_id, purchase_token):
"""
判断是否合法
"""
logger.error('purchase check start.bill_id: %s', bill_id)
if self.should_get_new_access_token():
if not self.get_new_access_token():
# 如果没有成功获取到access_token,也先认为成功吧
logger.error('get_new_access_token fail. bill_id:%s', bill_id)
return -1
url_tpl = 'https://www.googleapis.com/androidpublisher/v1.1/applications/{packageName}/inapp/{productId}/purchases/{token}'
url = url_tpl.format(
packageName=package_name,
productId=product_id,
token=purchase_token,
)
rsp = requests.get(url, params=dict(
access_token=self.access_token,
))
jdata = rsp.json()
if 'purchaseState' not in jdata:
logger.error('purchase invalid.bill_id: %s jdata: %s', bill_id, jdata)
return -2
if jdata['purchaseState'] == 0 and jdata['developerPayload'] == 'DeveloperPayloadITEM%s' % bill_id:
logger.error('purchase valid.bill_id: %s jdata: %s', bill_id, jdata)
return 0
logger.error('purchase invalid.bill_id: %s jdata: %s', bill_id, jdata)
return -3
最后感慨一下,以前在腾讯的时候,安全问题有大帮人帮你一起查,所以根本感觉不到什么危险。现在只有自己了,所有的问题都要考虑到,而且一旦处理不好就可能是致命的。
李雪冰 on #
敢问博主现在在哪里上班,还是自己出来做?
Reply
李振华 on #
vimer您好, 我们也遇到这种问题, 请问一下refresh_token 这个token是从哪里来的呢?是支付返回的purchaseToken吗?
Reply
phper08 on #
第一次获取access_token的时候得到,在后面获取access_token的时候不再返回refresh_token,所以要保存好,当然移除了应用授权之后再获取access_token也是可以得到refresh_token的,在https://security.google.com/settings/security/permissions 这里删除授权
Reply
李振华 on #
可以跟您交流一下吗? 我们也是在服务器端做了校验, 但是发现有用户模拟了大量可以通过RAS校验的订单数据,所以现在也只能用googleplay查询订单的方式来做验证了。但我们用了service account的JWT方式,获取token的时候总是报400无效授权, 所以想用您这种方式试试
Reply
Dante on #
你好没问题哈,其实我代码都贴在文章里的,应该直接拿过去用就行啦
Reply
wangying on #
你好,我按照上面的方法调用https://www.googleapis.com/androidpublisher/v1.1/applications/{packageName}/inapp/{productId}/purchases/{token} 这个的时候总是报 { "error": { "errors": [ { "domain": "global", "reason": "invalid", "message": "Invalid Value" } ], "code": 400, "message": "Invalid Value" }} 这个错误,这个订单的purchase_token 是客户端成功后返回的,请问怎么解呢?谢谢
Reply
xiaoqiang on #
请问博主,初始化时的三个参数(client_id, client_secret, refresh_token)都传什么啊?没看明白这块,麻烦帮忙解答下吧,万分感谢!这是客户端收到的谷歌返回的两个对象IabResult result, Purchase purchasepublic class IabResult { int mResponse; String mMessage;}public class Purchase { String mOrderId; String mPackageName; String mSku; long mPurchaseTime; int mPurchaseState; String mDeveloperPayload; String mToken; String mOriginalJson; String mSignature;}
Reply
xiaoqiang on #
博主,我在google通过通过Create Client ID 得到了一组json数据,下面是这组数据所有的key['auth_uri', 'redirect_uris', 'client_email', 'client_id', 'token_uri', 'client_secret', 'auth_provider_x509_cert_url', 'javascript_origins', 'client_x509_cert_url']其中client_id, client_secret找到了,但是 refresh_token还是不清楚应该传什么啊
Reply
Dante on #
https://accounts.google.com/o/oauth2/token 通过这个接口兑换回来的
Reply
裴星鑫 on #
是不是要客户端发起来获得?
Reply
Dante on #
参见: https://developers.google.com/accounts/docs/OAuth2InstalledApp#formingtheurl, 里面有个 grant_type=authorization_code 的部分就是
Reply
xiaoqiang on #
请问博主,这个里面的code是怎么得到的啊,我的数据里面没有找到code=4/v6xr77ewYqhvHSyW6UJ1w7jKwAzu&client_id=8819981768.apps.googleusercontent.com&client_secret=your_client_secret&redirect_uri=https://oauth2-login-demo.appspot.com/code&grant_type=authorization_code
Reply
xiaoqiang on #
感谢博主,这个校验我搞清楚是什么原理了,现在在做的是一个手机游戏在google play的支付校验,现在需要对方提供client_id, client_secret和redirect_uri,但是对方说没有这些。。。能问下博主,这些参数应该在哪里可以找到么,拜谢了!
Reply
肥皂 on #
博主,获取refresh_token的时候需要用户登陆授权么~还是怎么样可以通过发消息就获得呢~求解~
Reply
郭明 on #
refresh_token就是code吗?这个是客户端去获得还是服务器去获得?
Reply
Dante on #
用开发者帐号登录授权,就可以拿到对应的code和refreshcode
Reply
郭明 on #
code每次得到的值都不一样啊然后再获得refreshcode时,总是返回{error,invialid_request}
Reply
郭明 on #
code需要重定向得到,然后用code得到refresh_token。。。是这样吧但code怎么能在游戏逻辑里去跟googleplay请求呢?
Reply
郭明 on #
The browser will be redirected to your redirect URI with a code parameter, which will look similar to 4/eWdxD7b-YSQ5CNNb-c2iI83KQx19.wp6198ti5Zc7dJ3UXOl0T3aRLxQmbwI.这个code明显是通过浏览器来获取的,您是怎么处理的能完整的说下吗?多谢~
Reply
牵着的x右手 on #
google api 使用 https://accounts.google.com/o/oauth2/token 已经获取不到 refresh_token 了?就是说你写的那段代码没用了是吗?只能通过异步验证:每天手工登录生成access_token,然后批量检查昨日订单,有问题的用户进行封号处理?如何手工登录生成access_token?又如何批量检查昨日订单
Reply
Dante on #
ggpay中readme有写
Reply
lidashuang on #
refresh_token 不会过期吧
Reply