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
最后感慨一下,以前在腾讯的时候,安全问题有大帮人帮你一起查,所以根本感觉不到什么危险。现在只有自己了,所有的问题都要考虑到,而且一旦处理不好就可能是致命的。