电商类app最终都会面临一个问题:如何让用户便捷地在线支付。由于支付行业本身的复杂性和特殊性,大部分公司会选择集成第三方支付。支付宝作为国内最大的第三方支付公司,由于其极其广泛的用户基础,成了很多公司首选的支付方案。支付宝提供了相对完善的sdk和文档,只要按照步骤操作,集成sdk不是件太难的事情,但是sdk背后涉及许多安全相关的内容。如果能认真理解支付宝sdk demo的加密解密逻辑,举一反三,我们也能应用到自己的项目里。本文会从sdk demo提供的源码出发,分析背后的加密解密流程。
SDK需要解决的问题
- 是哪家商户发起了调用?如何保证发起请求的就是那家商家?
- 如何保证商品数据在网络传输中没有被篡改?
解决方案
- 客户端用自己的私钥将特征码加密后,将此数据发给服务器,服务器使用商户的公钥对密文进行解密,如果解密成功可唯一确定这是用商户的私钥加密的密文。只要商户的私钥不被泄露,那么使用私钥的人肯定是那家商户。
- 我们可以对商品数据做
SHA1
签名,目的是得到一个和这个商品信息唯一相关的字符串,然后对这个字符串加密,然后将这个加密后的字符串给支付宝,支付宝拿到商品信息和这个加密后的字符串后,首先解密,还原为SHA1
签名字符串,然后对商品信息进行SHA1
签名,对比这两个SHA1
签名字符串,如果相等,那么这个商品一定没有被篡改过。因为如果信息被篡改,那么这个SHA1
签名肯定不一样;如果有人想要伪造这个SHA1
签名,又需要私钥,所以这就保证了这个商品是你发的。
代码分析
支付宝sdk完整的一次加密解密涉及客户端和服务端两部分
- 客户端通过支付宝sdk发起调用,发送加密后的商品信息
- 支付宝sdk解析数据后跳转到支付宝app显示支付页面,用户执行支付操作
- 服务端解析支付宝支付完成后的回调,包含加密后的支付结果数据
本文分析客户端的实现,也就是上述的1和2:
/*
*生成订单信息
*/
//将商品信息赋予AlixPayOrder的成员变量
Order *order = [[Order alloc] init];
order.partner = partner;
order.sellerID = seller;
order.outTradeNO = [self generateTradeNO]; //订单ID(由商家自行制定)
order.subject = product.subject; //商品标题
order.body = product.body; //商品描述
order.totalFee = [NSString stringWithFormat:@"%.2f",product.price]; //商品价格
order.notifyURL = @"http://www.xxx.com"; //回调URL
这里设置了商品的信息,包括商品标题、商品描述和价格,要注意的是,这里order.notifyURL
是服务端的回调地址,也就是执行上述的第三步。
//将商品信息拼接成字符串
NSString *orderSpec = [order description];
NSLog(@"orderSpec = %@",orderSpec);
//获取私钥并将商户信息签名,外部商户可以根据情况存放私钥和签名,只需要遵循RSA签名规范,并将签名字符串base64编码和UrlEncode
id<DataSigner> signer = CreateRSADataSigner(privateKey);
NSString *signedString = [signer signString:orderSpec];
这里的orderSpec
包含了商品的所有信息。通过CreateRSADataSigner(privateKey)
首先对商品信息执行sha1
签名并执行RSA
加密,其中的privateKey
是商户自己的private key。具体的签名加密是通过下面这个函数执行的
//该签名方法仅供参考,外部商户可用自己方法替换
- (NSString *)signString:(NSString *)string {
......
const char *message = [string cStringUsingEncoding:NSUTF8StringEncoding];
int messageLength = (int)strlen(message);
unsigned char *sig = (unsigned char *)malloc(256);
unsigned int sig_len;
int ret = rsa_sign_with_private_key_pem((char *)message, messageLength, sig, &sig_len, (char *)[path UTF8String]);
//签名成功,需要给签名字符串base64编码和UrlEncode,该两个方法也可以根据情况替换为自己函数
if (ret == 1) {
NSString * base64String = base64StringFromData([NSData dataWithBytes:sig length:sig_len]);
//NSData * UTF8Data = [base64String dataUsingEncoding:NSUTF8StringEncoding];
signedString = [self urlEncodedString:base64String];
}
free(sig);
return signedString;
}
其中rsa_sign_with_private_key_pem
函数实现如下
int rsa_sign_with_private_key_pem(char *message, int message_length
, unsigned char *signature, unsigned int *signature_length
, char *private_key_file_path)
{
SHA1((unsigned char *)message, message_length, sha1);
......
int rsa_sign_valid = RSA_sign(NID_sha1
, sha1, 20
, signature, signature_length
, rsa_private);
return success;
}
通过OpenSSL
提供的SHA1((unsigned char *)message, message_length, sha1);
方法生成了sha1
签名,RSA_sign(NID_sha1, sha1, 20, signature, signature_length, rsa_private)
生成了RSA
加密后的signature
。
//将签名成功字符串格式化为订单字符串,请严格按照该格式
NSString *orderString = nil;
if (signedString != nil) {
orderString = [NSString stringWithFormat:@"%@&sign=\"%@\"&sign_type=\"%@\"",
orderSpec, signedString, @"RSA"];
[[AlipaySDK defaultService] payOrder:orderString fromScheme:appScheme callback:^(NSDictionary *resultDic) {
NSLog(@"reslut = %@",resultDic);
}];
}
这里通过payOrder
发起了对支付宝app的调用。这里的orderSpec
就是商品的明文信息,signedString
是加密后的SHA1
签名, orderString
包含了商品明文信息和加密后的SHA1
签名。payOrder
方法不是开源的,我们可以大致猜测下它的实现。
- 支付宝服务端接收到明文消息
orderSpec
和签名signedString
。 - 支付宝服务端使用商户的公钥和
orderSpec
验证签名signedString
- 如果验签成功,解析商品详细信息,并跳转到支付宝app执行支付流程;如果验签失败,则提示错误信息。
- 用户完成支付,跳回原来的app,并进入客户端
callback
执行NSLog(@"reslut = %@",resultDic);
。至此,客户端sdk这边的工作就完成了。
下一篇文章会基于支付宝提供的PHP demo分析服务端的实现。