由于业务需要,我们需要使用支付宝移动快捷支付做收款。支付宝给了我们《移动快捷支付应用集成接入包支付接口》见支付宝包《WS_SECURE_PAY_SDK》。
支付宝给的服务器demo只有Java、C#、PHP三种,而我们服务器端使用的是C++。这其中就涉及到接收支付宝的服务器异步通知。为了确保接收到的服务器异步通知来至支付宝,我们就必须验证支付宝的签名。坑爹的是,原来PC端使用MD5做签名,估计支付宝考虑到移动端的风险更高,于是改用RSA做移动快捷支付应用的签名。这无疑增加了我们迁移到移动端的开发成本。
支付宝文档中说明是使用openssl,我们这边就决定使用openssl做rsa签名验证。
由于第一次使用openssl做RSA验证签名,我们碰到了各种坑,为了避免其他项目也碰到类似问题,分享如下:
首先要说明的是RSA签名和签名验证的过程。
RSA签名的过程(支付宝操作)如下:对需要签名的字符串按key的字母升序排序,使用=和&连接,形成一个签名字符串。对该字符串做摘要(可以使用MD5或者SHA1,支付宝使用的是SHA1),然后对摘要字符串(即接口中的hash参数)使用支付宝私钥做RSA加密,获得加密字符串,即为签名字符串(放在sign中),设置sign_type=RSA。这样,就完成了发送字符串的签名。
RSA签名验证的过程(我们第三方企业操作)如下:接收到发送过来的字符串(如果字符串没有做url decode解码,需要做url decode解码),拆分为key、value对,按照支付宝的文档,根据key的字母升序排序,使用=和&链接,获得被签名字符串。被签名字符串做SHA1摘要算法,获得SHA1摘要字符串。如果sign_type=RSA,先将sign字段做base64解码,然后使用支付宝公钥做RSA解密,得到SHA1摘要字符串。比较两个SHA1摘要字符串,如果SHA1摘要字符串一致,则签名验证成功。
特别说明的是:支付宝的公钥字符串为以-----BEGIN PUBLIC KEY-----\n开始,以\n-----END PUBLIC KEY-----\n结束,中间的字符串需要每64个字符换行一次,即为:
- -----BEGIN PUBLIC KEY-----
- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCnxj/9qwVfgoUh/y2W89L6BkRA
- FljhNhgPdyPuBV64bfQNN1PjbCzkIM6qRdKBoLPXmKKMiFYnkd6rAoprih3/PrQE
- B/VsW8OoM8fxn67UDYuyBTqA23MML9q1+ilIZwBC2AQ2UBVOrFXfFl75p6/B5Ksi
- NG9zpgmLCUYuLkxpLQIDAQAB
- -----END PUBLIC KEY-----
理论说完了,再解释一下使用的函数吧。
验证签名函数为:int verifyAlipayNotify(const std::string& recvString, const std::string& alipayPublicKey);
recvString为接收的字符串,未做urldecode。
alipayPublicKey为本地内存中存储的支付宝公钥,已经保证包含特殊说明的条件。
使用的openssl函数如下:
int RSA_verify(int type, const unsigned char *m, unsigned int m_length,
const unsigned char *sigbuf, unsigned int siglen, RSA *rsa);
type 使用何种摘要算法,这里由于使用的是SHA1算法,填写NID_sha1
m 摘要字符串
m_length 摘要字符串长度
sigbuf 支付宝返回的签名,已经做了base64解码
siglen 支付宝返回的签名长度,这里应该为128
rsa openssl的RSA密钥结构体,这里由支付宝公钥转化而来的
返回值:负数为执行错误,0为签名验证失败(估计是有黑客攻击你),1为签名验证成功
verifyAlipayNotify代码如下:
- #include <openssl/rsa.h>
- #include <openssl/sha.h>
- #include <openssl/md5.h>
- #include <openssl/rand.h>
- #include <openssl/objects.h>
- #include <openssl/pem.h>
- #include <openssl/bio.h>
- #include <string>
- #include <map>
- #include "urlcodec.h"
- #include "base64.h"
- struct ltstr
- {
- bool operator()(std::string s1, std::string s2) const{return (s1.compare(s2) < 0);}
- };
- int verifyString(const std::string& signString, const std::string& sign, const std::string& alipayPublicKey)
- {
- //获得支付宝的签名字节串
- char szSign[128];
- unsigned long szSignLen = 128;
- bool decodeResult = CBase64::Decode(sign, (unsigned char*)szSign, &szSignLen);//CBase::Decode是Base64解码函数,您可以在网上随便下载一个
- if(!decodeResult)
- {
- return -1;
- }
- //获得SHA1摘要字符串
- unsigned char sha1Origin[20];
- SHA1((unsigned char*)signString.c_str(), signString.size(), sha1Origin);
- //由支付宝公钥内存字符串转化为openssl的RSA结构
- BIO* memBIO = NULL;
- memBIO = BIO_new(BIO_s_mem());
- int bioWriteLen = BIO_write(memBIO, alipayPublicKey.c_str(), alipayPublicKey.length());
- RSA* rsa = PEM_read_bio_RSA_PUBKEY(memBIO, NULL, NULL, NULL);
- if(NULL == rsa)
- {
- return -2;
- }
- //签名验证
- int verifyResult = RSA_verify(NID_sha1, sha1Origin, SHA_DIGEST_LENGTH, (unsigned char*)szSign, szSignLen, rsa);
- return verifyResult;
- }
- int verifyAlipayNotify(const std::string& alipayNotifyData, const std::string& alipayPublicKey)
- {
- std::string strAlipayNotifyData = alipayNotifyData;
- std::string sign;
- std::map<std::string, std::string, ltstr> omap;
- std::string::size_type pos = strAlipayNotifyData.find("&");
- while(std::string::npos != pos)
- {
- std::string one = strAlipayNotifyData.substr(0, pos);
- std::string::size_type subpos = one.find("=");
- if(std::string::npos != subpos)
- {
- std::string key = one.substr(0, subpos);
- if("sign_type" != key && "sign" != key)
- {
- std::string value = one.substr(subpos+1);
- std::string newValue = UrlDecode(value);//UrlDecode是URL解码函数,您可以在网上随便下载一个
- omap.insert(std::make_pair(key, newValue));
- }
- else if("sign" == key)
- {
- sign = UrlDecode(one.substr(subpos+1));
- }
- }
- strAlipayNotifyData = strAlipayNotifyData.substr(pos + 1);
- pos = strAlipayNotifyData.find("&");
- }
- std::string::size_type subpos = strAlipayNotifyData.find("=");
- if(std::string::npos != subpos)
- {
- std::string key = strAlipayNotifyData.substr(0, subpos);
- if("sign_type" != key && "sign" != key)
- {
- std::string value = strAlipayNotifyData.substr(subpos+1);
- std::string newValue = UrlDecode(value);
- omap.insert(std::make_pair(key, newValue));
- }
- else if("sign" == key)
- {
- sign = UrlDecode(strAlipayNotifyData.substr(subpos+1));
- }
- }
- //获得支付宝被签名字符串
- std::string signString = "";
- std::map<std::string, std::string, ltstr>::iterator itr = omap.begin();
- for(; itr != omap.end(); ++itr)
- {
- signString += itr->first;
- signString += "=";
- signString += itr->second;
- signString += "&";
- }
- if(!signString.empty())
- {
- signString.erase(signString.length() - 1);
- }
- return verifyString(signString, sign, alipayPublicKey);
- }
有时候,你本地存储的公钥是没有包含头尾的,如
- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCnxj/9qwVfgoUh/y2W89L6BkRAFljhNhgPdyPuBV64bfQNN1PjbCzkIM6qRdKBoLPXmKKMiFYnkd6rAoprih3/PrQEB/VsW8OoM8fxn67UDYuyBTqA23MML9q1+ilIZwBC2AQ2UBVOrFXfFl75p6/B5KsiNG9zpgmLCUYuLkxpLQIDAQAB
为此,提供一个函数支持转化为完整公钥的函数:
- std::string completeAlipayPublicKey(std::string strPublicKey)
- {
- int nPublicKeyLen = strPublicKey.size(); //strPublicKey为base64编码的公钥字符串
- for(int i = 64; i < nPublicKeyLen; i+=64)
- {
- if(strPublicKey[i] != '\n')
- {
- strPublicKey.insert(i, "\n");
- }
- i++;
- }
- strPublicKey.insert(0, "-----BEGIN PUBLIC KEY-----\n");
- strPublicKey.append("\n-----END PUBLIC KEY-----\n");
- return strPublicKey;
- }
最后,测试代码如下:
- int main(int argc, char **argv)
- {
- std::string strPublicKey = "**********";
- std::string strAlipayData = "**********";
- std::string strCompletePublicKey = completeAlipayPublicKey(strPublicKey);
- int result = verifyAlipayNotify(strAlipayData, strCompletePublicKey);
- if(1 == result)
- {
- printf("verify sign ok!\n");
- }
- else if(0 == result)
- {
- printf("mock alipay notify data");
- }
- else
- {
- printf("error\n");
- }
- return 0;
- }