API 双方认证探讨
开放 api 已是大势所趋。而 api 这种东西有个特点就是覆水难收。一旦公开出去了,被大量用户使用,一旦修改,就会让广大用户都掉坑里。所以,api 在设计之初就要尽量考虑周全,并预留扩展可能。
目前绝大多数 api 都是通过 http 协议访问。api 一般有两类,一类只涉及到提供方和使用者,另一类还涉及到最终用户。因此,前一类在认证上也只涉及两方,而后一类还涉及到用户授权,也就是通常使用的 OAuth。这里先说说第一类。比如 amazon aws 的 api、google 的地图 api 等的认证就属于这类。
双方认证的 api 通常的认证方式是使用一组 id 和密钥,用 id 来标记应用,用密钥来对请求做签名。id 和密钥一般是应用向服务方申请的。这样,服务费也可以控制 api 的使用。如果想停止某个用户访问 api,只需要取消这个用户的认证信息。即可。
举个例子,google 地图的 api,就是简单使用密钥对 url 做 HMAC_SHA1 签名。如:
$signature = hash_hmac("sha1",$url_to_sign, $key, true);
然后把签名编码后,追加在 url 后面访问。服务方根据请求中的 id 找到对应的密钥,同样做一遍签名,如果相等则认证通过。对于一些更复杂一些的 api ,请求还可能通过 HTTP header 、post body 来传递。此时,签名就需要把所有关键信息都一起签名。为了避免参数顺序的敏感性,即仅仅改变参数顺序就会影响签名结果,通常还会要求先对参数进行排序。amazon aws 的新版认证协议还对于一些关键参数改为了每次加入一个就签名一次。这恐怕也是因为 sha1 也已经没那么可靠了吧。
这是最简单的认证方式。使用这种方式,由于密钥并没有在网络上传递,hmac sha1 算法也是不可逆的,第三方无法冒充别的用户对自己的 url 进行签名。但是这种简单的方式有个缺点。同样的 url 在签名后是可以反复使用的。因此,如果恰好第三方需要的服务或数据和你的相同,那也可以直接复制你的 url 去用,花你的 money 、读你的数据。这也叫重放攻击。
为了避免或者减少重放攻击的危害,一些 api 把时间也作为了参数之一。签名的时候也把时间一起签进去。在服务端不仅要比较签名,还要请求的时间和服务端时间的差是否在许可范围,比如 15 分钟内。amazon aws 的 api 就是如此。这样,就算截获了一个请求,也顶多在 15 分钟内有效。不过由此带来的问题是,如果时钟偏差大了,就无法正常调用服务了。因此,客户端和服务端都必须严格对时。如在 linux 上开启 ntpd 。另外,对于 15 分钟内的重复攻击,仍然没有很好的办法。这时候,能做的也许就是在业务上减少由此可能带来的损失了。比如对 aws 来说,重复申请资源会因为重名而无法通过,而重复修改或删除资源请求通常也是没意义的。而另一些资源信息,仅在 15 分钟内可以重放,也无法获得新的有价值的数据。
如果真的在意这一点又该如何呢?以上方法都是在保持协议无状态前提下的。如果放弃这个前提,变成有状态协议,就有了别的方法。比如,可以引入一次性令牌来解决。首次访问前,先去请求一个一次性令牌,之后请求服务的签名的时候,把这个令牌也一起签名,并一起传递。服务端会检查该令牌是否仍然有效。请求后,会使原来的令牌失效,同时颁发一个新的一次性令牌。这看着是不是有点眼熟?有点像是 session 的实现方式吧。session 就是在客户端和服务端传递一个令牌来标示身份,在无状态协议中模拟状态的。第三方就算劫持到令牌,因为没有密钥,也没法加以利用。而拦截到的请求在使用一次后,也已经失效。这样就有效避免了重放攻击。但是,这样也是有代价的,如此一来,服务端就必须来维护这些会话。可能会占用大量资源,并有可能由此受到拒绝服务攻击。因此,多数服务都没有采用这种方式来认证。
以上的认证方式只适合用在服务端-服务端。而不适合用在客户度,如浏览器、移动端、桌面客户端等。在这些地方使用双方认证 API ,就不可避免的要把应用的密码携带分发。这是很危险的。一旦有人从中破解获取了你的密钥,一切就全完蛋了。这时候,要通知所有终端更换密钥也是件麻烦事。通常,都需要使用应用自己的服务端作为代理来访问。至少是通过应用自己的服务端来计算签名。但是这对于一些服务,比如云存储,代价就有点大了,这样一来,应用就还是得在自己的服务器上走大量的流量。要解决这样的问题,一些 API 提供方给了另外的一种认知方式。
应用先给自己的每个用户生成一个 uid,然后用密钥对其签名得到用户密钥,把应用 id,uid,签名保存在客户端分发。从客户端发起请求时,带上应用 id,uid,使用用户密钥签名。服务端则用同样的方式验证,并可以通过 uid 来做数据的访问控制。这样一旦客户端被破解,也只会影响一个终端用户,而不是像之前一样,所有用户的信息都收到威胁。