百度智能小程序 簽名與驗(yàn)簽

2020-09-05 15:00 更新

本章節(jié)主要介紹百度收銀臺(tái)使用的雙向RSA加密簽名規(guī)則與相關(guān)示例。

基本限定

1.統(tǒng)一字符集:UTF-8 ;
百度收銀臺(tái)接口中的所有參數(shù)字符集均保持為 UTF-8 ,與收銀臺(tái)交互接入或者計(jì)算簽名時(shí),要統(tǒng)一使用 UTF-8 ,暫不支持其他字符集,請接入業(yè)務(wù)方自行轉(zhuǎn)換;
2.請求接口的參數(shù)列表均為字符串類型鍵值對;
3.鍵值中如果為復(fù)雜數(shù)據(jù)類型,比如結(jié)構(gòu)體、數(shù)組、對象都必須先轉(zhuǎn)化為 JSON 結(jié)構(gòu)字符串;
4.參數(shù)中包含漢字的部分,需要做 URLEncode 處理。

簽名規(guī)則

1.排除參數(shù)列表中名為 sign 和 sign_type 的參數(shù);
2.將剩余參數(shù)按參數(shù)名字典序正序排列;
3.將參數(shù)與其對應(yīng)的值使用 “=” 連接,組成參數(shù)字符串,將參數(shù)字符串按排序結(jié)果,使用 “&” 連接,組成待簽名字符串;
4.將待簽名字符串和業(yè)務(wù)方私鑰使用 SHA1WithRSA 簽名算法得出最終簽名。

簽名計(jì)算過程示例

使用密鑰生成中的示例公私鑰來做計(jì)算演示。

1.初始請求業(yè)務(wù)參數(shù)

參數(shù)名 示例取值
appKey MMMabc
dealId 470193086
tpOrderId 3028903626
totalAmount 11300

2.生成待簽名字符串

appKey=MMMabc&dealId=470193086&totalAmount=11300&tpOrderId=3028903626

3.生成最終簽名串

TN0ZNPyQeTnPjCN5hUa7JwrXOhR8uDASXPazidVQHFSiGCH5aouBkVvJxtf8PeqzGYWAASwS2oOt2eJfunzC5dTFd/pWJeJToMgCSgRY7KtQUCCDnMrtpqiMAf+dLiXps3HpWhVB4CK6MXfHc64ejP5a2fu5bg8B0BTcHrqaGc0=

4.簽名的完整請求參數(shù)

參數(shù)名 示例取值
appKey MMMabc
dealId 470193086
tpOrderId 3028903626
totalAmount 11300
rsaSign TN0ZNPyQeTnPjCN5hUa7JwrXOhR8uDASXPazidVQHFSiGCH5aouBkVvJxtf8PeqzGYWAASwS2oOt2eJfunzC5dTFd/pWJeJToMgCSgRY7KtQUCCDnMrtpqiMAf+dLiXps3HpWhVB4CK6MXfHc64ejP5a2fu5bg8B0BTcHrqaGc0=

簽名工具參考代碼

  • PHP簽名工具類
<?php

/**
 * 通用簽名工具
 *
 * 基于openssl擴(kuò)展,提供使用私鑰生成簽名和使用公鑰驗(yàn)證簽名的接口
 *
 **/
class RSASign
{

    /**
     * @desc 使用私鑰生成簽名字符串
     * @param array $assocArr 入?yún)?shù)組
     * @param string $rsaPriKeyStr 私鑰原始字符串,不含PEM格式前后綴
     * @return string 簽名結(jié)果字符串
     * @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擴(kuò)展不存在");
        }

        $rsaPriKeyPem = self::convertRSAKeyStr2Pem($rsaPriKeyStr, 1);

        $priKey = openssl_pkey_get_private($rsaPriKeyPem);

        if (isset($assocArr['sign'])) {
            unset($assocArr['sign']);
        }

        ksort($assocArr); // 參數(shù)按字典順序排序

        $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 使用公鑰校驗(yàn)簽名
     * @param array $assocArr 入?yún)?shù)據(jù),簽名屬性名固定為rsaSign
     * @param string $rsaPubKeyStr 公鑰原始字符串,不含PEM格式前后綴
     * @return bool true 驗(yàn)簽通過|false 驗(yàn)簽不通過
     * @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擴(kuò)展不存在");
        }

        $sign = $assocArr['rsaSign'];
        unset($assocArr['rsaSign']);

        if (empty($assocArr)) {
            return false;
        }

        ksort($assocArr); // 參數(shù)按字典順序排序

        $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 將密鑰由字符串(不換行)轉(zhuǎn)為PEM格式
     * @param string $rsaKeyStr 原始密鑰字符串
     * @param int $keyType 0 公鑰|1 私鑰,默認(rèn)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;

/**
 * 百度收銀臺(tái)雙向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     待簽名參數(shù)集合
     * @param privateKey 私鑰原始字符串
     *
     * @return 簽名結(jié)果字符串
     *
     * @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));
    }

    /**
     * 使用公鑰校驗(yàn)簽名
     *
     * @param params    入?yún)?shù)據(jù),簽名屬性名固定為rsaSign
     * @param publicKey 公鑰原始字符串
     *
     * @return true 驗(yàn)簽通過 | false 驗(yàn)簽不通過
     *
     * @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)));
    }

    /**
     * 對輸入?yún)?shù)進(jìn)行key過濾排序和字符串拼接
     *
     * @param params 待簽名參數(shù)集合
     *
     * @return 待簽名內(nèi)容
     *
     * @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();
    }

    /**
     * 將公鑰字符串進(jìn)行Base64 decode之后,生成X509標(biāo)準(zhǔn)公鑰
     *
     * @param publicKey 公鑰原始字符串
     *
     * @return X509標(biāo)準(zhǔn)公鑰
     *
     * @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));
    }

    /**
     * 將私鑰字符串進(jìn)行Base64 decode之后,生成PKCS #8標(biāo)準(zhǔn)的私鑰
     *
     * @param privateKey 私鑰原始字符串
     *
     * @return PKCS #8標(biāo)準(zhǔn)的私鑰
     *
     * @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));
    }

    /**
     * 有效的待簽名參數(shù)key值
     * 非空、且非簽名字段
     *
     * @param key 待簽名參數(shù)key值
     *
     * @return true | false
     */
    private static boolean legalKey(String key) {
        return StringUtils.hasText(key) && !SIGN_KEY.equalsIgnoreCase(key);
    }

}

驗(yàn)證

加簽邏輯驗(yàn)證

開發(fā)者實(shí)現(xiàn)加簽邏輯之后,使用計(jì)算示例中步驟 1 的初始請求參數(shù)作為輸入,結(jié)合密鑰生成中的示例私鑰,進(jìn)行 RSA 簽名的生成,如果結(jié)果與步驟 3 中最終簽名串一致,說明加簽邏輯正確。

驗(yàn)簽邏輯驗(yàn)證

使用計(jì)算示例中步驟 4 完整請求參數(shù)作為輸入,結(jié)合密鑰生成中的示例公鑰,進(jìn)行 RSA 簽名的 check ,返回 true 則說明驗(yàn)簽邏輯正確。

相關(guān)鏈接


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)