前端爬虫攻防之接口签名方案

导语

本文通过爬虫与接口服务的攻防回合制,简单介绍如何使用签名来提高接口安全性。道高一尺,魔高一丈。道不服,再高1.5丈。

背景

如何提高api 接口的安全性?

服务端将接口提供给客户端,在公网开放访问,非常危险。我们既要确保客户端与服务端之间的安全通信,又要考虑防爬防篡改等恶意攻击。没有绝对安全的接口,我们只能做的让接口相对安全一些。提高接口安全性的方法一般分为以下几个方面:

  • 服务端鉴别:服务端根据请求特征、行为等判断请求是否是爬虫或者机器。

  • 动态签名:针对每次的请求,根据参数不同,按照一定算法规则动态生成相应的签名,服务端验证签名合法性,主要防篡改。

  • 身份验证:需要用户登录,后端在处理接口之前先校验用户信息合法性,然后再处理接口。

  • 数据加密:针对安全性要求较高的请求做数据加密,比如游戏密令登录,支付转账等。

本文主要探讨接口在使用动态签名的机制下,爬虫与接口的相互攻防策略。

故事开始

一个很普通的晚上,一段代码悄悄运行,代号 -爬虫。

接口的访问量突然升高报警不断。就这样一段爬虫与接口的博弈,拉开序幕。

牛刀小试

1、 【攻】数据采集 脚本抓取

爬虫找到数据接口,通过for 循环更改page 页码参数,很轻松的采集到了该接口下的所有数据。但并不满足,还写了定时脚本,每天晚上了2点准时开跑。

突然有一天,接口失效了。接口由原来的https://xxx.58.com/zufang/list?page=1变为https://xxx.58.com/zufang/list?page=1&sign=de2e67afcc554c1840886a96e2211589 。

是的,爬虫的行为被接口察觉到了,接口反手做了签名处理。

2、 【防】参数篡改 — 尾随签名

首先,业务代码中需要安装一个请求接口的sdk,sdk中封装了签名逻辑。

SDK中签名步骤大致如下:

  • 将所有业务请求body参数序列化成一个json。

  • body参数和url中的请求参数通过Sign函数md5加密得到签名sign。

  • 将签名追加在url 参数中,随请求一起发送。

经过签名处理后,爬虫将参数篡改后发送的请求,接口能够识别出来,并直接拒掉返回。

崭露头角

然而并没有难倒爬虫,url 虽然经过了签名处理,但参数相同的情况下,签名出来的url是固定的。

1、【攻】参数签名– url 收集

大致步骤如下:

  • 用一个数组将每个接口完整地址(含签名)收集并存储起来。

  • 循环将数组中的接口挨个请求。

收集url过程虽然有点麻烦,但却是一劳永逸的事。

接口侧很快发现之前的策略并没有有效的制止住爬虫,于是做了升级

2、【防】url重复利用 — 签名引入时间

客户端计算签名时候,请求参数中添加t=当前时间,然后计算签名,当前时间随参数发送到后端,后端校验签名的时间是否在一段时间范围内,如果超过一定时间,则判断签名非法。

大致步骤如下:

  • 计算签名时,将当前时间一起算入签名中。

  • 发送请求时候,将时间与签名放入参数中,一起随请求发送。

  • 后端可根据签名,验证参数的合法性,根据时间,判断签名有没有过期。

引入时间因子后,签名出来的url在一定时间后会失效。爬虫无法将一个签名长期使用。

针锋相对

爬虫发现请求参数、时间均可在客户端生成,既然不能重复的利用现有的签名,那就自己去生成签名。

1、【攻】动态签名 — 签名伪造

自己生成签名,其实不易,但阻止不了一个爱钻牛角尖的爬虫。

A、逆向代码,肉眼找到相关逻辑

B、借助工具格式化

C、抽取签名函数,爬虫主程序修改参数重新计算签名,然后抓取。

提取出签名函数后,重新计算签名,与正常用户请求的数据一模一样。并且签名所需要的各种因素均可自给自足,简单方便有效。

接口不得不再次升级,之前只对签名正确性做校验,没有对签名合法性做校验。所以决定在签名因子中加入一个token字符串。这个字符串,只能后端生成,后端校验。

