Files
junhong_cmp_fiber/pkg/fuiou/client.go

387 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package fuiou
import (
"crypto"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"encoding/xml"
"fmt"
"io"
"net"
"net/http"
"net/url"
"reflect"
"sort"
"strings"
"sync"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
"go.uber.org/zap"
)
// Client 富友支付客户端
type Client struct {
InsCd string // 机构号
MchntCd string // 商户号
TermId string // 终端号
ApiURL string // 富友 API 地址
NotifyURL string // 支付回调地址
privateKey *rsa.PrivateKey // 商户私钥(用于签名)
publicKey *rsa.PublicKey // 富友公钥(用于验签)
logger *zap.Logger
}
// NewClient 从配置参数构造富友客户端
// privateKeyBase64: 商户 RSA 私钥 Base64 编码(支持 PEM/DER、PKCS1/PKCS8 格式)
// publicKeyBase64: 富友 RSA 公钥 Base64 编码(支持 PEM/DER 格式)
func NewClient(insCd, mchntCd, termId, apiURL, notifyURL, privateKeyBase64, publicKeyBase64 string, logger *zap.Logger) (*Client, error) {
privKey, err := parsePrivateKey(privateKeyBase64)
if err != nil {
return nil, fmt.Errorf("解析商户私钥失败: %w", err)
}
pubKey, err := parsePublicKey(publicKeyBase64)
if err != nil {
return nil, fmt.Errorf("解析富友公钥失败: %w", err)
}
if rsaPublicKeysEqual(pubKey, &privKey.PublicKey) {
return nil, fmt.Errorf("富友公钥配置错误:当前 fy_public_key 与商户私钥匹配,请配置富友平台提供的验签公钥")
}
return &Client{
InsCd: insCd,
MchntCd: mchntCd,
TermId: termId,
ApiURL: apiURL,
NotifyURL: notifyURL,
privateKey: privKey,
publicKey: pubKey,
logger: logger,
}, nil
}
// Sign 对请求数据签名
// 算法: 按字典序排列非空字段 → key=value&key=value → GBK 编码 → MD5 → RSA PKCS1v15 签名 → Base64
func (c *Client) Sign(data interface{}) (string, error) {
signStr := buildSignString(data)
c.logger.Debug("富友签名原文", zap.String("sign_str", signStr))
// UTF-8 → GBK
gbkBytes, err := utf8ToGBK([]byte(signStr))
if err != nil {
return "", fmt.Errorf("GBK 编码失败: %w", err)
}
// MD5
hash := md5.Sum(gbkBytes)
// RSA PKCS1v15 签名
signature, err := rsa.SignPKCS1v15(rand.Reader, c.privateKey, crypto.MD5, hash[:])
if err != nil {
return "", fmt.Errorf("RSA 签名失败: %w", err)
}
return base64.StdEncoding.EncodeToString(signature), nil
}
// Verify 验证签名
func (c *Client) Verify(data interface{}, sign string) error {
signStr := buildSignString(data)
return c.verifySignString(signStr, sign)
}
// verifySignString 使用指定签名原文验证 RSA 签名。
func (c *Client) verifySignString(signStr, sign string) error {
c.logger.Debug("富友验签原文", zap.String("sign_str", signStr))
c.logger.Debug("富友签名", zap.String("sign", sign))
gbkBytes, err := utf8ToGBK([]byte(signStr))
if err != nil {
return fmt.Errorf("GBK 编码失败: %w", err)
}
hash := md5.Sum(gbkBytes)
sigBytes, err := base64.StdEncoding.DecodeString(sign)
if err != nil {
return fmt.Errorf("Base64 解码签名失败: %w", err)
}
if err := rsa.VerifyPKCS1v15(c.publicKey, crypto.MD5, hash[:], sigBytes); err != nil {
c.logger.Error("富友验签失败诊断",
zap.Error(err),
zap.Int("signature_len", len(sigBytes)),
zap.Int("public_key_bits", c.publicKey.N.BitLen()),
zap.String("public_key_sha256", rsaPublicKeyFingerprint(c.publicKey)),
zap.Bool("public_key_matches_private_key", rsaPublicKeysEqual(c.publicKey, &c.privateKey.PublicKey)),
)
return err
}
return nil
}
// DoRequest 发送 HTTP 请求到富友
// 处理流程: XML 编码 → GBK 转换 → 双重 URL 编码 → POST → URL 解码 → GBK→UTF-8 → XML 解析
func (c *Client) DoRequest(path string, req interface{}, resp interface{}) error {
// 1. XML 编码
xmlBytes, err := xml.Marshal(req)
if err != nil {
return fmt.Errorf("XML 编码失败: %w", err)
}
// 2. 添加 XML 声明并替换编码声明为 GBK
xmlStr := `<?xml version="1.0" encoding="GBK"?>` + string(xmlBytes)
// 3. UTF-8 → GBK
gbkBytes, err := utf8ToGBK([]byte(xmlStr))
if err != nil {
return fmt.Errorf("GBK 编码失败: %w", err)
}
// 4. 两次 URL 编码
encoded := url.QueryEscape(url.QueryEscape(string(gbkBytes)))
// 5. POST 请求
reqURL := strings.TrimRight(c.ApiURL, "/") + path
body := "req=" + encoded
c.logger.Info("富友请求完整报文",
zap.String("url", reqURL),
zap.String("path", path),
zap.String("request_xml", xmlStr),
zap.String("request_body", body),
)
httpResp, err := http.Post(reqURL, "application/x-www-form-urlencoded", strings.NewReader(body))
if err != nil {
return fmt.Errorf("HTTP 请求失败: %w", err)
}
defer httpResp.Body.Close()
// 6. 读取响应
respBody, err := io.ReadAll(httpResp.Body)
if err != nil {
return fmt.Errorf("读取响应失败: %w", err)
}
// 7. URL 解码
decoded, err := url.QueryUnescape(string(respBody))
if err != nil {
return fmt.Errorf("URL 解码响应失败: %w", err)
}
// 8. GBK → UTF-8
utf8Bytes, err := GBKToUTF8([]byte(decoded))
if err != nil {
return fmt.Errorf("UTF-8 转换失败: %w", err)
}
// 9. 替换 XML 声明中的编码为 UTF-8
utf8Str := strings.Replace(string(utf8Bytes), `encoding="GBK"`, `encoding="UTF-8"`, 1)
// 10. XML 解析
if err := xml.Unmarshal([]byte(utf8Str), resp); err != nil {
return fmt.Errorf("XML 解析响应失败: %w", err)
}
return nil
}
// buildSignString 构建签名原文
// 提取字段(排除 sign 和 reserved 开头字段),按字典序拼接为 key=value&key=value。
func buildSignString(data interface{}) string {
return buildSignStringFromMap(structToMap(data))
}
// buildSignStringFromMap 按富友规则将字段集合拼接为签名原文。
func buildSignStringFromMap(fields map[string]string) string {
// 按 key 字典序排列
keys := make([]string, 0, len(fields))
for k := range fields {
keys = append(keys, k)
}
sort.Strings(keys)
// 拼接
pairs := make([]string, 0, len(keys))
for _, k := range keys {
pairs = append(pairs, k+"="+fields[k])
}
return strings.Join(pairs, "&")
}
// structToMap 通过反射提取结构体的 xml tag 和值。
// 排除 sign 字段和 reserved 开头字段,保留空值字段参与签名。
func structToMap(data interface{}) map[string]string {
result := make(map[string]string)
v := reflect.ValueOf(data)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// 排除 XMLName 字段xml.Name 类型,仅用于指定根元素名称)
if field.Name == "XMLName" {
continue
}
value := v.Field(i).String()
tag := field.Tag.Get("xml")
if tag == "" || tag == "-" {
continue
}
// 排除 sign 字段
if tag == "sign" {
continue
}
// 排除 reserved 开头字段
if strings.HasPrefix(tag, "reserved") {
continue
}
result[tag] = value
}
return result
}
// utf8ToGBK 将 UTF-8 字节转换为 GBK 编码
func utf8ToGBK(input []byte) ([]byte, error) {
reader := transform.NewReader(strings.NewReader(string(input)), simplifiedchinese.GBK.NewEncoder())
return io.ReadAll(reader)
}
// GBKToUTF8 将 GBK 字节转换为 UTF-8 编码
func GBKToUTF8(input []byte) ([]byte, error) {
reader := transform.NewReader(strings.NewReader(string(input)), simplifiedchinese.GBK.NewDecoder())
return io.ReadAll(reader)
}
// generateRandomStr 生成 32 字符随机字符串
func generateRandomStr() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
return fmt.Sprintf("%x", b)
}
var (
serverIP string
serverIPOnce sync.Once
)
// GetServerIP 获取服务器非回环 IPv4 地址(进程生命周期内缓存)
func GetServerIP() string {
serverIPOnce.Do(func() {
addrs, _ := net.InterfaceAddrs()
for _, addr := range addrs {
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() && ipNet.IP.To4() != nil {
serverIP = ipNet.IP.String()
return
}
}
serverIP = "127.0.0.1"
})
return serverIP
}
// parsePrivateKey 解析 Base64 编码的 RSA 私钥
// 支持多种格式: PEM(PKCS1/PKCS8) 和 DER(PKCS1/PKCS8)
func parsePrivateKey(base64Str string) (*rsa.PrivateKey, error) {
decoded, err := base64.StdEncoding.DecodeString(base64Str)
if err != nil {
return nil, fmt.Errorf("Base64 解码失败: %w", err)
}
// 尝试 PEM 格式Base64 解码后是 PEM 文本)
if block, _ := pem.Decode(decoded); block != nil {
return parseDERPrivateKey(block.Bytes)
}
// 尝试 DER 格式Base64 解码后直接是 DER 字节)
return parseDERPrivateKey(decoded)
}
// parseDERPrivateKey 从 DER 字节解析 RSA 私钥,先尝试 PKCS8 再尝试 PKCS1
func parseDERPrivateKey(derBytes []byte) (*rsa.PrivateKey, error) {
// 先尝试 PKCS8
if key, err := x509.ParsePKCS8PrivateKey(derBytes); err == nil {
if rsaKey, ok := key.(*rsa.PrivateKey); ok {
return rsaKey, nil
}
return nil, fmt.Errorf("PKCS8 解析结果不是 RSA 私钥")
}
// 再尝试 PKCS1
if key, err := x509.ParsePKCS1PrivateKey(derBytes); err == nil {
return key, nil
}
return nil, fmt.Errorf("无法解析私钥(已尝试 PKCS8 和 PKCS1 格式)")
}
// parsePublicKey 解析 Base64 编码的 RSA 公钥
// 支持 PEM 和 DER 格式
func parsePublicKey(base64Str string) (*rsa.PublicKey, error) {
decoded, err := base64.StdEncoding.DecodeString(base64Str)
if err != nil {
return nil, fmt.Errorf("Base64 解码失败: %w", err)
}
// 尝试 PEM 格式
if block, _ := pem.Decode(decoded); block != nil {
return parseDERPublicKey(block.Bytes)
}
// 尝试 DER 格式
return parseDERPublicKey(decoded)
}
// parseDERPublicKey 从 DER 字节解析 RSA 公钥
func parseDERPublicKey(derBytes []byte) (*rsa.PublicKey, error) {
pub, err := x509.ParsePKIXPublicKey(derBytes)
if err != nil {
return nil, fmt.Errorf("PKIX 公钥解析失败: %w", err)
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("解析结果不是 RSA 公钥")
}
return rsaPub, nil
}
// rsaPublicKeyFingerprint 返回 RSA 公钥的 SHA256 指纹,避免日志输出完整密钥。
func rsaPublicKeyFingerprint(pub *rsa.PublicKey) string {
derBytes, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
return ""
}
sum := sha256.Sum256(derBytes)
return hex.EncodeToString(sum[:])
}
func rsaPublicKeysEqual(a, b *rsa.PublicKey) bool {
if a == nil || b == nil {
return false
}
return a.E == b.E && a.N.Cmp(b.N) == 0
}