浅谈支付加密及签名验证

如果说做支付接入最基本的要求就是安全、稳定、快捷,那么数据安全则是支付接入的核心.只要涉及到支付,必然会对数据的准确性特别敏感.根据不同的应用场景,不同的支付渠道商会对自己的支付加密方式有不同的见解.

以MD5加密方式系列

以MD5(Message-Digest Algorithm 5)加密作为支付加密最为普遍,尽管网上也有相应的”破解方法”.使用MD5加密作为支付加密方案的渠道,加密的区别在于分配给接入商不同的签名Key.这类的加密基本可以归纳为以下几个步骤:

  1. 指定待加密参数有哪些,或者哪些参数不参与加密,比如签名参数signature不参与签名
  2. 待加密参数是否按照键排序,一般是升序
  3. 指定待加密参数是否要做转码转译,特别是针对某些参数值是中文.比如自定义参数,需要urlencode
  4. 加密的字符串的拼接方式及其连接符.这部分的拼接方式比较多,有些只要参数值拼接起来不需要任何连接符,有的需要参数的键和值之间以”=“拼接而参数直接以”&“拼接.
  5. 将待加密的拼接好的字符串与约定好的签名Key拼接后做MD5,加密的结果一般需要转成小写.最终的结果作为校验的签名.
  6. 得到的签名值与参数中的指定的参数比对,相同则验证成功.

以新浪联运平台游戏支付为例

新浪联运平台游戏支付接口签名机制中,关于签名步骤如下:

a)将所有待签名参数按参数名排序(字母字典顺序,例如PHP的ksort()函数)
b)把数组所有元素,按照“参数|参数值”的模式用“|”字符拼接成字符串,组成字符串A
c)将字符串A与 appsecret,用英文竖杠进行连接, 得到字符串B,对字符串B取sha1值,得到字符串C,C就是所需要的签名

值得说明的是,新浪支付把支付结果(POST方式)通知给开发者配置的支付通知地址(回调地址),加密方式用的是sha1.签名代码实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
public static function buildRequestMysign($secret)
{
if (empty($_REQUEST)) return FALSE;
if (isset($_REQUEST['signature'])) unset($_REQUEST['signature']);
ksort($_REQUEST);
$strSignature = '';
foreach ($_REQUEST as $key => $value){
$strSignature .= sprintf('%s|%s|', $key, $value);
}
return sha1($strSignature.$secret);
}


以AnySDK第三方接入平台为例

AnySDK作为第三方支付平台,其角色处于支付渠道商与接入方之间的一道桥梁.其原理是渠道通知AnySDK服务端,再由AnySDK服务端通知接入方.因此在渠道后台需要配置AnySDK提供的充值回调,在AnySDK后台配置自己的充值回调.
AnySDK这么处理目的就是为了尽量统一现有的支付平台不规范支付方式,对于专注于产品研发的企业而言的确是不可多得的福利.相比于其他支付渠道商,最大的特点在于它有两步签名验证机制.

a) 准备验签密钥 private_key
b) 对所有不为空的参数按照参数名字母升序排列,sign参数不参与签名;
c) 将排序后的参数名对应的参数值字符串方式按顺序拼接在一起(所有参数);
d) 做一次md5处理并转换成小写,得到的加密串1;
e) 在加密串1末尾追加private_key,做一次md5加密并转换成小写,得到的字符串就是签名sign的值
f) 得到的签名值与参数中的sign值比对,相同则验证成功.

值得说明的是,AnySDK支付的所有测试可以在后台根据需要配相应的参数测试,通过后台模拟充值方便调试.以下是核心签名代码实现:

1
2
3
4
5
6
7
8
9
10
11
function checkSign($data, $privateKey) {
if (empty($data) || !isset($data['sign']) || empty($privateKey)) {
return false;
}
$sign = $data['sign'];
$_sign = $this->getSign($data, $privateKey);
if ($_sign != $sign) {
return false;
}
return true;
}

值得特别注意的是,有时候在测试的时候会出现客户端已经充值了但是后台显示却是 “充值中“,这时要特别注意AnySDK的相关说明.对于没有收到渠道通知的情况,官方的解释如下:

1.用户打开支付界面后并没有支付直接关掉;
2.渠道后台配置的支付通知地址并不是正确的AnySDK的地址;
3.渠道出问题没通知或延迟通知.可先跟渠道那边确认是否有通知,通知到哪个地址.

以RSA非对称加密算法系列

通过双方约定好的公钥和私钥加密作为支付方式,作为一种非对称加密方式其安全性会更高.

1.所谓的非对称加密算法,就是加密和解密使用的是两个不同的密钥,即公钥和私钥.公钥和私钥是一对密钥.
2.如果用公钥加密的数据,只有使用对应的私钥才能解密.如果用私钥加密的数据,只有使用对应的公钥才能解密.