2、【防】客户端伪造签名 — 引入token

  • 服务端收到请求,首先验证cookie 中的token 是否合法,然后验证签名是否合法。

  • 服务端在返回数据时,将重新生成token 随cookie下发至浏览器。

SDK中根据token生成签名代码如图:

整个流程大致如下:

  • 生成签名时,客户端从cookie 中获取 token 值。将token 值加入到签名规则中。

  • 服务端收到请求,首先验证cookie 中的token 是否合法,然后验证签名是否合法。

  • 服务端在返回结果时,在cookie 中重新设置新的token。

  • 客户端第一次请求,当cookie中没有token时候,服务端返回特殊的错误状态码,sdk 识别这个状态码,会进行第二次请求(第二次会拿到第一次返回的token)。

token 由后端生成,随cookie 下发,客户端无法伪造,也不知道生成规则,无法自给自足伪造签名。

神仙打架

一段时间后,爬虫发现接口失效,排查发现接口并没有增加新的参数,而且发现一个奇怪的现象:

在控制台中抓到一个链接,能正常返回数据。然后将这个链接拷贝出来,在浏览器访问,则提示请求非法。

爬虫百思不得其解。同样的浏览器,同样的环境,上一次请求,第二次就不行了。两次请求区别在哪儿?

此时一道的闪电劈中爬虫的天灵盖,脑中闪过一道灵感 “cookie 中有诈”

经过爬虫分析,发现每次签名都会将cookie 中的一个字符串带入进去,并且,每次请求完成后,这个cookie 被响应头给重置了。所以一个链接在浏览器里面第二次使用的时候,cookie 里面的token 已经发生变化,导致后端校验不通过。

爬虫很快对程序进行修改。

1、【攻】动态签名 — 浏览器行为模拟

大致步骤如下:

  • 第一次请求,必返回异常(没有token),将返回header 中的cookie 记录下来用于第二次请求。

  • 请求中随机构造浏览器ua 等参数,突破后端的反爬策略。

  • 从第二次请求开始,每次请求返回的token,用map 存储起来,便于下次使用。

v4.0的爬虫犹若开启了神智,签名不管什么策略,复杂程度,都能模拟。而且市面上大部分的签名程序都能通过相似的步骤破解 。

接口方脑袋疼。爬虫请求来的数据和正常用户的一模一样,还有没有什么办法辨别?

同样的,那道劈中爬虫的闪电又劈中了接口的天灵盖。


2、【防】爬虫利用token — 行为统计

  • token 的设计,一千个人就有一千个设计。token 的生成和校验是一个高频行为,所以token的生成要保证高效 、加密,随机、体积小。

  • 爬虫与正常用户最大的差别是行为上访问频次不同,如果token 能记录下一个用户在一段时间内的访问频次,那从行为上可以区分出爬虫和正常用户,所以我们token 的设计除了用来校验合法性以外,我们还要记录用户一段时间内的访问频次。

  • 接口采用9位数组存放token, token中包含随机数,时间,请求量,以及加密校后的校验位置。

  • 服务端不对token 做存储,只校验token 的合法性校验。

  • t在一定允许时间内,token中的时间不更新,用做漏斗计数,记录用户在这段时间内的访问频次。

token 中采用漏斗计数策略后,从访问频次大致可以区分爬虫和正常访问。从而拒绝掉一些高频流量访问。

3、还有一个问题,如果接口每次都不利用上一次token,token传空怎么处理?

这儿在sdk 侧有个简单处理,当token 为空时,后端返回一个token 为空的错误状态,但返回错误状态时,cookie中会下发token。sdk此时会进行第二次请求,第二次则能够拿到正确数据。

爬虫也可以利用这个原理,绕开token行为统计。

接口其实还有一层处理,


异常token IP计数

上面代码中:
没有token的请求,我们拿到ip ,将ip转换为32位int,在一个int 的map中加一,并判断map中这个ip是否超过一定阈值,如果超过则拒绝返回。

4、代码中有几个细节简单说明一下:


A、为什么这个规则只应用于没有token的请求,而不应用于所有的请求。

