本文介绍如何校验数字签名信息。
背景信息
在计算巢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 - 推送计量数据获取。
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']}")
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>
- 本页导读 (1)
- 背景信息
- 数字签名信息校验流程
- 消息处理流程
- 示例代码