以游戏多游戏支付接入为例

游戏多号称是中国第一多端手游用户互动平台,在同行中率先完成ios、android、html5、pc多端全平台布局,其加密方式采用的便是公私钥非对称加解密.其验证机制基本分为以下几个步骤:

a) 游戏多传给充值回调链接传递参数.其中包括deliveryCode,checkCode,sign.
b) 游戏厂商在接到充值回调时,需要将deliveryCode先私钥解密后再通过查询通知接口进行确认,以防伪造通知.
c) 对于sign签名验证,其基本的验证方式可参考MD5签名验证,并改用公钥加密进行验证.

游戏多采用非对称加密算法,以”公钥加密私钥解密”的方式通过查询通知接口确认订单成功与否.其核心代码如下所示:

1.私钥解密:

1
2
3
4
5
function getRsaDecryptByKeyPrivate($data,$keyPrivate){
$keyPrivate = file_get_contents($keyPrivate);
openssl_private_decrypt($this->urlSafe_base64_decode($data),$decrypted,$keyPrivate);
return $decrypted;
}

2.公钥加密:

1
2
3
4
5
function getRsaEncryptByKeyPublic($data,$keyPublic){
$keyPublic = file_get_contents($keyPublic);
openssl_public_encrypt($data,$crypted,$keyPublic);
return $this->urlSafe_base64_encode($crypted);
}

3.URL安全形式的Base64编码:

1
2
3
4
5
6
7
8
9
10
11
function urlSafe_base64_encode($str){
$strFind = array("+","/");
$strReplace = array("-","_");
return str_replace($strFind,$strReplace,base64_encode($str));
}
function urlSafe_base64_decode($code){
$strFind = array("+","/");
$strReplace = array("-","_");
return base64_decode(str_replace($strReplace,$strFind,$code));
}

不过,在与游戏多对接支付的时候文档上描述模棱两可造成很多误解.如有不足之处,以后再加以改正.

其他支付验证方式

国内接入的充值接口,其支付充值签名验证主要是通过接入方验证方式实现.而国外的主流平台,验证方式主要还是通过接入方服务端验证实现.

以苹果支付验证为例

苹果内购(IPA)是指IOS在沙箱环境下购买成功之后,服务端会返回四个参数给应用,应用需要向苹果服务端进行二次验证,以确认是否购买成功.

1.产品标识符: product Identifier.即在itunes store应用内定义的产品ID
2.交易状态: state.分别是Purchased(成功)、Restored(恢复购买)、Failed(失败)、Deferred(待确认) 四个状态
3.交易收据: Receipt.每笔订单的唯一交易收据,作为二次验证的重要依据
4.交易标识符: transaction Identifier.

苹果内购的二次验证的基本流程,根据内购是否是沙盒模式大致分为如下几步:

a.先将Receipt通过苹果的正式服务器验证.如果苹果返回的状态码是21007,说明该Receipt属于SandBox Receipt,但却”错误”发送至生产系统的验证服务.
b.如果交易属于沙盒模式,需要重新把Receipt通过苹果的沙盒测试服务器验证.
c.Receipt通过相应的苹果服务器验证之后,将会收到相应的返回以验证用户是否购买成功.

关于苹果内购二次验证返回的状态码,其相应的解释如下所示:

状态码 描述
21000 App Store不能读取你提供的JSON对象
21002 receipt-data域的数据有问题
21003 receipt无法通过验证
21004 提供的shared secret不匹配你账号中的shared secret
21005 receipt服务器当前不可用
21006 receipt合法,但是订阅已过期。服务器接收到这个状态码时,receipt数据仍然会解码并一起发送
21007 receipt是Sandbox receipt,但却发送至生产系统的验证服务
21008 receipt是生产receipt,但却发送至Sandbox环境的验证服务

苹果内购二次验证的核心代码如下所示:

  1. 向苹果服务器端提交内购二次验证:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function curlByIOSPay($receipt_data, $sandbox=0){
$fieldPost = array("receipt-data" => $receipt_data);
$jsonFieldPost = json_encode($fieldPost);
$url_buy = "https://buy.itunes.apple.com/verifyReceipt";
$url_sandbox = "https://sandbox.itunes.apple.com/verifyReceipt";
$url = $sandbox ? $url_sandbox : $url_buy;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonFieldPost);
curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}

