一个接口签名使用错误的案例

Categories:

背景

当和第三方通讯的时候,对接口进行鉴证是很有必要的事。
在原有参数的基础上,额外增加appid、timestamp、nonce、sign是一个较常见的做法。

偶然看到一个项目里也用了这么一套方案。

具体举例如下,假设某接口的参数为

{
  "name": "zhangsan",
  "age": 14,
  "phone": "15012341234"
}

那么它的签名计算逻辑为

  1. 先对body求md5。
    md5(body) = md5('{"name":"zhangsan","age":14,"phone":"15012341234"}')  
              = a7f8196bcfc81d0a8a13fe4901047088
    
  2. 再将md5(body)和appid、timestamp、nonce、secret等按字典排序后计算sign
    string1 = "appid=xxxxx" +  
               "&body=a7f8196bcfc81d0a8a13fe4901047088" +  
               "&nonce=xxxxxx" +  
               "&secret=xxxxxxx" +  
               "&timestamp=1111111111"    
     sign = md5(string1)
    

至于服务器端,只做了appid、nonce、timestamp的必传校验,和timestamp的5分钟限制。

问题

小伙伴们有发现这里的问题在哪吗?

第一个问题就是nonce了。

第二个问题在body的序列化上,对开发人员不友好。

揭秘

nonce的作用是什么?

签名方案用到的参数,按作用分3部分:

  1. 报文体
    签名的意义就是防止别人篡改、伪造报文体,因为报文体本身作为参数参与签名计算,一旦内容发生变化,必然导致签名变化。

  2. appid + secret
    secret是签名的密码,如果接口同时提供给多个第三方用,那么每个第三方用不同的密码是很有必要的。
    即使接口只会提供给一个第三方,考虑到增加appid的成本低及未来的可能,保留appid这个参数的收益是远远大于成本的。

  3. 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 原始报文。他说,

客户端签名的时候,要先将自己的报文体序列化成字符串,然后参与签名,所以他签名的包问题和服务器端收到的原始报文肯定是一样的。

嗯,技术上确实没有问题。

但是我认为

  1. 这对客户端的开发人员不友好,因为和行业的常见做法有出入。
  2. 服务器端去读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规范的说明,哈哈

如果你觉得本文对你有帮助或不错,可略表心意,请我喝一杯冰可乐。

Comments