调用支付宝支付接口时,需要用商户自己的私钥生成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的形式就可以了,暂时没找到原因。