一、过程
1.调用第三方接口前,一般需要按规则将参数按key1=value1&key2=value2 阿斯克码排序,sign参数不参与加密
2.将排序并连接好的参数字符串通过我方的私钥证书(.pem)进行加密得到加密串,当然加密得到的是 []byte 字节流,需要将字节流转换成base64字符串
3.将加密字符串赋值给sign参数,并与其他加密参数一起通过post (application/x-www-form-urlencoded )请求第三方接口。 当然,第三方与我方对接的时,会要求我方提供公约证书给到他们,用以拿到我方请求后,对我方请求参数进行延签。
4.同理,我方请求第三方后,会返回数据给到我方以及第三方会请求我们的回调接口,那么就需要对第三方返回的参数进行验签,而验签同理需要将加密参数提取出来,并进行排序,需要明白的是,此时解签我们需要用到 第三方的公钥 进行验签,
5.其中需要注意的是:我所了解的目前golang 加密和解密一般都是用的pem证书,而大部分第三方提供的证书是 .pfx .cer ,所以我需要在win上安装openssl工具,然后将 .pfx .cer转成 .pem证书
证书转换方式如下:
1. 下载Win64OpenSSL-3_1_2.exe 并安装(下载:https://slproweb.com/products/Win32OpenSSL.html)
2. 环境变量 Path 加入 D:\OpenSSL-Win64\bin
3. amd 下命令: openssl version 验证是否安装成功
4. 通过命令解析生成 公私钥 uat.pfx
openssl pkcs12 -in xxxx.pfx -nodes -out server.pem #生成为原生格式pem 私钥 ******
openssl rsa -in server.pem -out server.key #生成为rsa格式私钥文件
openssl x509 -in server.pem -out server.crt
openssl pkcs12 -in xxxx.pfx -clcerts -nokeys -out key.cert
openssl x509 -inform der -in xxx.cer -out xxx.pem #公钥pem *******
二、加密和解密要用到的方法(供参考)
1.参数进行排序并连接成 key1=value1&key2=value2 字符串并将sign参数排除在外
func GetSignStr(maps map[string]string) string {
signData := make(map[string]string, 0)
for k, v := range maps {
if k != "sign" {
signData[k] = v
}
}
var dataParams string
var keys []string
for k := range signData {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
dataParams = dataParams + k + "=" + signData[k] + "&"
}
ff := dataParams[0 : len(dataParams)-1]
return ff
}
2.将排序好的参数字符串进行证书私钥加密并将加密串base64转换
// 将排序好的参数进行加密签名并转成base64字符串返回 signData:参数字符串 pemPath :私钥路径./aaa/bbb/private.pem
func PrivateSha1SignData(signData, pemPath string) (string, error) {
h := crypto.Hash.New(crypto.SHA1)
h.Write([]byte(signData))
hashed := h.Sum(nil)
_, private, err := LoadPrivateKey(pemPath)
if err != nil {
return "", err
}
signer, err := rsa.SignPKCS1v15(rand.Reader, private, crypto.SHA1, hashed)
if err != nil {
fmt.Println("PrivateSha1SignData Error from signing: %s\n", err)
return "", err
}
return Base64Encode(signer), nil
}
// 根据路径加载证书文件
func LoadPrivateKey(pemPath string) (string, *rsa.PrivateKey, error) {
key, err := ioutil.ReadFile(pemPath)
if err != nil {
return "", nil, err
}
block, _ := pem.Decode(key)
if block == nil {
return "", nil, errors.New("pem.Decode err")
}
p, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return "", nil, err
} else {
pk := p.(*rsa.PrivateKey)
return "", pk, nil
}
}
// base64 加密
func Base64Encode(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
// base64 解密
func Base64Decode(data string) ([]byte, error) {
return base64.StdEncoding.DecodeString(data)
}
3.将sign和加密参数转成map post方式请求第三方接口,而此处需要注意接口返回的参数,返回的参数sign值中是带有+ 和 / 符号的,按正常情况 用 url.QueryUnescape(string(body[:]))转换是安全的,但奇怪的是本人遇到过用此方法反而字符串中出现了空格,所以用string(body[:])就OK了
func PayPost(requrl string, request map[string]string, publicPemPath string) (respData string, err error) {
http := TimeoutClient()
resp, err := http.Post(requrl, "application/x-www-form-urlencoded", strings.NewReader(HttpBuildQuery(request)))
if err != nil {
return respData, errors.New("paypost err1:" + err.Error())
}
if resp.StatusCode != 200 {
return respData, fmt.Errorf("http request response StatusCode:%v", resp.StatusCode)
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return respData, errors.New("paypost err2:" + err.Error())
}
dataString := string(data[:])
//dataString, err := url.QueryUnescape(string(data[:]))
//if err != nil {
// return respData, errors.New("paypost err3:" + err.Error())
//}
return respData, err
}
4. 对方回调我们方接口接收参数
//获取参数字节码
body, err := ioutil.ReadAll(c.Request.Body)
info := ""
if err != nil {
//返回错误
return
}
//参数转成字符串
dataString, err := url.QueryUnescape(string(body[:]))
if err != nil {
//返回错误
return
}
5.解密
// signStr:参数字符k=v&k=v
// signature:加密串base64需要转换
// PublicPemPath:第三方公约路径 ./aaa/bbb/public.pem
// 解密
func YSCallbackVerify(signStr,signature, PublicPemPath string) (bool, error) {
hash := crypto.Hash.New(crypto.SHA1)
hash.Write([]byte(str))
hashed := hash.Sum(nil)
var inSign []byte
inSign, err := Base64Decode(signature)
if err != nil {
return false, errors.New("解析signature失败:" + err.Error())
}
publicPem, err := LoadPublicKey(PublicPemPath)
if err != nil {
return false, errors.New("获取公钥失败:" + err.Error())
}
err = rsa.VerifyPKCS1v15(publicPem, crypto.SHA1, hashed, inSign)
if err != nil {
return false, errors.New("PublicSha1Verify Error from signing:" + err.Error())
}
return true, nil
}
//加载公约证书
func LoadPublicKey(pemPath string) (*rsa.PublicKey, error) {
key, err := ioutil.ReadFile(pemPath)
if err != nil {
return nil, errors.New("加载公钥错误1:" + err.Error())
}
block, _ := pem.Decode(key)
if block == nil {
return nil, errors.New("加载公钥错误2:" + err.Error())
}
certBody, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.New("加载公钥错误3:" + err.Error())
}
pb := certBody.PublicKey.(*rsa.PublicKey)
return pb, nil
}
6.说明:请求第三方接口,一般参数会有两层或更多层,那么我们获取到参数而第二层原本是json字符串,那么就需要注意,我们golang定义的结构体struct各字段就需要跟它对应,以免参数书序不对造成解密失败。当然,如上方法都是用的map接收参数当中需要将struct转成map 将map转成struct则需要根据需要来灵活转换。