Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

签名和解密

签名(sign)

  • 将所有非空参数appSecret,按参数名升序排序。
  • key=value 方式拼接,多个用 & 连接。
  • 对拼接后的整个字符串进行 小写32位MD5

签名格式:

md5(key1=value1&key2=value2&...&keyN=valueN)

示例:

拼接前:

amount=1&appId=YTUvZeeOdx&appSecret=owfwFkDnlCuiUTYz&callbackUrl=http://127.0.0.1:8921/api/callback/test&itemId=100001&outOrderId=2975857684279803&timestamp=20200717133601001&uuid=18898810602

MD5结果:

2f64566717836f1b62276519ac71aaf3

提示:

  • appSecret 仅用于签名,不要作为参数上传
  • 排序应为全字节升序,空值参数不参与签名。
  • 常见签名失败原因:时间戳不一致、未加 appSecret、排序错误、空参数被用于签名等。

签名示例代码

Go 示例

package main

import (
    "crypto/md5"
    "encoding/hex"
    "fmt"
    "sort"
    "strings"
)

// 构建签名字符串
func BuildSignString(params map[string]string, appSecret string) string {
    keys := make([]string, 0)
    for k, v := range params {
        if v != "" {
            keys = append(keys, k)
        }
    }
    keys = append(keys, "appSecret")
    sort.Strings(keys)
    var list []string
    for _, k := range keys {
        var val string
        if k == "appSecret" {
            val = appSecret
        } else {
            val = params[k]
        }
        if val != "" {
            list = append(list, fmt.Sprintf("%s=%s", k, val))
        }
    }
    return strings.Join(list, "&")
}

func Md5Lower(s string) string {
    h := md5.New()
    h.Write([]byte(s))
    return hex.EncodeToString(h.Sum(nil))
}

func main() {
    params := map[string]string{
        "amount":      "1",
        "appId":       "YTUvZeeOdx",
        "callbackUrl": "http://127.0.0.1:8921/api/callback/test",
        "itemId":      "100001",
        "outOrderId":  "2975857684279803",
        "timestamp":   "20200717133601001",
        "uuid":        "18898810602",
    }
    appSecret := "owfwFkDnlCuiUTYz"
    signStr := BuildSignString(params, appSecret)
    fmt.Println("待签名字符串:", signStr)
    fmt.Println("MD5签名:", Md5Lower(signStr))
}

Rust 示例

// md5 = "0.7"
use std::collections::BTreeMap;
use md5::{Md5, Digest};

// 将参数按名称升序拼接 & md5
fn build_sign_string<'a>(mut params: BTreeMap<&'a str, &'a str>, app_secret: &'a str) -> String {
    params.insert("appSecret", app_secret);
    params.iter()
        .filter(|(_, v)| !v.is_empty())
        .map(|(k, v)| format!("{}={}", k, v))
        .collect::<Vec<_>>()
        .join("&")
}

fn md5_lower(s: &str) -> String {
    let mut hasher = Md5::new();
    hasher.update(s.as_bytes());
    format!("{:x}", hasher.finalize())
}

fn main() {
    let mut params = BTreeMap::new();
    params.insert("amount", "1");
    params.insert("appId", "YTUvZeeOdx");
    params.insert("callbackUrl", "http://127.0.0.1:8921/api/callback/test");
    params.insert("itemId", "100001");
    params.insert("outOrderId", "2975857684279803");
    params.insert("timestamp", "20200717133601001");
    params.insert("uuid", "18898810602");
    let app_secret = "owfwFkDnlCuiUTYz";
    let sign_str = build_sign_string(params, app_secret);
    let sign = md5_lower(&sign_str);
    println!("待签名字符串: {}", sign_str);
    println!("MD5签名: {}", sign);
}

解密(AES)

  • 算法: AES/ECB/PKCS5Padding,无盐
  • 密钥: appSecret
  • 输入: base64编码的密文

示例:

  • 密文: jvU9Vaz67Gj+Xn/qlaXKHA==
  • AES密钥: uDMGUIodDeKcja0nDbIBzpfDJFIHld56
  • 解密后: 110

Go 示例

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "encoding/base64"
    "fmt"
)

// 去除PKCS#7填充
func PKCS7Unpad(data []byte) []byte {
    padlen := int(data[len(data)-1])
    if padlen > len(data) {
        return data
    }
    return data[:len(data)-padlen]
}

func AESDecryptBase64(ciphertextBase64, key string) (string, error) {
    data, err := base64.StdEncoding.DecodeString(ciphertextBase64)
    if err != nil {
        return "", err
    }
    // 密钥长度应为 16/24/32
    block, err := aes.NewCipher([]byte(key))
    if err != nil {
        return "", err
    }
    if len(data)%aes.BlockSize != 0 {
        return "", fmt.Errorf("密文不是块大小的倍数")
    }
    decrypted := make([]byte, len(data))
    mode := newECBDecrypter(block)
    mode.CryptBlocks(decrypted, data)
    decrypted = PKCS7Unpad(decrypted)
    return string(decrypted), nil
}

// -------- 下面是 ECB mode 支持 --------------
type ecb struct{ b cipher.Block }
type ecbDecrypter ecb

func newECBDecrypter(b cipher.Block) cipher.BlockMode {
    return (*ecbDecrypter)(&ecb{b})
}
func (x *ecbDecrypter) BlockSize() int        { return x.b.BlockSize() }
func (x *ecbDecrypter) CryptBlocks(dst, src []byte) {
    bs := x.b.BlockSize()
    for len(src) > 0 {
        x.b.Decrypt(dst[:bs], src[:bs])
        src = src[bs:]
        dst = dst[bs:]
    }
}

func main() {
    encrypted := "U88v0HHTLxUp9LUkj95AJA=="
    key := "sBOvCZZurSbbdJiA" // 16字节
    plain, err := AESDecryptBase64(encrypted, key)
    if err != nil {
        fmt.Println("解密失败:", err)
    } else {
        fmt.Println("解密结果:", plain)
    }
}

Rust 示例

// aes = "0.8"
// block-modes = "0.9"
// block-padding = "0.3"
// base64 = "0.21"
use aes::Aes128;
use block_modes::{BlockMode, Ecb};
use block_modes::block_padding::Pkcs7;
use base64::{engine::general_purpose, Engine as _};

/// AES/ECB/PKCS5Padding 解密(Base64输入)
/// Java 兼容:Cipher.getInstance("AES/ECB/PKCS5Padding")
fn aes_ecb_decrypt_base64(cipher_base64: &str, key: &str) -> Result<String, Box<dyn std::error::Error>> {
    let cipher_bytes = general_purpose::STANDARD.decode(cipher_base64)?;

    let key_bytes = key.as_bytes();
    if !matches!(key_bytes.len(), 16 | 24 | 32) {
        return Err("密钥长度必须为 16 / 24 / 32 字节".into());
    }

    let cipher = Ecb::<Aes128, Pkcs7>::new_from_slices(key_bytes, &[])?;
    let decrypted = cipher.decrypt_vec(&cipher_bytes)?;

    Ok(String::from_utf8(decrypted)?)
}

fn main() {
    let encrypted = "jvU9Vaz67Gj+Xn/qlaXKHA==";
    let key = "uDMGUIodDeKcja0nDbIBzpfDJFIHld56"; // 32 字节 → AES-256

    match aes_ecb_decrypt_base64(encrypted, key) {
        Ok(plain) => println!("解密结果: {}", plain),
        Err(e) => println!("解密失败: {}", e),
    }
}