package fuiou import ( "crypto" "crypto/md5" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/pem" "encoding/xml" "fmt" "io" "net/http" "net/url" "reflect" "sort" "strings" "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) } 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) // 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) 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) } return rsa.VerifyPKCS1v15(c.publicKey, crypto.MD5, hash[:], sigBytes) } // 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 := `` + 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.Debug("富友请求发送", zap.String("url", reqURL), zap.String("path", path), ) 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 { fields := structToMap(data) // 按 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) 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 } // 排除空值 if value == "" { 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) } // 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 }