类似第三方接入这种事情,所能够参考的比较有价值的资料就是文档和demo,如果它们描述地足够清晰,那接入起来当然是件非常容易的事情。相对来说,阿里提供的文档和demo都比较清晰。在这之前我也做过其他的第三方接入,一次是官方文档和给的demo对不上,对接的时候还得找官方的接入人员咨询,非常蛋疼,对接成功后上线了一段时间又发现了问题,原来对方又把接口给改了,返回数据都跟以前的不一致,我艹~~
官方提供的服务端demo有三种语言实现:C#、Java和PHP,这基本能够满足大多数开发的需求,当然语言都是相通的,用其它语言也可以实现相同的功能,比如用C/C++来完成,本篇博文重点总结下用C/C++做支付宝无线支付后台接入时所遇到的问题和解决方法。
交互流程
数据交互的流程就不赘述了,文档上描述地很清楚,借用一下文档上的交互时序图:
这样一看其实服务端所要完成的事情还是比较简单的,即处理支付宝的异步回调,异步回调默认当交易成功或支付成功时触发。
异步回调逻辑
异步回调究竟要完成什么内容呢?查看官方的demo就大致明白了,简单来说可以分为三个部分:数据校验、业务逻辑和回调返回。
数据校验
数据校验具体又可以分为两部分,
校验notify_id和
sign。校验notify_id的目的在于鉴别请求是否由支付宝发起,而校验sign的目的在于检验数据是否被篡改。
校验notify_id的方法比较容易实现,需要你构造一个URL(支付宝校验地址+partner id+notify_id),并向这个URL发起HTTP GET请求,如果返回的数据是true,则校验成功,否则失败,具体信息可以参考文档10 如何验证是否支付宝请求。HTTP请求的实现可以使用
libcurl,参考官方提供的这个
demo。
校验sign有三步,请求参数过滤(空值、sign与sign_type)、提取待签名字符串和签名校验,具体可以参考官方提供的demo,如Java demo中的AlipayNotify.java中getSignVeryfy方法,也可以参考文档9 签名机制。查看阿里商户后台,猜测它应该支持两种加密算法:RSA和DSA,我采用的是RSA,有关RSA的细节可以参考
wiki和阮一峰关于
RSA算法原理的介绍。
校验sign的前两步用STL就能够实现,至于签名校验的部分我用的
OpenSSL库来完成这部分的内容。虽然这货前段时间曝漏洞很频繁,但鉴于大家对它的关注度比较高,相信未来它会发展的越来越好。目前OpenSSL提供的文档比较稀缺,代码看起来也比较费劲,当然是我水平比较有限。这部分需要关注的有以下几步:
(1)公钥编码,读取之前要对公钥进行Base64解码,有开源的
Base64编解码实现;
(2)RSA公钥读取(从内存),用到了
BIO_new_mem_buf和
PEM_read_bio_RSA_PUBKEY;
(3)RSA验签,重点要用到的API有EVP_VerifyInit_ex、EVP_VerifyUpdate和EVP_VerifyFinal,附下
文档链接。
需要注意的是RSA公钥字符串的格式,以“-----BEGIN PUBLIC KEY-----\n”开头,以“-----END PUBLIC KEY-----”结束,中间每隔64个字符要加上换行符,多出的24个字符最后也要加上换行,否则会在读取时报错。
URL encode可以使用libcurl提供的
curl_easy_escape函数。
业务逻辑和回调返回的部分就不赘述了,按照demo的格式来就行。
订单数据构造
如果考虑安全性,我们还能够做更多的事情。比如订单数据构造的部分完全由后台来完成。这部分内容要参考客户端demo的代码,读取私钥可以使用
PEM_read_bio_RSAPrivatekey直接从内存中读取(注意读取前也要对私钥做Base64解码,格式与生成的PKCS8格式私钥保持一致就行);用到的加密函数有EVP_SignInit、EVP_SignUpdate和EVP_SignFinal,文档上有相关函数的
说明;加密完成后需要对密文做Base64编码。
还有一点需要强调的是程序中所用到的公钥是阿里后台生成的公钥,在你把公钥上传上去后它会生成一个新的公钥。阿里返回的数据是经过它的私钥加密的,所以在验签阿里的数据是需要使用它提供的公钥,而不是你用openssl生成的公钥。
可以参考如下代码,也可以直接在gist上
下载(需翻墙)
#ifndef __ALIPAY_H__
#define __ALIPAY_H__
#include "base64/base64.h"
#include <curl/curl.h>
#include <map>
#include <openssl/bio.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/err.h>
class Rsa {
public:
static bool verify(const char *public_key,
const string &content, const string &sign) {
BIO *bufio = NULL;
RSA *rsa = NULL;
EVP_PKEY *evpKey = NULL;
bool verify = false;
EVP_MD_CTX ctx;
int result = 0;
string decodedSign = base64_decode(sign);
char *chDecodedSign = const_cast<char*>(decodedSign.c_str());
bufio = BIO_new_mem_buf((void*)public_key, -1);
if (bufio == NULL) {
ERR("BIO_new_mem_buf failed");
goto safe_exit;
}
rsa = PEM_read_bio_RSA_PUBKEY(bufio, NULL, NULL, NULL);
if (rsa == NULL) {
ERR("PEM_read_bio_RSA_PUBKEY failed");
goto safe_exit;
}
evpKey = EVP_PKEY_new();
if (evpKey == NULL) {
ERR("EVP_PKEY_new failed");
goto safe_exit;
}
if ((result = EVP_PKEY_set1_RSA(evpKey, rsa)) != 1) {
ERR("EVP_PKEY_set1_RSA failed");
goto safe_exit;
}
EVP_MD_CTX_init(&ctx);
if (result == 1 && (result = EVP_VerifyInit_ex(&ctx,
EVP_sha1(), NULL)) != 1) {
ERR("EVP_VerifyInit_ex failed");
}
if (result == 1 && (result = EVP_VerifyUpdate(&ctx,
content.c_str(), content.size())) != 1) {
ERR("EVP_VerifyUpdate failed");
}
if (result == 1 && (result = EVP_VerifyFinal(&ctx,
(unsigned char*)chDecodedSign,
decodedSign.size(), evpKey)) != 1) {
ERR("EVP_VerifyFinal failed");
}
if (result == 1) {
verify = true;
} else {
ERR("verify failed");
}
EVP_MD_CTX_cleanup(&ctx);
safe_exit:
if (rsa != NULL) {
RSA_free(rsa);
rsa = NULL;
}
if (evpKey != NULL) {
EVP_PKEY_free(evpKey);
evpKey = NULL;
}
if (bufio != NULL) {
BIO_free_all(bufio);
bufio = NULL;
}
return verify;
}
static string sign(const char *private_key,
const string &content) {
BIO *bufio = NULL;
RSA *rsa = NULL;
EVP_PKEY *evpKey = NULL;
bool verify = false;
EVP_MD_CTX ctx;
int result = 0;
unsigned int size = 0;
char *sign = NULL;
string signStr = "";
bufio = BIO_new_mem_buf((void*)private_key, -1);
if (bufio == NULL) {
ERR("BIO_new_mem_buf failed");
goto safe_exit;
}
rsa = PEM_read_bio_RSAPrivateKey(bufio, NULL, NULL, NULL);
if (rsa == NULL) {
ERR("PEM_read_bio_RSAPrivateKey failed");
goto safe_exit;
}
evpKey = EVP_PKEY_new();
if (evpKey == NULL) {
ERR("EVP_PKEY_new failed");
goto safe_exit;
}
if ((result = EVP_PKEY_set1_RSA(evpKey, rsa)) != 1) {
ERR("EVP_PKEY_set1_RSA failed");
goto safe_exit;
}
EVP_MD_CTX_init(&ctx);
if (result == 1 && (result = EVP_SignInit_ex(&ctx,
EVP_sha1(), NULL)) != 1) {
ERR("EVP_SignInit_ex failed");
}
if (result == 1 && (result = EVP_SignUpdate(&ctx,
content.c_str(), content.size())) != 1) {
ERR("EVP_SignUpdate failed");
}
size = EVP_PKEY_size(evpKey);
sign = (char*)malloc(size+1);
memset(sign, 0, size+1);
if (result == 1 && (result = EVP_SignFinal(&ctx,
(unsigned char*)sign,
&size, evpKey)) != 1) {
ERR("EVP_SignFinal failed");
}
if (result == 1) {
verify = true;
} else {
ERR("verify failed");
}
signStr = base64_encode((const unsigned char*)sign, size);
EVP_MD_CTX_cleanup(&ctx);
free(sign);
safe_exit:
if (rsa != NULL) {
RSA_free(rsa);
rsa = NULL;
}
if (evpKey != NULL) {
EVP_PKEY_free(evpKey);
evpKey = NULL;
}
if (bufio != NULL) {
BIO_free_all(bufio);
bufio = NULL;
}
return signStr;
}
private:
static void ERR(const string &pre) {
ERR_load_crypto_strings();
char buf[512];
ERR_error_string_n(ERR_get_error(), buf, sizeof buf);
// log error here
}
};
// partner number, start with 2088
#define PARTNER ""
// alipay verify url, used to check notify_id
// to confirm the data is sent by alibaba
#define HTTPS_VERIFY_URL "https://mapi.alipay.com/gateway.do?service=notify_verify"
// our private key, PKCS#8 format
#define PRIVATE_KEY ""
// alipay public key, used to check data from alibaba
#define PUBLIC_KEY ""
#define SUBJECT ""
#define SELLER_ID ""
#define BODY ""
class Alipay {
public:
/* verify callback data */
static bool verify(CgiUtil &cgi) {
string responseTxt = "true";
string notifyId = cgi.get_str("notify_id", "");
if (!notifyId.empty()) {
responseTxt = verify_response(notifyId);
}
string sign = cgi.get_str("sign", "");
bool isSign = get_sign_verify(cgi, sign);
if (isSign && responseTxt == "true") {
return true;
}
return false;
}
static string get_order(const string &oid,
unsigned int price, const string &cb_url) {
string orderInfo = get_order_info(oid, price, cb_url);
// we just support RSA right now, don't bother
string sign = Rsa::sign(PRIVATE_KEY, orderInfo);
stringstream ss;
ss << orderInfo
<< "&sign=\""
<< url_encode(sign)
<< "\"&"
<< "sign_type=\"RSA\"";
return ss.str();
}
private:
static string get_order_info(const string &oid,
unsigned int price, const string &cb_url) {
float payMoney = (float)price / 100.0;
string encode_url = url_encode(cb_url);
stringstream ss;
ss << "service=\"mobile.securitypay.pay\"&"
<< "partner=\""
<< PARTNER
<< "\"&"
<< "_input_charset=\"utf-8\"&"
<< "notify_url=\""
<< encode_url
<< "\"&"
<< "out_trade_no=\""
<< oid
<< "\"&"
<< "subject=\""
<< SUBJECT
<< "\"&"
<< "payment_type=\"1\"&"
<< "seller_id=\""
<< SELLER_ID
<< "\"&"
<< "total_fee=\""
<< payMoney
<< "\"&"
<< "body=\""
<< BODY
<< "\"";
return ss.str();
}
static string verify_response(const string ¬ifyId) {
stringstream ss;
ss << HTTPS_VERIFY_URL
<< "&partner="
<< PARTNER
<< "¬ify_id="
<< notifyId;
// do it yourself using curl
return check_alipay_cb_url(ss.str().c_str());
}
static bool get_sign_verify(CgiUtil &cgi, string sign) {
std::map<string, string> map;
para_filter(cgi, map);
string preSignStr = create_link_string(map);
bool isSign = false;
// RSA verify
if (Rsa::verify(PUBLIC_KEY, preSignStr, sign)) {
isSign = true;
}
return isSign;
}
static void para_filter(CgiUtil &cgi,
std::map<string, string> &hmap) {
vector<FormEntry> entry = cgi.getElements();
vector<FormEntry>::iterator it = entry.begin();
for (; it != entry.end(); it++) {
FormEntry fe = *it;
string name = fe.getName();
string value = fe.getValue();
if (value == "" || iequal(name, "sign") ||
iequal(name, "sign_type")) {
continue;
}
hmap.insert(pair<string, string>(name, value));
}
}
static string create_link_string(std::map<string, string> &map) {
std::map<string, string>::iterator it = map.begin();
unsigned int size = map.size();
stringstream ss;
for (unsigned int i = 0; it != map.end() &&
i < size; it ++, i ++) {
if (i == size - 1) {
ss << it->first << "=" << it->second;
} else {
ss << it->first << "=" << it->second << "&";
}
}
return ss.str();
}
static string url_encode(const string &url) {
string encode = "";
char *data_encode = curl_easy_escape(NULL, url.c_str(), 0);
if (data_encode) {
encode = data_encode;
curl_free(data_encode);
}
return encode;
}
};
#endif /* __ALIPAY_H__ */
需要注意的是:代码中使用的<span style="background-color: rgb(240, 240, 240);">CgiUtil类这里没有实现,可以参考支付宝给的C#或者其它语言版本的demo模仿实现!!</span>
<span style="background-color: rgb(240, 240, 240);">demo在:http://doc.open.alipay.com/doc2/detail?treeId=54&articleId=103419&docType=1</span>
<span style="background-color: rgb(240, 240, 240);">
</span>