2.苹果内购二次验证流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
function verifyPaySign($arrData){
if(!empty($arrData)){
$tokenPay = $arrData['paytoken'];
if(strlen($tokenPay) >= 20){
$arrVerifyPayGetIfSandbox = json_decode($this->paySDK->curlByIOSPay($tokenPay),true);
if($arrVerifyPayGetIfSandbox['status'] == '21007'){
$arrVerifyPayGetIfSandbox = json_decode($this->paySDK->curlByIOSPay($tokenPay,1),true);
}
}
}
$arrRst = empty($arrVerifyPayGetIfSandbox) ? array() : $arrVerifyPayGetIfSandbox;
return json_encode($arrRst);
}

以谷歌充值验证为例

与苹果内购相类似的,谷歌内购几乎也走相近的验证方式.当用户通过客户端完成充值之后,谷歌服务器会通过充值回调链接下发通知给服务端.服务端再通过这些数据进行验证,从而判断该笔订单的合法性.这对于受尽国外某些伪订单肆虐之苦的商家而言无疑是一个保障.

谷歌服务器回调参数格式

当用户在客户端完成充值之后,谷歌服务器会自动往相应的服务端下发固定格式的参数.如下所示:

参数 说明
nonce
order 订单详情(json对象)

关于订单详情的json对象,其固定的数据格式如下所示:

参数 说明
consumptionState 消费状态
developerPayload 谷歌开发者特殊单号字符串
kind 支付类型
purchaseState 支付状态:0表示已支付,1表示取消支付
purchaseTimeMillis 支付时的时间

谷歌验证的基本流程

谷歌API类库的引入

谷歌验证的核心代码

  • 对于谷歌旧版API类库(Google API V2)

谷歌API类库是对谷歌当前的所有API的封装,但是对于谷歌内购验证部分而言并非所需要.所以,我们需要有目的的调用其中的某些API即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class androidPublisher{
private $client;
function __construct($arrPaySetting) {
$dirKeyFile = dirname(__FILE__)."/p12/";
$keyFileName = $dirKeyFile.$arrPaySetting["keyFileName"];
require_once (dirname(__FILE__) . '/autoload.php');
$this->client = new Google_Client();
$this->client->setApplicationName($arrPaySetting["applicationName"] );
$this->client->setClientId($arrPaySetting["idClient"]);
$key = file_get_contents($keyFileName);
$service_account_name = $arrPaySetting["accountName"];
$auth = new Google_Auth_AssertionCredentials( $service_account_name, array('https://www.googleapis.com/auth/androidpublisher'), $key);
$this->client->setAssertionCredentials($auth);
}
function verifyAndroidPublisher($idApp,$idProduct,$purchaseToken){
try {
$externalAppId=$idApp;
$externalProductId=$idProduct;
$purchaseToken=array($purchaseToken);
require_once (dirname(__FILE__) . '/autoload.php');
$service = new Google_Service_AndroidPublisher($this->client);
$googleApiResult = $service->purchases_products->get($externalAppId,$externalProductId,$purchaseToken);
if(array_key_exists('purchaseState',$googleApiResult) && $googleApiResult['purchaseState'] == 0){
$rst = 1;
}else{
$rst = 0;
}
} catch (Exception $e){
$rst = 0;
}
return $rst;
}
}
  • 对于谷歌最新版本API类库(Google API V3)

谷歌支付跳过的那些坑

  • 请求该支付验证接口之前,谷歌需要先验证发起该请求的用户权限,所以需要根据文档提示的要求给予合理的权限.
  • 提起”争议”,获取钻石后全身而退.
    谷歌支付验证接口虽然可以有效的过滤到某些别有用心的人刻意的假单,但就算是走了正确的充值流程也会让玩家完全可以”免费”拿走钻石并且全身而退的情况.玩家充值之后并未付款而是待钻石收入囊中之后,向谷歌售后服务人员申请”未到账”的争议,而这些”愚蠢”的售后人员非但不会与卖家联系了解情况,反而直接把账单取消掉了.

未完待续

以上是关于充值过程支付加密及其签名验证机制的一些总结和看法,有很多的不足须待以后逐渐优化和完善.

参考资料

文章目录
  1. 1. 以MD5加密方式系列
    1. 1.1. 以新浪联运平台游戏支付为例
    2. 1.2. 以AnySDK第三方接入平台为例
  2. 2. 以RSA非对称加密算法系列
    1. 2.1. 以游戏多游戏支付接入为例
  3. 3. 其他支付验证方式
    1. 3.1. 以苹果支付验证为例
    2. 3.2. 以谷歌充值验证为例
      1. 3.2.1. 谷歌服务器回调参数格式
      2. 3.2.2. 谷歌验证的基本流程
      3. 3.2.3. 谷歌API类库的引入
      4. 3.2.4. 谷歌验证的核心代码
      5. 3.2.5. 谷歌支付跳过的那些坑
  4. 4. 未完待续
  5. 5. 参考资料