应用于所有请求,容易大面积误伤。学校,机场等公共wifi,大部分人的出口ip是同一个,很容易命中策略。而对没有token的请求应用此策略,既能做到ip限制也可以降低误伤可能性

B、为什么不直接用ip 字符串作为key,需要转换为int作为key?

32位int 占4字节,在node 中是一个number 最多也就8字节,而字符串的话所占字节位7-15字节,int 存储可节省内存。

map 中number 作为key 效率相当于数组下标寻址,而用字符串做key,map 中还需要转一遍hash。number key 在存储和查询效率上会高出一大截。


5、说明完毕,回到正文


爬虫发现接口抓取在一定数量后,接口返回就异常了。一定是通过啥手段识别出来短时间内访问太多,爬虫进行了更进一步的调整。

点到为止

爬虫对着忽有忽无的接口,大致也猜到了接口的一些限流处理。然而并没有难住他。

1、【攻】高频被拒 — 限流访问

显示的规则破解sdk可以拿到签名代码,隐示规则可以自己写爬取策略,换ip、限频次、模拟行为、伪造内容,防不胜防。

策略全开的爬虫,有了那么一点灵性。这点灵性,接口如何来应对?

接口祭上了一份小小礼物。


2、【防】反编译 — 混淆加密

  • sdk 代码不宜全部混淆,混淆核心代码就行了。

原始代码:

将核心代码加密

加密的目是为了提高破解的难度,并不是从理论层面去达到代码的不可逆。所以我们加密的选型大致从两个维度考虑。
A、采用市面上已有的不可逆加密(至少不容易)
已有的将代码混淆的加密方案 Eval,
Array,_Number,JSfuck,JJencode,AAencode,URLencode,Packer,JS Obfuscator,My Obfuscate。均可被解密,且都能找到相应的开源解密工具,(不过可以使用两种以上的混淆方式,达到混合混淆,破解也有一定难度)。
上图代码采用的国内的jshaman 加密,不能保证完全不可逆,但至少还没有一个开源的将代码一粘就可以逆回来工具。
B、自己实现简单的可逆加密,但是逆向工程需要自己实现。
例如自己修改jjencode加密算法,然后混淆代码如下图:

首先说明一下,以上代码是可逆的,混淆逻辑就是将jjencode混淆函数中的混淆变量做了一些替换,逻辑上做了一些调整。但替换后有两个好处:

  • 无法用肉眼识别是采用的哪种混淆方案。

  • 无法用市面上现有的开源解密工具解密出来,除非爬虫精通市面上的大部分混淆方案,然后自己实现一个解密函数。

3、收!让我们回到主题

代码加密有什么用呢?加密代码和不加密代码,放在浏览器控制台不都能运行出相同的结果?

是的,在浏览器端都能运行出相同的结果,但是加密后的代码在node端则无法正常运行出正确的结果(黑人问号.jpg)。

例如代码中判断 window.document 对象是否存在,如果存在,则走正常的签名,如果不存在则返回错误的签名。由于混淆后的代码,不易反编译,所以爬虫无法知道里面的判断逻辑,无法伪造浏览器环境。进一步提高破解门槛。

秋色平分

1、【攻】混淆加密 — 完全浏览器模拟


逆向加密后的代码比较困难,但有个思路,本地装一个浏览器内核,调用浏览器api 执行js 就可以抓取数据了。

结尾

爬虫与接口没有绝对的胜负,两者的攻防较量都了献上了自己的智慧,当爬虫具备一些灵智,开始模拟用户环境和行为后,接口辨别爬虫与正常用户变得更为困难。接口签名只是提高提高伪造门槛,属于防爬的一个环节,拦住一些低级的爬虫。

当爬虫真的能完全模拟浏览器后,接口可开启接入反爬服务。反爬服务中,对ip,浏览器ua等有全方面的分析,爬虫虽然能完全模拟浏览器行为,但爬虫并没有那么多机器用来来部署,ip是有限的,最终也会被反爬


服务识别出来。但反爬服务会对接口性能造成一定影响,所以可作为最后的防线,选择性的开启。

本文 完

作者简介

龚虹宇,2017年8月加入房产事业部。主要负责58商业地产业务与前端基础服务的建设工作。

阅读推荐