本文介紹如何校正數位簽章資訊。
背景資訊
在計算巢ECS內發起API調用時(如調用CheckoutLicense、PushMeteringData等),計算巢會返回數位簽章資訊(即Token欄位),服務商可以使用後文的方法計算數位簽章資訊的值,並將計算得到的數位簽章資訊和計算巢返回的數位簽章資訊進行對比,判斷資料是否被篡改。
訊息處理流程
此處以調用CheckoutLicense的傳回值為例,計算數位簽章的值。
調用
CheckoutLicense,擷取傳回值。curl -H "Content-Type: application/json" -XPOST https://cn-wulanchabu.axt.aliyun.com/computeNest/license/check_out_license -d '{}'傳回值
{ "code":200, "requestId":"4ea52d12-8e28-440b-b454-938d0518****", "instanceId":"i-0jl1ej1czubkimg6****", "result":{ "RequestId":"CF54B4C9-E54C-1405-9A37-A0FE3D60****", "ServiceInstanceId":"si-85a343279cf341c2****", "LicenseMetadata":"{\"TemplateName\":\"Custom_Image_Ecs\",\"SpecificationName\":\"dataDiskSize\",\"CustomData\":\"30T\"}", "Token":"21292abff855ab5c2a03809e0e4fb048", "ExpireTime":"2022-11-10T08:03:16Z" } }取出參數部分的欄位,並將參數欄位去掉數位簽章(Token)資訊後,按首字母進行排序並用&符號進行拼接。
拼接後的字串如下:
ExpireTime=2022-11-02T02:39:43Z&LicenseMetadata={"TemplateName":"Custom_Image_Ecs","SpecificationName":"dataDiskSize","CustomData":"30T"}&RequestId=CF54B4C9-E54C-1405-9A37-A0FE3D60xxxx&ServiceInstanceId=si-85a343279cf341c2xxxx說明對於存在
LicenseMetadata(許可證中繼資料)的服務。請注意以下內容,否則在加簽處理中會出現錯誤,導致簽名不一致。模板中的參數不要出現中文(例如套餐名、模板名稱)。
使用時將返回的JSON轉譯為
{"TemplateName":"Custom_Image_Ecs","SpecificationName":"dataDiskSize","CustomData":"30T"}。
在排序後的字串最後加上該服務的密鑰。
服務密鑰的字串格式為:Key={ServiceProviderKey}。您可在服務詳情頁,擷取服務密鑰。

