签名与验签
本章节主要介绍百度收银台使用的双向RSA加密签名规则与相关示例。
基本限定
1.统一字符集:UTF-8 ;
百度收银台接口中的所有参数字符集均保持为 UTF-8 ,与收银台交互接入或者计算签名时,要统一使用 UTF-8 ,暂不支持其他字符集,请接入业务方自行转换;
2.请求接口的参数列表均为字符串类型键值对;
3.键值中如果为复杂数据类型,比如结构体、数组、对象都必须先转化为 JSON 结构字符串;
4.参数中包含汉字的部分,需要做 URLEncode 处理。
签名规则
1.排除参数列表中名为 sign 和 sign_type 的参数;
2.将剩余参数按参数名字典序正序排列;
3.将参数与其对应的值使用 “=” 连接,组成参数字符串,将参数字符串按排序结果,使用 “&” 连接,组成待签名字符串;
4.将待签名字符串和业务方私钥使用 SHA1WithRSA 签名算法得出最终签名。
签名计算过程示例
使用密钥生成中的示例公私钥来做计算演示。
1.初始请求业务参数
参数名 | 示例取值 |
---|---|
appKey | MMMabc |
dealId | 470193086 |
tpOrderId | 3028903626 |
totalAmount | 11300 |
2.生成待签名字符串
appKey=MMMabc&dealId=470193086&totalAmount=11300&tpOrderId=3028903626
3.生成最终签名串
TN0ZNPyQeTnPjCN5hUa7JwrXOhR8uDASXPazidVQHFSiGCH5aouBkVvJxtf8PeqzGYWAASwS2oOt2eJfunzC5dTFd/pWJeJToMgCSgRY7KtQUCCDnMrtpqiMAf+dLiXps3HpWhVB4CK6MXfHc64ejP5a2fu5bg8B0BTcHrqaGc0=
4.签名的完整请求参数
参数名 | 示例取值 |
---|---|
appKey | MMMabc |
dealId | 470193086 |
tpOrderId | 3028903626 |
totalAmount | 11300 |
rsaSign | TN0ZNPyQeTnPjCN5hUa7JwrXOhR8uDASXPazidVQHFSiGCH5aouBkVvJxtf8PeqzGYWAASwS2oOt2eJfunzC5dTFd/pWJeJToMgCSgRY7KtQUCCDnMrtpqiMAf+dLiXps3HpWhVB4CK6MXfHc64ejP5a2fu5bg8B0BTcHrqaGc0= |
签名工具参考代码
- PHP签名工具类
<?php
// 通用签名工具,基于openssl扩展,提供使用私钥生成签名和使用公钥验证签名的接口
class RSASign
{
/**
* @desc 使用私钥生成签名字符串
* @param array $assocArr 入参数组
* @param string $rsaPriKeyStr 私钥原始字符串,不含PEM格式前后缀
* @return string 签名结果字符串
* @throws Exception
*/
public static function sign(array $assocArr, $rsaPriKeyStr)
{
$sign = '';
if (empty($rsaPriKeyStr) || empty($assocArr)) {
return $sign;
}
if (!function_exists('openssl_pkey_get_private') || !function_exists('openssl_sign')) {
throw new Exception("openssl扩展不存在");
}
$rsaPriKeyPem = self::convertRSAKeyStr2Pem($rsaPriKeyStr, 1);
$priKey = openssl_pkey_get_private($rsaPriKeyPem);
if (isset($assocArr['sign'])) {
unset($assocArr['sign']);
}
// 参数按字典顺序排序
ksort($assocArr);
$parts = array();
foreach ($assocArr as $k => $v) {
$parts[] = $k . '=' . $v;
}
$str = implode('&', $parts);
openssl_sign($str, $sign, $priKey);
openssl_free_key($priKey);
return base64_encode($sign);
}
/**
* @desc 使用公钥校验签名
* @param array $assocArr 入参数据,签名属性名固定为rsaSign
* @param string $rsaPubKeyStr 公钥原始字符串,不含PEM格式前后缀
* @return bool true 验签通过|false 验签不通过
* @throws Exception
*/
public static function checkSign(array $assocArr, $rsaPubKeyStr)
{
if (!isset($assocArr['rsaSign']) || empty($assocArr) || empty($rsaPubKeyStr)) {
return false;
}
if (!function_exists('openssl_pkey_get_public') || !function_exists('openssl_verify')) {
throw new Exception("openssl扩展不存在");
}
$sign = $assocArr['rsaSign'];
unset($assocArr['rsaSign']);
if (empty($assocArr)) {
return false;
}
// 参数按字典顺序排序
ksort($assocArr);
$parts = array();
foreach ($assocArr as $k => $v) {
$parts[] = $k . '=' . $v;
}
$str = implode('&', $parts);
$sign = base64_decode($sign);
$rsaPubKeyPem = self::convertRSAKeyStr2Pem($rsaPubKeyStr);
$pubKey = openssl_pkey_get_public($rsaPubKeyPem);
$result = (bool)openssl_verify($str, $sign, $pubKey);
openssl_free_key($pubKey);
return $result;
}
/**
* @desc 将密钥由字符串(不换行)转为PEM格式
* @param string $rsaKeyStr 原始密钥字符串
* @param int $keyType 0 公钥|1 私钥,默认0
* @return string PEM格式密钥
* @throws Exception
*/
public static function convertRSAKeyStr2Pem($rsaKeyStr, $keyType = 0)
{
$pemWidth = 64;
$rsaKeyPem = '';
$begin = '-----BEGIN ';
$end = '-----END ';
$key = ' KEY-----';
$type = $keyType ? 'PRIVATE' : 'PUBLIC';
$keyPrefix = $begin . $type . $key;
$keySuffix = $end . $type . $key;
$rsaKeyPem .= $keyPrefix . "\n";
$rsaKeyPem .= wordwrap($rsaKeyStr, $pemWidth, "\n", true) . "\n";
$rsaKeyPem .= $keySuffix;
if (!function_exists('openssl_pkey_get_public') || !function_exists('openssl_pkey_get_private')) {
return false;
}
if ($keyType == 0 && false == openssl_pkey_get_public($rsaKeyPem)) {
return false;
}
if ($keyType == 1 && false == openssl_pkey_get_private($rsaKeyPem)) {
return false;
}
return $rsaKeyPem;
}
}
- Java签名工具类
/*
* Copyright (C) 2020 Baidu, Inc. All Rights Reserved.
*/
package com.baidu.*;
import static org.springframework.util.Assert.isTrue;
import static org.springframework.util.Assert.notNull;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
// 百度收银台双向RSA签名工具,JDK版本要求:1.8+
public class RSASign {
private static final String CHARSET = "UTF-8";
private static final String SIGN_TYPE_RSA = "RSA";
private static final String SIGN_ALGORITHMS = "SHA1WithRSA";
private static final String SIGN_KEY = "rsaSign";
/**
* 使用私钥生成签名字符串
*
* @param params 待签名参数集合
* @param privateKey 私钥原始字符串
*
* @return 签名结果字符串
*
* @throws Exception
*/
public static String sign(Map<String, Object> params, String privateKey) throws Exception {
isTrue(!CollectionUtils.isEmpty(params), "params is required");
notNull(privateKey, "privateKey is required");
String signContent = signContent(params);
Signature signature = Signature.getInstance(SIGN_ALGORITHMS);
signature.initSign(getPrivateKeyPKCS8(privateKey));
signature.update(signContent.getBytes(CHARSET));
byte[] signed = signature.sign();
return new String(Base64.getEncoder().encode(signed));
}
/**
* 使用公钥校验签名
*
* @param params 入参数据,签名属性名固定为rsaSign
* @param publicKey 公钥原始字符串
*
* @return true 验签通过 | false 验签不通过
*
* @throws Exception
*/
public static boolean checkSign(Map<String, Object> params, String publicKey) throws Exception {
isTrue(!CollectionUtils.isEmpty(params), "params is required");
notNull(publicKey, "publicKey is required");
// sign & content
String content = signContent(params);
String rsaSign = params.get(SIGN_KEY).toString();
// verify
Signature signature = Signature.getInstance(SIGN_ALGORITHMS);
signature.initVerify(getPublicKeyX509(publicKey));
signature.update(content.getBytes(CHARSET));
return signature.verify(Base64.getDecoder().decode(rsaSign.getBytes(CHARSET)));
}
/**
* 对输入参数进行key过滤排序和字符串拼接
*
* @param params 待签名参数集合
*
* @return 待签名内容
*
* @throws UnsupportedEncodingException
*/
private static String signContent(Map<String, Object> params) throws UnsupportedEncodingException {
Map<String, String> sortedParams = new TreeMap<>(Comparator.naturalOrder());
for (Map.Entry<String, Object> entry : params.entrySet()) {
String key = entry.getKey();
if (legalKey(key)) {
String value =
entry.getValue() == null ? null : URLEncoder.encode(entry.getValue().toString(), CHARSET);
sortedParams.put(key, value);
}
}
StringBuilder builder = new StringBuilder();
if (!CollectionUtils.isEmpty(sortedParams)) {
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
builder.append(entry.getKey());
builder.append("=");
builder.append(entry.getValue());
builder.append("&");
}
builder.deleteCharAt(builder.length() - 1);
}
return builder.toString();
}
/**
* 将公钥字符串进行Base64 decode之后,生成X509标准公钥
*
* @param publicKey 公钥原始字符串
*
* @return X509标准公钥
*
* @throws InvalidKeySpecException
* @throws NoSuchAlgorithmException
*/
private static PublicKey getPublicKeyX509(String publicKey) throws InvalidKeySpecException,
NoSuchAlgorithmException, UnsupportedEncodingException {
if (StringUtils.isEmpty(publicKey)) {
return null;
}
KeyFactory keyFactory = KeyFactory.getInstance(SIGN_TYPE_RSA);
byte[] decodedKey = Base64.getDecoder().decode(publicKey.getBytes(CHARSET));
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
}
/**
* 将私钥字符串进行Base64 decode之后,生成PKCS #8标准的私钥
*
* @param privateKey 私钥原始字符串
*
* @return PKCS #8标准的私钥
*
* @throws Exception
*/
private static PrivateKey getPrivateKeyPKCS8(String privateKey) throws Exception {
if (StringUtils.isEmpty(privateKey)) {
return null;
}
KeyFactory keyFactory = KeyFactory.getInstance(SIGN_TYPE_RSA);
byte[] decodedKey = Base64.getDecoder().decode(privateKey.getBytes(CHARSET));
return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
}
/**
* 有效的待签名参数key值
* 非空、且非签名字段
*
* @param key 待签名参数key值
*
* @return true | false
*/
private static boolean legalKey(String key) {
return StringUtils.hasText(key) && !SIGN_KEY.equalsIgnoreCase(key);
}
}
验证
加签逻辑验证
开发者实现加签逻辑之后,使用计算示例中步骤 1 的初始请求参数作为输入,结合密钥生成中的示例私钥,进行 RSA 签名的生成,如果结果与步骤 3 中最终签名串一致,说明加签逻辑正确。
验签逻辑验证
使用计算示例中步骤 4 完整请求参数作为输入,结合密钥生成中的示例公钥,进行 RSA 签名的 check ,返回 true 则说明验签逻辑正确。