调用支付宝支付接口时,需要用商户自己的私钥生成sign,将数据与sign一起发送给支付宝来发起支付。
这里总结一下签名的流程,以支付宝手机网站支付为例。实现语言为golang。
一. 生成biz_content业务参数信息:
func GenBizContent(subject, outTradeNo, buyerId, payType string, totalAmount int64) (string, error) {
m := make(map[string]interface{})
m["subject"] = subject
m["out_trade_no"] = outTradeNo
PayMoney, err := Int64DividedBy100(totalAmount)
if err != nil {
err = errors.New("change amount int64 to float64 fail," + err.Error())
return "", err
}
m["total_amount"] = PayMoney //TODO
switch payType {
case constants.PayTypeAlipayWap:
m["product_code"] = AlipayWapProductCode
case constants.PayTypeAlipayApp:
m["product_code"] = AlipayAppProductCode
case constants.PayTypeAlipayMini:
m["buyer_id"] = buyerId
}
jsonStr, err := json.Marshal(m)
if err != nil {
err = errors.New("generate biz_content fail," + err.Error())
return "", err
}
return string(jsonStr), nil
}
func Int64DividedBy100(amount int64) (float64, error) {
str := strconv.FormatInt(amount, 10)
switch len(str) {
case 1:
str = "0.0" + str
case 2:
str = "0." + str
default:
str = str[:len(str)-2] + "." + str[len(str)-2:]
}
ret, err := strconv.ParseFloat(str, 64)
if err != nil {
err = errors.Wrap(err)
return -0.1, err
}
return ret, nil
}
这里定义map[string]interface{}来存储biz_content中的必填内容,total_amount字段传进来时单位为‘分’,需要转换成‘元’,为了避免浮点数运算产生误差,这里转换成字符串模拟除法,最后转换回来,也可以用特定的Money包(github.com/chanxuehong/util/money)。最后利用json.Marshel将map形式转换成string。另外尝试过这里total_amount为float或者string都可以。
二. 生成sign
1. 这里先将签名需要的数据填充到url.Values{}中,
func FillSign2Data(appid, payType, outTradeNo, bizContent, privateKey string, userId int64, method string) (url.Values, error) {
data := url.Values{}
data.Set("app_id", appid)
data.Set("method", method)
data.Set("charset", "utf-8")
data.Set("sign_type", "RSA2")
now := time.Now().Format(TimestampForm)
data.Set("timestamp", now)
data.Set("version", "1.0")
data.Set("notify_url", fmt.Sprintf(config.AlipayCallBack, userId, outTradeNo))
data.Set("biz_content", bizContent)
dlog.Debug("signed_data", data)
//生成签名
signContentBytes, _ := url.QueryUnescape(data.Encode())
dlog.Debug("data to be signed", signContentBytes)
//fmt.Println("data to be signed", signContentBytes)
signature, err := util.Sign([]byte(signContentBytes), "RSA2", privateKey)
if err != nil {
err = errors.Errorf("生成签名失败,请检查私钥是否配置成功。error:%v", err)
return nil, err
}
data.Set("sign", signature)
dlog.Debug("sign", signature)
return data, nil
}
注意这里url.Encode()函数会将每个参数进行url编码,然后参数之间用&符号连接,对应的key和value用'='连接。
然而支付宝的数据只要求参数之间用&符号连接,对应的key和value用'='连接,对每个参数是不用url编码的,所以这里将编码后的url再解码,即将url编码的参数解码回原先的字符。
这里有例子:
func testUrl() {
data := url.Values{}
data.Set("1", "1")
data.Set("2", "2")
data.Set("liyunlong", "liyunlong")
data.Set("data", "http://alipared:10003/paydcaldlback/alidpay/432412/Nofaffa")
fmt.Println(data.Encode())
//tmp := url.Values{"data":[]{"data is nklsjfklajf"}}
fmt.Println(url.QueryUnescape(data.Encode()))
os.Exit(0)
}
输出:
1=1&2=2&data=http%3A%2F%2Falipared%3A10003%2Fpaydcaldlback%2Falidpay%2F432412%2FNofaffa&liyunlong=liyunlong
1=1&2=2&data=http://alipared:10003/paydcaldlback/alidpay/432412/Nofaffa&liyunlong=liyunlong <nil>
2. 生成sign
func Sign(data []byte, SignType, pemPriKey string) (signature string, err error) {
var h hash.Hash
var hType crypto.Hash
switch SignType {
case SignTypeRsa:
h = sha1.New()
hType = crypto.SHA1
case SignTypeRsa2:
h = sha256.New()
hType = crypto.SHA256
}
h.Write(data)
d := h.Sum(nil)
pk, err := ParsePrivateKey(pemPriKey)
if err != nil {
err = errors.Wrap(err)
return
}
bs, err := rsa.SignPKCS1v15(rand.Reader, pk, hType, d)
if err != nil {
err = errors.Wrap(err)
return
}
signature = base64.StdEncoding.EncodeToString(bs)
return
}
func ParsePrivateKey(privateKey string) (pk *rsa.PrivateKey, err error) {
block, _ := pem.Decode([]byte(privateKey))
if block == nil {
err = errors.Errorf("私钥格式错误1:%s", privateKey)
return
}
switch block.Type {
case "RSA PRIVATE KEY":
rsaPrivateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err == nil {
pk = rsaPrivateKey
} else {
err = errors.Wrap(err)
}
default:
err = errors.Errorf("私钥格式错误:%s", privateKey)
}
return
}
这里利用rsa2方式,先将要签名的数据用sha256的形式来hash,可以利用sha256.sum256直接来hash数据,上面写的稍微麻烦点,实际是一样的。然后调用SignPKCS1v15来生成sign,传入转换后的私钥、加密方式、hash之后的数据,即可获取sign。最后将sign进行base64编码即可。
三.期间遇到的问题总结:
调试过程中经常碰到传给支付宝相关数据后,支付宝验签失败的情况(手机网站支付方式),主要原因有以下几种:
1. 私钥公钥不匹配。这是很常见的,一定要确认好,可以利用支付宝沙箱模式和支付宝签名工具检测。
2. 少传了参数。在服务端生成签名时,利用了某些参数,但是传给支付宝时,由于不是必填参数,就没填,这样也会导致验签失败。
3. biz_content中存在中文,出现乱码。wap提交的是form表单,我改成get的形式就可以了,暂时没找到原因。