Appearance
RSA 签名机制
概述
米付科技聚合支付平台采用 RSA 非对称加密 签名机制,确保接口请求的安全性和完整性。所有接口请求都需要进行签名验证。
签名算法
- 签名算法: RSA-SHA256
- 编码方式: Base64
- 密钥长度: 2048 位
签名流程
1. 参数排序
将所有请求参数(除 sign 字段外)按照参数名 ASCII 码从小到大排序:
javascript
// 原始参数
{
"pay_mch_id": "1234567890",
"out_trade_no": "20240101120000",
"total_amount": 100,
"timestamp": "1704067200",
"nonce_str": "abc123def456"
}
// 排序后
[
"pay_mch_id=1234567890",
"nonce_str=abc123def456",
"out_trade_no=20240101120000",
"timestamp=1704067200",
"total_amount=100"
]2. 拼接字符串
将排序后的参数用 & 拼接:
pay_mch_id=1234567890&nonce_str=abc123def456&out_trade_no=20240101120000×tamp=1704067200&total_amount=1003. 生成签名
使用商户私钥对拼接后的字符串进行 RSA-SHA256 签名,然后进行 Base64 编码:
javascript
sign = Base64(RSA-SHA256(signString, privateKey))4. 添加签名到请求
将签名值添加到请求参数中:
json
{
"pay_mch_id": "1234567890",
"out_trade_no": "20240101120000",
"total_amount": 100,
"timestamp": "1704067200",
"nonce_str": "abc123def456",
"sign_type": "RSA",
"sign": "签名值"
}代码示例
Java 示例
java
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;
public class SignUtil {
/**
* 生成 RSA 签名
*/
public static String sign(Map<String, Object> params, String privateKey) throws Exception {
// 1. 参数排序(使用 TreeMap 自动排序)
TreeMap<String, Object> sortedParams = new TreeMap<>(params);
// 2. 拼接字符串
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Object> entry : sortedParams.entrySet()) {
if (entry.getValue() != null && !entry.getKey().equals("sign")) {
sb.append(entry.getKey())
.append("=")
.append(entry.getValue())
.append("&");
}
}
// 移除最后一个 &
String signString = sb.substring(0, sb.length() - 1);
// 3. 生成签名
byte[] keyBytes = Base64.getDecoder().decode(privateKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey key = keyFactory.generatePrivate(keySpec);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(key);
signature.update(signString.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(signature.sign());
}
/**
* 验证签名
*/
public static boolean verify(Map<String, Object> params, String publicKey, String sign) throws Exception {
// 1. 参数排序
TreeMap<String, Object> sortedParams = new TreeMap<>(params);
sortedParams.remove("sign");
// 2. 拼接字符串
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Object> entry : sortedParams.entrySet()) {
if (entry.getValue() != null) {
sb.append(entry.getKey())
.append("=")
.append(entry.getValue())
.append("&");
}
}
String signString = sb.substring(0, sb.length() - 1);
// 3. 验证签名
byte[] keyBytes = Base64.getDecoder().decode(publicKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey key = keyFactory.generatePublic(new java.security.spec.X509EncodedKeySpec(keyBytes));
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(key);
signature.update(signString.getBytes("UTF-8"));
return signature.verify(Base64.getDecoder().decode(sign));
}
}Node.js 示例
javascript
const crypto = require('crypto');
/**
* 生成 RSA 签名
*/
function sign(params, privateKey) {
// 1. 参数排序
const sortedKeys = Object.keys(params)
.filter(key => key !== 'sign' && params[key] !== null && params[key] !== undefined)
.sort();
// 2. 拼接字符串
const signString = sortedKeys
.map(key => `${key}=${params[key]}`)
.join('&');
// 3. 生成签名
const sign = crypto
.createSign('RSA-SHA256')
.update(signString)
.sign(privateKey, 'base64');
return sign;
}
/**
* 验证签名
*/
function verify(params, publicKey, sign) {
// 1. 参数排序
const sortedKeys = Object.keys(params)
.filter(key => key !== 'sign' && params[key] !== null && params[key] !== undefined)
.sort();
// 2. 拼接字符串
const signString = sortedKeys
.map(key => `${key}=${params[key]}`)
.join('&');
// 3. 验证签名
return crypto
.createVerify('RSA-SHA256')
.update(signString)
.verify(publicKey, sign, 'base64');
}
// 使用示例
const params = {
pay_mch_id: '1234567890',
out_trade_no: '20240101120000',
total_amount: 100,
timestamp: '1704067200',
nonce_str: 'abc123def456'
};
const privateKey = `-----BEGIN PRIVATE KEY-----
Your Private Key Here
-----END PRIVATE KEY-----`;
const signValue = sign(params, privateKey);
console.log('签名值:', signValue);PHP 示例
php
<?php
class SignUtil {
/**
* 生成 RSA 签名
*/
public static function sign($params, $privateKey) {
// 1. 参数排序
ksort($params);
// 2. 拼接字符串
$signString = '';
foreach ($params as $key => $value) {
if ($key !== 'sign' && $value !== null && $value !== '') {
$signString .= $key . '=' . $value . '&';
}
}
$signString = rtrim($signString, '&');
// 3. 生成签名
$privateKey = openssl_pkey_get_private($privateKey);
openssl_sign($signString, $sign, $privateKey, OPENSSL_ALGO_SHA256);
return base64_encode($sign);
}
/**
* 验证签名
*/
public static function verify($params, $publicKey, $sign) {
// 1. 参数排序
ksort($params);
unset($params['sign']);
// 2. 拼接字符串
$signString = '';
foreach ($params as $key => $value) {
if ($value !== null && $value !== '') {
$signString .= $key . '=' . $value . '&';
}
}
$signString = rtrim($signString, '&');
// 3. 验证签名
$publicKey = openssl_pkey_get_public($publicKey);
$result = openssl_verify($signString, base64_decode($sign), $publicKey, OPENSSL_ALGO_SHA256);
return $result === 1;
}
}
// 使用示例
$params = [
'pay_mch_id' => '1234567890',
'out_trade_no' => '20240101120000',
'total_amount' => 100,
'timestamp' => '1704067200',
'nonce_str' => 'abc123def456'
];
$privateKey = file_get_contents('private_key.pem');
$signValue = SignUtil::sign($params, $privateKey);
echo "签名值: " . $signValue . PHP_EOL;密钥管理
生成密钥对
使用 OpenSSL 生成
bash
# 生成私钥(2044位)
openssl genrsa -out private_key.pem 2048
# 生成公钥
openssl rsa -in private_key.pem -pubout -out public_key.pem密钥格式
私钥格式(PKCS#8):
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
...
-----END PRIVATE KEY-----公钥格式:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
...
-----END PUBLIC KEY-----密钥上传
- 登录米付科技商户平台
- 进入「密钥管理」页面
- 上传您的公钥文件
- 平台将返回平台公钥(用于验签)
注意事项
- 私钥保密: 商户私钥必须严格保密,不可泄露
- 公钥上传: 商户公钥需上传至平台,用于平台验证商户签名
- 平台公钥: 平台公钥用于商户验证平台返回数据的签名
- 定期更换: 建议定期更换密钥对,提高安全性
- 密钥长度: 必须使用 2048 位或以上密钥长度
验签流程
接收平台返回的响应时,需要验证平台签名:
- 提取响应数据中的
sign字段 - 将其余参数按 ASCII 排序并拼接
- 使用平台公钥验证签名
- 验证通过后才处理业务数据
常见问题
Q: 签名验证失败怎么办?
- 检查参数排序是否正确
- 检查拼接字符串格式是否正确
- 确认使用的私钥/公钥是否匹配
- 检查字符编码是否统一为 UTF-8
- 确认空值参数是否已排除
Q: 是否支持其他签名算法?
当前仅支持 RSA-SHA256 签名算法,暂不支持 MD5、HMAC 等其他算法。
Q: 时间戳有效期是多久?
请求时间戳与服务器时间相差超过 5 分钟 的请求将被拒绝。
