API签名认证设计
设计目标
- 安全性:确保签名算法足够安全,防止篡改和重放攻击。
- 可扩展性:算法应支持不同的数据格式和传输协议。
- 易用性:算法应易于实现和集成,方便开发者使用。
算法设计
签名参数
- Access Key:每个API调用者持有的唯一标识。
- Secret Key:与Access Key关联的密钥,用于生成签名。
- Timestamp:请求发出的时间戳,用于防止重放攻击。
- Nonce:随机数,用于防止重放攻击。
- 请求方法:HTTP请求方法(如GET、POST等)。
- 请求路径:HTTP请求的相对路径。
- 请求参数:HTTP请求的参数,包括查询参数和请求体。
签名生成步骤
- 排序参数:将请求参数(包括请求方法、请求路径、请求参数等)按字典顺序排序。
- Header参数:x-ta-access-key、x-ta-timestamp、x-ta-nonce
- 查询参数:GET方法中查询参数
- 表单参数:POST方法中参数,使用JSON格式,需要JSON数据按照key平铺后设置字典key
- 对象字段通过"."拼接,如user.name
- 数组字段通过"[0]"拼接,如files[0]
- 排序方法:通过平铺后的key,按照ASCII升序排列
- 排序参数按照urlencode方式进行拼接
- 拼接字符串:将排序后的参数拼接成一个字符串。
- 拼接规则:Method + 空格 + 请求路径 + 空格 + 排序后的请求参数
- 生成签名:使用Secret Key对拼接后的字符串进行HMAC-SHA256加密。
- 添加签名到请求:将生成的签名作为请求头的一部分发送。
- 请求头为signature
签名验证步骤
- 解析请求:从请求中提取、Timestamp、Nonce等参数。
- 重新生成签名:使用提取的参数和Secret Key重新生成签名。
- 比较签名:将重新生成的签名与请求中的签名进行比较,验证是否一致。
- 检查时间戳:检查Timestamp是否在有效的时间范围内,防止重放攻击。
代码实现
- 平铺json数据
- 签名算法
Java版
public static Map<String, Object> flattenMap(Map<String, Object> map, String parentKey, String separator, TreeMap<String, Object> sortedMap) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
String newKey = parentKey.isEmpty() ? key : parentKey + separator + key;
if (value instanceof Map) {
flattenMap((Map<String, Object>) value, newKey, separator, sortedMap);
} else if (value instanceof Collection) {
int index = 0;
for (Object item : (Collection<?>) value) {
if (item instanceof Map) {
flattenMap((Map<String, Object>) item, newKey + "[" + index + "]", separator, sortedMap);
} else {
sortedMap.put(newKey + "[" + index + "]", item);
}
index++;
}
} else {
sortedMap.put(newKey, value);
}
}
return sortedMap;
}
Python版
def flatten_dict(d, parent_key='', sep='_', include_lists=True):
items = []
for k, v in d.items():
new_key = f"{parent_key}{sep}{k}" if parent_key else k
if isinstance(v, dict):
items.extend(flatten_dict(v, new_key, sep=sep).items())
elif include_lists and isinstance(v, list):
for i, item in enumerate(v):
sub_key = f"{new_key}_{i}" if new_key else str(i)
if isinstance(item, dict):
items.extend(flatten_dict(item, sub_key, sep=sep).items())
else:
items.append((sub_key, item))
else:
items.append((new_key, v))
return dict(items)
Nodejs版
// 基于qs第三方库
const qs = required('qs')
const flatParams = qs.stringify(obj, {
allowDots: true,
encode: false
})
问题记录
使用qs排序平铺参数
在qs中,排序平铺参数时,数组参数顺序为:files[0]、files[1]、files[10]、files[2],按照ASCII排序,files[10] < files[1]