签名计算使用指引

签名机制

为保证 HTTP/HTTPS 服务的安全使用,在服务配置中可选择开启签名计算,在调用 API 时全局服务会对开启签名的服务进行签名计算,并将签名放到请求header中。本文介绍计算签名的方法和示例。

步骤一:拼接不同请求类型的参数

1、CanonicalizedHeaderString:

(1)header参数范围:包括header中以“x-dmpaas”开头的系统参数、header自定义参数,不包括header参数中的x-dmpaas-signature参数。

(2)header参数排序和拼接:

按照参数字符串字典升序对header参数排序,多个header之间用&连接。

使用等号(=)连接编码后的header参数和编码后的header参数值,编码方式参考附录。

2、CanonicalizedQueryString:

(1)query参数范围:全局服务页面上所有query参数、URL连接串提前预置的query参数

(2)query参数排序和拼接:

使用等号(=)连接编码后的query参数和编码后的query参数值,编码方式参考附录。

按照参数字符串字典升顺对query参数排序,多个query之间用&连接。

3、CanonicalizedBodyString:请求的body 字符串,如果没有body,就用空字符串("");

步骤二:构造签名字符串

我们以 Java 为例,该字符串构造规则如下:

String stringToSign =

HTTPMethod + "&" +

encodeURIComponent("/") + "&" +

encodeURIComponent(CanonicalizedHeaderString) + "&" +

encodeURIComponent(CanonicalizedQueryString) + "&" +

encodeURIComponent(CanonicalizedBodyString)

步骤三:计算签名

在全局服务配置中可查到AccessKey匹配的AccessToken,作为加密的密钥,使用 HMAC-SHA1 的签名算法,计算待签名字符串StringToSign的签名。

参考代码

通过`ChatbotSignUtil.checkSign()`进行验签。

  • 对于请求头,在验签过程中,仅关注以“x-dmpaas”开头的系统参数和“全局服务/API插件-编辑-header参数”中配置的header,其他header需要在验签前移除。

  • 特别注意:在进行GET请求时,body一定为null。

import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.TreeMap;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

public class ChatbotSignUtil {
    /**
     * 签名密钥对,可以从“全局服务/API插件-编辑-启用签名”中获取
     */
    public static final String ACCESS_KEY = "yourAccessKey";
    public static final String ACCESS_TOKEN = "yourAccessToken";
    /**
     * chatbot所需header
     */
    public static final String HTTP_CHATUUID_HEADER = "x-dmpaas-beebot-chat-id";
    public static final String HTTP_TIMESTAMP_HEADER = "x-dmpaas-timestamp";
    public static final String HTTP_ACCESSKEY_HEADER = "x-dmpaas-accesskey";
    public static final String HTTP_SIGNATURE_NONCE_HEADER = "x-dmpaas-signature-nonce";
    public static final String HTTP_SIGNATURE_HEADER = "x-dmpaas-signature";

    public static final String ENCODING = "UTF-8";
    public static final String SIGN_ALGORITHM = "HmacSHA1";

    /**
     * 校验签名。
     * @param method        请求方法,GET/POST。
     * @param requestParams 请求参数,无论是GET还是POST,requestParams中均有参数。
     * @param headers       请求头,其中包含chatbot所需要的header和业务所需的header,其他header需要过滤掉。
     * @param body          请求体,如果是GET请求,则body为null,如果是POST请求,则body为json格式的请求体。
     * @throws Exception 签名校验失败抛出异常。
     */
    public static void checkSign(String method, Map<String, String> requestParams,
        Map<String, String> headers, String body) throws Exception {
        // 根据入参生成签名
        String signString = getSignatureString(method, headers, requestParams, body);
        // 校验生成的签名和传入的签名是否一致
        String signature = headers.getOrDefault(HTTP_SIGNATURE_HEADER, null);
        if (!signString.equals(signature)) {
            throw new RuntimeException("签名不一致");
        }
        System.out.println("签名校验通过");
    }

    public static String getSignatureString(String method, Map<String, String> headerParams,
        Map<String, String> queryParams, String body) throws Exception {
        //1.注意:header仅包含上方名为"x-dmpaas-***"的header和“api插件-编辑-header参数”中配置的header
        TreeMap<String, Object> headerTreeMap = new TreeMap<>(headerParams);
        //2.HTTP_SIGNATURE_HEADER要排除掉
        headerTreeMap.remove(HTTP_SIGNATURE_HEADER);
        //3.构造header和query的规范化签名字符串
        TreeMap<String, Object> queryTreeMap = new TreeMap<>(queryParams);
        StringBuilder headerStr = new StringBuilder();
        StringBuilder queryStr = new StringBuilder();
        for (String key : headerTreeMap.keySet()) {
            headerStr.append("&").append(specialCharUrlEncode(key)).append("=").append(
                specialCharUrlEncode(headerTreeMap.get(key).toString()));
        }
        for (String key : queryTreeMap.keySet()) {
            queryStr.append("&").append(specialCharUrlEncode(key)).append("=").append(
                specialCharUrlEncode(queryTreeMap.get(key).toString()));
        }
        //拼接完要将第一个&去掉,拼接上body
        String canonicalizedHeaderString;

        if (headerTreeMap.isEmpty()) {
            canonicalizedHeaderString = "";
        } else {
            canonicalizedHeaderString = headerStr.substring(1);
        }
        System.out.println("canonicalizedHeaderString: " + canonicalizedHeaderString);
        String canonicalizedQueryString;
        if (queryTreeMap.isEmpty()) {
            canonicalizedQueryString = "";
        } else {
            canonicalizedQueryString = queryStr.substring(1);
        }
        System.out.println("canonicalizedQueryString: " + canonicalizedQueryString);
        //4.拼接出构造密钥所需的加密前字符串
        //specialCharUrlEncode,对特殊字符进行替换,例如空格编码成%20
        String stringToSign = method + "&" + specialCharUrlEncode("/") + "&" +
            specialCharUrlEncode(canonicalizedHeaderString) + "&" +
            specialCharUrlEncode(canonicalizedQueryString) + "&" +
            specialCharUrlEncode(body);
        System.out.println("stringToSign: " + stringToSign);
        //5.使用accessToken构造密钥secret
        String accessKey = headerParams.get(HTTP_ACCESSKEY_HEADER);
        if (!ACCESS_KEY.equals(accessKey)) {
            throw new RuntimeException("ERROR accessKey");
        }
        String secret = ACCESS_TOKEN + "&";
        System.out.println("secret: " + secret);
        return doSignature(stringToSign, secret, SIGN_ALGORITHM);
    }

    private static String specialCharUrlEncode(String value) throws UnsupportedEncodingException {
        if (value == null || value.isEmpty()) {
            return "";
        }
        //调用中,需要对请求参数和请求值使用 UTF-8 字符集按照RFC3986规则进行编码。
        return java.net.URLEncoder.encode(value, ENCODING).replace("+", "%20")
            .replace("*", "%2A").replace("%7E", "~");
    }

    private static String doSignature(String stringToSign, String secret, String signAlgorithm) throws Exception {
        SecretKeySpec signinKey = new SecretKeySpec(secret.getBytes(ENCODING), signAlgorithm);
        Mac mac = Mac.getInstance(signAlgorithm);
        mac.init(signinKey);
        return DatatypeConverter.printBase64Binary(mac.doFinal(stringToSign.getBytes(ENCODING)));
    }
}