车联网下单接口加签与验签规则

车联网下单接口加签与验签规则

一、背景说明

本章节主要介绍交易中台使用的双向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=

含签名的完整请求参数

参数名 示例取值
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)) {
            sortedParams.put(key, entry.getValue().toString());
        }
    }

    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);
}

}
由于goLang语言,中台中暂无开发人员,是已经接入的同学们提供的,在此中台非常感谢热心的同学们!
但是也提醒开发者们,这些代码都有同学们各自开发场景在里头,不一定满足所有人, 对于开发者们自己的代码,建议:仅供参考,酌情改动,完整验证!

    type RsaSign struct {
        PrivateKey []byte
        PublicKey  map[string][]byte
    }

    // Sign签名算法
    func (s RsaSign) Sign(plainText string) (string, error) {
        block, _ := pem.Decode(s.PrivateKey)
        if block == nil {
            return "", errors.New("sign failed")
        }
        privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
        if err != nil {
            return "", err
        }

    hash := crypto.Hash.New(crypto.SHA1)
    hash.Write([]byte(plainText))
    privKey := privateKey.(*rsa.PrivateKey)
    sign, err := privKey.Sign(rand.Reader, hash.Sum(nil), crypto.SHA1)

    if err != nil {
        return "", err
    }
    return base64.StdEncoding.EncodeToString(sign), nil
    }

    // CheckSign 验签:对采用sha1算法进行签名后转base64格式的数据进行验签
    func (s RsaSign) CheckSign(originalData, signData string, appkey string) error {
        sign, err := base64.StdEncoding.DecodeString(signData)
        if err != nil {
            return err
        }
        block, _ := pem.Decode(s.PublicKey[appkey])
        if block == nil {
            return errors.New("block nil")
        }
        pub, err := x509.ParsePKIXPublicKey(block.Bytes)
        if err != nil {
            return err
        }
        hash := sha1.New()
        hash.Write([]byte(originalData))
        return rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA1, hash.Sum(nil), sign)
    }点击复制复制失败复制成功
    •	go代码实例2,感谢@*杰
    //KeySort 按照key排序
    func KeySort(m map[string]interface{}) []map[string]string {
        ret := make([]map[string]string, len(m))

    kvMap := make(map[string]string)
    sortedKeys := make([]string, 0)

    for k, v := range m {
        kvMap[k] = fmt.Sprintf("%v", v)
        sortedKeys = append(sortedKeys, k)
    }
    sort.Strings(sortedKeys)

    for idx, key := range sortedKeys {
        ret[idx] = map[string]string{
            "key": key,
            "val": kvMap[key],
        }
    }

    return ret
    }

    //GetSign 计算签名
    func GetSign(params map[string]interface{}) (string, error) {
        rsaPrivateKey := []byte(`-----BEGIN RSA PRIVATE KEY-----
    asdfsfd
    -----END RSA PRIVATE KEY-----`)
        delete(params, "rsaSign")
        sortedList := KeySort(params)
        parts := make([]string, len(sortedList))
        for k, v := range sortedList {
            parts[k] = v["key"] + "=" + v["val"]
        }
        plainText := strings.Join(parts, "&")
        cipherText, err := sign(plainText, rsaPrivateKey)
        if err != nil {
            return "", err
        }

    params["rsaSign"] = cipherText

    return cipherText, nil
    }

    func sign(plainText string, rsaPrivateKey []byte) (string, error) {
        block, _ := pem.Decode(rsaPrivateKey)
        if block == nil {
            return "", errors.New("pem error")
        }
        privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
        if err != nil {
            return "", err
        }

    hash := crypto.Hash.New(crypto.SHA1)
    if _, err := hash.Write([]byte(plainText)); err != nil {
        return "", err

    }

    sign, err := privateKey.Sign(rand.Reader, hash.Sum(nil), crypto.SHA1)

    if err != nil {
        return "", err
    }

    return base64.StdEncoding.EncodeToString(sign), nil
    }

    //CheckSign 验签
    func CheckSign(params map[string]interface{}) error {
        sign := params["rsaSign"].(string)
        delete(params, "rsaSign")

        sortedList := maps.KeySort(params)
        parts := make([]string, len(sortedList))
        for k, v := range sortedList {
            parts[k] = v["key"] + "=" + v["val"]
        }
        originalData := strings.Join(parts, "&")

        return checkSign(originalData, sign, []byte(`-----BEGIN PUBLIC KEY-----
    asdf
    -----END PUBLIC KEY-----`))
    }

    // checkSign 验签:对采用sha1算法进行签名后转base64格式的数据进行验签
    func checkSign(originalData, signData string, publicKey []byte) error {
        sign, err := base64.StdEncoding.DecodeString(signData)
        if err != nil {
            return err
        }

    block, _ := pem.Decode(publicKey)
    if block == nil {
        return errors.New("pem decode error")
    }
    pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return err
    }
    pub := pubInterface.(*rsa.PublicKey)

    hash := sha1.New()
    if _, err := hash.Write([]byte(originalData)); err != nil {
        return err
    }
    return rsa.VerifyPKCS1v15(pub, crypto.SHA1, hash.Sum(nil), sign)
    }

五、验证

1、加签逻辑验证
开发者实现加签逻辑之后,使用计算示例中步骤1的初始请求参数作为输入,结合密钥生成中的示例私钥,进行RSA签名的生成,如果结果与步骤3中最终签名串一致,说明加签逻辑正确
2、验签逻辑验证
使用计算示例中步骤4完整请求参数作为输入,结合密钥生成中的示例公钥,进行RSA签名的check,返回true则说明验签逻辑正确

超级支付接口API创建订单接口(云对云)