加上服務密鑰後的字串如下:
ExpireTime=2022-11-02T02:39:43Z&LicenseMetadata={"TemplateName":"Custom_Image_Ecs","SpecificationName":"dataDiskSize","CustomData":"30T"}&RequestId=CF54B4C9-E54C-1405-9A37-A0FE3D60xxxx&ServiceInstanceId=si-85a343279cf341c2xxxx&Key=37131c4a485141xxxxxx使用MD5密碼編譯演算法對處理後的字串進行加密,加密後得到的為32位小寫值。
加密完成後,得到的值為:21292abff855ab5c2a03809e0e4fb048。
服務商通過上述方法計算得到的Token值,與計算巢返回的Token值進行對比。若兩個值相同,則表示資料未被篡改。
範例程式碼
代碼中的樣本響應可通過校正服務執行個體有效期間,PushMeteringData - 推送計量資料擷取。
Python
import json
import hashlib
def format_value(value):
"""格式化參數值,處理不同類型的參數"""
if isinstance(value, bool):
return "true" if value else "false"
elif isinstance(value, dict):
# 處理字典類型,產生鍵=值格式,索引值對之間用逗號加空格分隔
items = []
for k, v in value.items():
# 將布爾值轉為小寫字串,其他值轉為字串
v_str = str(v).lower() if isinstance(v, bool) else str(v)
items.append(f"{k}={v_str}")
return "{" + ", ".join(items) + "}"
elif isinstance(value, list):
# 處理清單類型,直接轉為JSON格式
return json.dumps(value, separators=(',', ':'))
elif isinstance(value, str):
try:
# 嘗試解析字串為JSON對象(如Components的值)
parsed = json.loads(value)
if isinstance(parsed, dict):
# 如果是字典,轉為標準JSON格式
return json.dumps(parsed, separators=(',', ':'))
elif isinstance(parsed, list):
return json.dumps(parsed, separators=(',', ':'))
else:
return value
except json.JSONDecodeError:
return value
else:
return str(value)
def calculate_token(response_json_str, service_key):
response = json.loads(response_json_str)
result = response.get("result", {})
# 提取所有非Token參數並格式化
params = {}
for key, value in result.items():
if key.lower() != "token":
formatted_value = format_value(value)
params[key] = formatted_value
# 按首字母排序參數
sorted_params = sorted(params.items(), key=lambda x: x[0].lower())
# 產生拼接字串
query_string = "&".join(f"{k}={v}" for k, v in sorted_params)
print(f"產生拼接字串: {query_string}")
# 添加服務密鑰
final_str = f"{query_string}&Key={service_key}"
print(f"添加服務密鑰後的字串: {final_str}")
# 計算MD5
md5_hash = hashlib.md5(final_str.encode()).hexdigest().lower()
return md5_hash
# 樣本用法
if __name__ == "__main__":
# 樣本響應(替換為實際響應)
response_json = r'''{
"code": 200,
"requestId": "4ea52d12-8e28-440b-b454-938d0518****",
"instanceId": "i-0jl1ej1czubkimg6****",
"result": {
"RequestId": "CF54B4C9-E54C-1405-9A37-A0FE3D60****",
"ServiceInstanceId": "si-85a343279cf341c2****",
"LicenseMetadata": "{\"TemplateName\":\"Custom_Image_Ecs\",\"SpecificationName\":\"dataDiskSize\",\"CustomData\":\"30T\"}",
"Token": "21292abff855ab5c2a03809e0e4fb048",
"ExpireTime": "2022-11-10T08:03:16Z"
}
}'''
service_key = "37131c4a485141xxxxxx" # 替換為實際服務密鑰
calculated_token = calculate_token(response_json, service_key)
print(f"計算得到的Token: {calculated_token}")
print(f"返回的Token: {json.loads(response_json)['result']['Token']}")
# 驗證是否一致
print(f"驗證是否一致: {calculated_token == json.loads(response_json)['result']['Token']}")Java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.security.MessageDigest;
import java.util.*;
public class TokenVerifier {
private static final String REQUEST_ID = "RequestId";
private static final String TOKEN_KEY = "Token";
// --- 測試案例 ---
public static void main(String[] args) {
String responseJson = "{\n" +
" \"code\": 200,\n" +
" \"result\": {\n" +
" \"ExpireTime\": \"2022-11-02T02:39:43Z\",\n" +
" \"LicenseMetadata\": \"{\\\"TemplateName\\\":\\\"Custom_Image_Ecs\\\",\\\"SpecificationName\\\":\\\"dataDiskSize\\\",\\\"CustomData\\\":\\\"30T\\\"}\",\n" +
" \"RequestId\": \"CF54B4C9-E54C-1405-9A37-A0FE3D60****\",\n" +
" \"ServiceInstanceId\": \"si-85a343279cf341c2****\",\n" +
" \"Token\": \"21292abff855ab5c2a03809e0e4fb048\"\n" +
" }\n" +
"}";
String serviceProviderKey = "37131c4a485141xxxxxx";
boolean isValid = verifyToken(responseJson, serviceProviderKey);
System.out.println("Token驗證結果: " + isValid);
}
/**
* 根據JSON響應計算Token並驗證
*/
public static boolean verifyToken(String responseJsonStr, String serviceProviderKey) {
try {
// 1. 解析JSON響應
ObjectMapper objectMapper = new ObjectMapper();
JsonNode responseNode = objectMapper.readTree(responseJsonStr);
JsonNode resultNode = responseNode.get("result");
// 2. 提取參數並格式化
Map<String, String> params = new HashMap<>();
Iterator<String> fieldNames = resultNode.fieldNames();
while (fieldNames.hasNext()) {
String key = fieldNames.next();
if (!key.equalsIgnoreCase(TOKEN_KEY)) {
Object value = resultNode.get(key);
String formattedValue = formatValue(value);
params.put(key, formattedValue);
}
}
// 3. 排序參數
List<Map.Entry<String, String>> sortedParams = new ArrayList<>(params.entrySet());
sortedParams.sort(Comparator.comparing(entry -> entry.getKey().toLowerCase()));
// 4. 產生拼接字串
String urlParams = buildOrderUrlParams(sortedParams);
String finalStr = urlParams + "&Key=" + serviceProviderKey;
System.out.println("拼接後的字串: " + finalStr);
// 5. 計算MD5
String calculatedToken = generateMD5(finalStr);
// 6. 擷取返回的Token
String returnedToken = resultNode.get(TOKEN_KEY).asText();
System.out.println("計算得到的Token: " + calculatedToken);
System.out.println("返回的Token: " + returnedToken);
// 7. 驗證一致性
return calculatedToken.equals(returnedToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
private static String formatValue(Object value) {
if (value instanceof Boolean) {
return (Boolean) value ? "true" : "false";
} else if (value instanceof JsonNode) {
JsonNode node = (JsonNode) value;
if (node.isObject()) {
// 處理物件類型,產生鍵=值格式
List<String> items = new ArrayList<>();
Iterator<String> fieldNames = node.fieldNames();
while (fieldNames.hasNext()) {
String key = fieldNames.next();
Object childValue = node.get(key);
String formattedChildValue = formatValue(childValue);
items.add(key + "=" + formattedChildValue);
}
return "{" + String.join(", ", items) + "}";
} else if (node.isArray()) {
// 處理數群組類型(按需)
return node.toString().replace(" ", "");
} else {
return node.asText();
}
} else {
return value.toString();
}
}
private static String buildOrderUrlParams(List<Map.Entry<String, String>> entries) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : entries) {
sb.append(entry.getKey())
.append("=")
.append(entry.getValue())
.append("&");
}
if (sb.length() > 0) {
sb.setLength(sb.length() - 1); // 去除最後一個&
}
return sb.toString();
}
private static String generateMD5(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hash = md.digest(input.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = String.format("%02x", b);
hexString.append(hex);
}
return hexString.toString();
} catch (Exception e) {
throw new RuntimeException("MD5計算失敗", e);
}
}
}確保專案中包含 com.fasterxml.jackson.core 庫(如 Maven 依賴):
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version> <!-- 推薦使用最新穩定版 -->
</dependency>