签名和解密
签名(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×tamp=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),
}
}