golang 对接第三方接口 RSA 做签(加密) 验签(解密)

一、过程

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参数排除在外

Go 复制代码
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转换

Go 复制代码
// 将排序好的参数进行加密签名并转成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了

Go 复制代码
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
}
  1. 对方回调我们方接口接收参数
Go 复制代码
//获取参数字节码
    body, err := ioutil.ReadAll(c.Request.Body)
	info := ""
	if err != nil {
		//返回错误
        return
	}
	//参数转成字符串
	dataString, err := url.QueryUnescape(string(body[:]))
	if err != nil {
		//返回错误
		return
	}

5.解密

Go 复制代码
// 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则需要根据需要来灵活转换。

相关推荐
恣艺2 分钟前
Python 游戏开发与文件处理:PyGame + Turtle + openpyxl + python-docx + PyPDF2
开发语言·python·pygame
Postkarte不想说话3 分钟前
Jupyter Lab安装
后端
fliter6 分钟前
在 Async Rust 中实现请求合并(Request Coalescing)
后端
王立志_LEO6 分钟前
Gunicorn 启动django服务
后端
fliter7 分钟前
一个让我调试一周的 Rust match 陷阱
后端
高林雨露10 分钟前
kotlin 相关code
开发语言·kotlin
我还记得那天13 分钟前
函数的递归调用
c语言·开发语言·visualstudio
zhangfeng113314 分钟前
ThinkPHP5 事件系统的标准最佳实践 事件系统的完整设计逻辑tags.php tags.php(事件地图)
android·开发语言·php
xyq202417 分钟前
HTML 标签简写及全称
开发语言
tongluowan00718 分钟前
数据结构 Bitmap(位图)示例 - 用户签到系统
开发语言·数据结构·bitmap·用户签到系统