簽名機制
為保證 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)));
}
}