签名机制
为保证 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)));
}
}