一个接口签名使用错误的案例
背景
当和第三方通讯的时候,对接口进行鉴证是很有必要的事。
在原有参数的基础上,额外增加appid、timestamp、nonce、sign是一个较常见的做法。
偶然看到一个项目里也用了这么一套方案。
具体举例如下,假设某接口的参数为
{
"name": "zhangsan",
"age": 14,
"phone": "15012341234"
}
那么它的签名计算逻辑为
- 先对body求md5。
md5(body) = md5('{"name":"zhangsan","age":14,"phone":"15012341234"}') = a7f8196bcfc81d0a8a13fe4901047088
- 再将md5(body)和appid、timestamp、nonce、secret等按字典排序后计算sign
string1 = "appid=xxxxx" + "&body=a7f8196bcfc81d0a8a13fe4901047088" + "&nonce=xxxxxx" + "&secret=xxxxxxx" + "×tamp=1111111111" sign = md5(string1)
至于服务器端,只做了appid、nonce、timestamp的必传校验,和timestamp的5分钟限制。
问题
小伙伴们有发现这里的问题在哪吗?
第一个问题就是nonce了。
第二个问题在body的序列化上,对开发人员不友好。
揭秘
nonce的作用是什么?
签名方案用到的参数,按作用分3部分:
报文体
签名的意义就是防止别人篡改、伪造报文体,因为报文体本身作为参数参与签名计算,一旦内容发生变化,必然导致签名变化。appid + secret
secret是签名的密码,如果接口同时提供给多个第三方用,那么每个第三方用不同的密码是很有必要的。
即使接口只会提供给一个第三方,考虑到增加appid的成本低及未来的可能,保留appid这个参数的收益是远远大于成本的。timestamp + nonce
如果有人截获了http请求后,不修改http请求,直接把整个http请求重放,会怎样呢?
显然在任何参数都不篡改的前提下,sign是正确的。因此为了防止http重放攻击,增加了nonce参数。
nonce不是一个英文单词,它是Number used once或Number once的缩写,在密码学中Nonce是一个只被使用一次的任意或非重复的随机数值。
这样如果服务器收到一个相同的nonce,就可以拒绝该请求了。
但是相同的nonce如果不加时间限制,难道要将10年甚至100年内用过的nonce都存起来吗?
因此又加了timestamp,如限制5分钟内nonce不能重复。
所以,该项目里没对nonce判断是否在timestamp内重复,是不能防止5分钟内的重放攻击的。
body的序列化有什么问题?
从文档的描述上看,使用的是json的序列化。
但是,在json的规范里,json是无序的。
举例,
{
"a": 1,
"b": 2
}
被序列化成{"a":1,"b":2}
或{"b":2,"a":1}
都是符合规范的。
虽然目前大多数的json序列化算法实际上都会按定义顺序将其序列化成{"a":1,"b":2}
,
但是万一就有一个库将其序列化成{"b":2,"a":1}
呢?
毕竟json规范就规定了json是无序的。
我当时向代码作者提了这个问题。作者答复我,
我这里不涉及json的序列化,我取的是http报文的原文。
我当时愣了下,去看了他服务器端校验签名的代码。果然,他是直接读取request对象里的raw 原始报文。他说,
客户端签名的时候,要先将自己的报文体序列化成字符串,然后参与签名,所以他签名的包问题和服务器端收到的原始报文肯定是一样的。
嗯,技术上确实没有问题。
但是我认为
- 这对客户端的开发人员不友好,因为和行业的常见做法有出入。
- 服务器端去读http的原始报文似乎也不是一个好的做法?
因为json的流行,所以大多数http库都自带了将对象序列化成字符串的处理。以RestTemplate为例,
RestTemplate restTemplate = new RestTemplate();
Employee employee = new Employee("Adam", "Gilly", "test@email.com");
Employee createdEmployee = restTemplate.postForObject(uri, employee, Employee.class);
如上,post传入的请求参数是一个employee对象,RestTemplate会自动将employee序列化成字符串再将http请求发出去。
但是,如果按照上文提到的签名要求,第三方的开发者必须自己手动将报文体序列化。
RestTemplate restTemplate = new RestTemplate();
Employee employee = new Employee("Adam", "Gilly", "test@email.com");
String emplyeeString = JSON.stringify(employee);//必须先自己将对象序列化后传入
Employee createdEmployee = restTemplate.postForObject(uri, employeeString, Employee.class);
后续
呃说实话,昨晚写完body序列化问题的时候,我觉得序列化这里好像也没什么问题。
毕竟,只要是统一的规则,都可以在框架层、aop层等统一拦截处理。
所以特意补上后续,权当对json规范的说明,哈哈
如果你觉得本文对你有帮助或不错,可略表心意,请我喝一杯冰可乐。 ☕