gitbook/反爬虫兵法演绎20讲/docs/489218.md
2022-09-03 22:05:03 +08:00

291 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 12 | 反爬虫概述(四):前后端都不合适的时候如何进行处理?
你好我是DS Hunter。
上一讲,我们谈到了无收益的前端是如何为反爬提供基础保障支持的。但是你应该是知道的,前端没有什么秘密可言。那么,那部分前端不能做的,后端不好做的,我们放在哪里处理呢?
是的最终还是BFF扛下了所有。BFF天生就是为了这些事情而生的。
在当前的浪潮下大家似乎将BFF与Node.js划了等号事实上并非如此。BFFBackend For Frontend服务于前端的后端仅仅是一种设计思路只不过Node.js在当前环境下是最优解而已。实际上很多公司都会有一个API层来进行服务转发这一层就相当于BFF层它未必是Node.js站点。
这一讲我们就来看一下BFF的主要功能以及实现方式。在学习的过程中我也希望你能理解BFF存在的真正价值。我们先从BFF形成的第一步选型开始了解吧。
## BFF选型
关于BFF选型理论上我们可以使用任何语言。但是实际上90%以上的公司最终会选择Node.js。
### Node.jsExpress
Node.js可以承载常规http请求并且因为与前端使用相同的编程语言JavaScript因此能很方便的让前端人员兼职掉BFF工作。这是常规公司选型的理由之一。对于反爬来说我们有更进一步的选型原因
1. 我们之前提到的加解密是成对出现的客户端解密BFF加密出于方便测试的理由使用JavaScript做为BFF语言会十分方便。
2. 反爬虫系统是一个高并发的非关键系统其余语言需要额外引入支持Node.js默认是异步的天生为高并发而生。
3. JavaScript代码处理可能涉及AST转换 Node.js有足够多的工具可以支持。
基于这些理由Node.js几乎成了唯一解。但是万事不是绝对的。对于部分公司来说实在不能选择Node.js的时候也可以被迫使用别的技术栈。
被迫选择其余技术栈的理由千差万别,但通常是历史原因或基建支持不足。但是,现实世界的架构也常常不是最优解。我这里的建议就是:妥协吧,编码不方便,可以通过增加人力来解决,这不是无解的问题。
## 主要功能:中转以及聚合
BFF的功能往往是聚焦于网络请求的中转以及聚合。中转很简单就是前端服务请求过来BFF承接了请求然后处理后转发给后端。而聚合则是多个请求被合并一次性请求后端降低后端的QPS。别人用BFF往往只是用于中转和聚合而我们享受了BFF的好处拿来做反爬。那么这里我们就来讨论一下中转以及聚合部分的机器配置以及相应的反爬操作。
从BFF机器配置来看我们为了承载高并发就要增加资源来承载。但是增加哪方面的资源是机器数还是增加配置这个取决于场景。
### 机器配置:网络负载分类
我们经常说高并发高负载但是实际上业务上存在两种不同的负载方式一种是请求量多也就是链接数高。另一种是流量大或者资源消耗多也就是每秒的byte数多或者需要的业务逻辑多。这两者的处理方式是不同的。当然也有业务是两者都多这就没有什么取巧办法了。
针对链接数高其实可以通过增加机器数来实现。简单的说比如你使用1C2G的机器顶住了网络流量。这个时候链接数忽然增加到8倍你是增加至8台1C2G的机器还是替换成一台8C16G的机器呢其实两者成本是一致的但是我们还是会选择8台机器因为更多的网卡意味着更多的负载。
但是是不是永远这样选择呢也不一定。我们可以看一下第二种情况流量大、资源消耗多。这种情况下可能内存大的“一台8C16G”的机器就是首选了。
当然事情还有反转的可能如果系统对实时性要求很高你可以承受频繁GC但是不能承受长时间GC那么可能大内存又不是首选了。这个在选型的时候要慎重考虑。
如果非要有一个结论的话,那么其实反爬虫系统大部分是对实时性有要求,所以低配置多机器数通常是首选。但是这并非绝对的,还是要看具体的需求来慎重选择。
### 具体操作key与随机值
有了合适的机器我们就可以考虑具体操作的问题了。我们从反爬验证的核心问题“key的生成”开始说起。
* **key生成相关**
我们在[09讲](http://time.geekbang.org/column/article/486912)提到过key生成逻辑并不复杂并且无需存储因此可以直接在BFF生成不需要动用后端生成再用BFF转发。而且加密本身是一个计算密集型的需求不是IO密集型所以这个对CPU的消耗还是比较可观的。
但是考虑到反爬是一个低ROI的项目成本节约是重中之重。所以我们来分析一下目前面临的问题Node.js天生是为单CPU设计的而Cluster本身只是充分利用了多核并没有什么额外的优化。所以对于Docker部署的公司来说可以把机器分散成多个一核的实例这样可以充分降低成本。
这里有两个理由。第一与其用不靠谱的Cluster来实现多核优化还不如用成熟的Docker技术来隔离反正总核数不变。第二核数少的话配置Docker的人可以使用一些边角料来凑成本能大大降低。例如公司的k8s人员分配了一堆高配机器之后还剩余一些机器没办法分配出去由于我们的需求是单核因此无论剩余什么边角料都可以使用上这个成本能低很多。
此外考虑到BFF是一个集群那么发布过程中需要考虑key兼容的情况。
例如发布了50%的机器那么50%机器用了新版的key生成与校验逻辑另外50%还使用的是旧版,这里就会存在发布常见的交叉问题。要么发布过程中停掉反爬,要么在新校验逻辑里要兼容上一版。停掉反爬代价会比较大,而且很难进行线上观察测试。
因此我们的验证模块通常会对上一版做兼容。要知道key生成并非一个频繁变更的需求所以必要时可以通过多一次发布来解决这个问题。也就是一次发key的生成方式 一次发key的验证方式。
* **key验证相关**
key验证方法在[09](http://time.geekbang.org/column/article/486912)已经讲过了这里不再赘述。今天我们主要论述的是针对刚刚提到key验证中“多一次发布”的问题产生的相对应的key验证问题。
我们先看一下key验证的版本问题是怎么产生的。假设我们的key生成使用的是v1版本验证使用的也是v1版本。那么我们要将他们都变更为v2版本。由于发布途中的n台机器只有1台发布完成了那么如果用户请求key的时候访问了v2版本生成的key然后取v1版本去验证key毫无悬念这个验证是不可能通过的。那么这个时候我们就需要进行线上版本兼容处理。
**首先发布的新版本应该支持所有前置版本的key生成以及验证逻辑并且支持配置。**例如我们刚刚发布途中1台机器支持v2的key生成与验证n-1台支持v1的生成与验证那么我们的配置应该是生成key配置使用v1验证key配置也使用v1。全部发布完毕之前实际上线上一直使用的是v1版本。全部发布完毕之后我们可以将一台机器的key生成调整为v2而验证key的配置设置为同时支持v1与v2。
这样线上部分用户使用v2版本的key大部分用户使用v1的key。慢慢地就可以逐渐灰度切换为v2版本key了。然后全部切换完毕v1版本key消失此时验证key版本设置为v2。我们就可以进行灰度的切换了。
整个过程会缓慢执行,**同时,要时刻监控我们前面提到的误伤统计报表。**这个报表是实时的所以可以用来做发布监控这也是他存在的最大意义。但是这里要注意一个细节我们误伤检测的理论是从价格页面带Cookie到订单页面检测Cookie假设没有Cookie就认为没问题。这个在平时没有问题**但是在发布时要考虑下用户操作延时这点。**
简单的说用户从价格页到订单页是有一个延迟的任何一个用户访问了价格都要通过几分钟各种比较才会真正下单根据经验一般是5到15分钟不等不过这个并非定值每个公司应该不一样可以在Cookie上加上时间戳自己线上测试下符合自己业务的时间段因此灰度变更的时间间隔不应该小于这个时间差否则可能导致命名误伤了用户但是误伤检测还没到发布的时候误以为策略是安全的最终因为变更过快引发生产事故。
* **随机值处理**
从刚刚的发布相关论述中,你会发现,我们通过集群实现了一个随机。实际上,由于集群本身是随机访问的,这里相当于有了一个天生的随机模块。所以我们可以使用集群本身直接做随机。这句话你可能不太理解,没关系,我们还是通过一个例子来讲解具体的操作方式。
举个例子,我们在[09讲](http://time.geekbang.org/column/article/486912)中提到过的随机放过。假设你想随机放过50%的爬虫那么我们并不需要编写随机模块对每次请求进行roll操作。实际上通过集群进来的访问已经随机过了他们被随机并且平均分配到了集群的每一台机器上。你只需要针对50%的机器配置上“对于任何验证key的请求不进行验证直接判定为key正确”就可以实现50%的随机放过了。
这样不仅降低了代码复杂度,避免出现随机不准的问题,还提升了效率。属于集群使用的小技巧了。当然,这种小技巧除了反爬,似乎也没有别的业务用得到了。
这里可以给你补充一点那就是如果你需要比较特殊的随机值例如你有10台机器你需要的概率却是55%这个需要变更5.5台机器很难做到你可以通过变更Nginx的weight来实现详情可以参阅Nginx的官方文档。不过这样会导致负载不均衡因此建议是用工整的随机值。如果不工整可以向上向下取值到一个合适的值。
## 实现方式自定义Node.js的engine
实际上我们可以通过定制Express的Engine创造一个新的扩展名来避免大量的切片直接实现BFF中转以及聚合的复杂操作。
### Express的Engine定制方法
我们都知道Express支持自定义Engine。默认情况下框架提供了一个非常好用的Engine。但是问题就是他太好用了一点也不混乱。而我们做反爬就是要做混乱让爬虫方摸不到头脑。因此定制Engine是一个势在必行的事情。
关于Express如何编写自己的 Engine可以查阅官方文档。我们这里主要讨论哪些方面我们要变更。
* **engine混淆**
Engine的混淆主要包括变量名混淆eval博弈AST转换以及虚拟机。
这里我主要用变量名混淆给你举例。至于其余的混淆方式大同小异都是定制Engine只是复杂度会提升不少。所以我们先用稍简单些的变量名混淆理解基本原理。
我们假设,原来是如下的代码:
```javascript
var a = 3;
var b = 4;
console.log(a-b);
```
但是,我们希望给爬虫方展示的是这样的代码:
```javascript
var $asbasdfewf = 3;
var $asbasdfevf = 4;
console.log($asbasdfewf - $asbasdfevf);
```
明显可以看得出虽然只有三行的demo但是下文的阅读难度比上文复杂了不少。
但是我们不能容忍自己在写代码的时候,书写下面的代码。恶心的代码是给爬虫看的,不是给自己看的。那么我们如何处理呢?
我们可以定义一个engine。假设定制完毕后我们的engine扩展名是as代表anti-spider 那么也许你的index.as文件可能看起来类似这样
```javascript
var $a = 3;
var $b = 4;
console.log($a-$b);
```
然后你的engine可能定制类似这样的代码
```javascript
// 遍历模版, 获取所有的变量名。
// 可能是正则, 可能是AST 看你喜欢, 以及实现难度
// 设置到variables里。
var variables = getAllVariables();
// 根据所有的变量, 创建一个字典, 实现旧变量名与新变量名的一对一mapping。
// 新变量名可以尽可能长, 并尽可能视觉上相似。 例如混用v和w 1和l之类的。
// 为了避免数字开头, 可以使用通用的开头, 例如$
var dict = createVariableDictionary(variables);
// 批量替换查找到的所有变量名。
// 老规矩, 正则或者AST
replaceAll(page, dict);
```
这样,我们就可以看到干净整洁的代码,而那些恶心的代码,就让爬虫一点点看去吧。
如果上面的代码看着迷惑没关系这说明你缺乏的是express定制engine的知识而不是缺乏反爬虫知识。你先去跟着官网demo定制一个简单的engine看下流程再回来看就能明白我们要干什么了。
其余的都是用类似的思路去做即可。定制engine虽然是个很非主流的功能甚至很多人都觉得Express支持这个东西简直匪夷所思。但是对于反爬虫来说这简直是神器。
* **engine效率**
定制engine最需要注意的就是效率。
我们为了方便很可能大量使用正则表达式对整个模版进行字符串扫描。这个复杂度至少是On的。而我们又需要频繁扫描加上不断的局部调整可能导致CPU过高。众所周知JavaScript的字符串操作性能堪忧。
因此这里可以进行一些必要的提速操作。在迫不得已的情况下甚至可以用其他语言来实现char数组操作就像编译原理课上的作业一样尽可能在较少的全字符串扫描中完成任务。这可能会很耗开发资源但是绝对是值得的。还记得咱们在[09讲](http://time.geekbang.org/column/article/486912)中提到过的几乎无解的计算力进攻嘛?这样的改变可以在一定程度上缓解这样几近疯狂的进攻。
也就是说,你消耗的资源不能过高,否则将产生资源的不对等,爬虫消耗的资源远远低于你,你就会陷入被动。而如果你提升了效率,消耗的资源并不高,那么你就可以相对放心很多。
* **engine切片**
切片主要是针对一些浏览器特征检测的。
我们假设你要检测location.href是否存在某个字符串你的URL包含一个关键字那么这里location.href就应该包含这个关键字那么一定不要直接写如果不包含就如何如何。这样被调试出来的概率太高了对方只要查找一下if就知道所有的判定在哪里了。那么这个代码应该怎么写呢
如果你的某一步加密操作是+3那么你的代码可以这样写
```javascript
var offset = 3*(+!!(location.href.indexOf(keyword)+1)
```
> 代码逻辑大概是:
>
> 如果存在keyword那么+1之后将得到一个大于0的数字。这个数字取反为false再取反为true再加个加号就是1了。然后用3来乘这个数字就得到了正常的offset。
>
> 如果没有找到,那么它是-1-1+1得到0两次取反为false再用加号转为Number是03\*0还是0这个offset就是0了。
>
> 如果offset错了那么会导致什么结果呢会导致key解密错误最终验证的时候验证失败。
>
> 但是整个过程没有任何一个if是不能直接查找搜索得到结果的。
那么这个代码我们是直接写到解密里面吗不是的我们代码里会放3\*11就是一个切片会被替换成这段js判定在不同条件下运行出不同的结果。
那么替换的动作就要由Engine来执行了。同时这个替换是随机的也就是不同的切片与不同的切片代码来互相替换实现代码的随机性。我们知道随机出现的代码是最影响调试的。
当然这个切片只是个思路核心在于消除if判定而不是全篇都是这样操作要想各种办法来变通。如果千篇一律对方只要搜索"+!!"这种关键字就可以找到你的切片了。万幸js有大量的特性bad parts可以供我们试用。
### Engine下的成对加解密
我们在[10讲](http://time.geekbang.org/column/article/487864)中提到了解密方法是逆排序的,那么这里就可以顺便看下做法了。简单的说,既然是逆序,那就是后进先出,也就是一个简单的栈操作。思路确定后,那么问题就简单了。
大体来说,代码如下:
```javascript
// 首先我们从题库roll出来几各加解密的pair
// 然后把他们临时保存一下
var methods = rollMethods();
// 我们设定一个
var methodStack = [];
// 这里要对methods进行遍历
// 针对key进行加密 每加密一次, 就在methodStack进行一次push操作
// methods的每一个item应该是一个加解密pair 加密方法当场用掉,
// 解密方法是一个字符串, 留在栈里备用
methods.forEach...
while(methodStack.length !== 0){
// 这里则需要不断的popmethodStack了。
// 然后推给engine 让engine按顺序拼接解密方法
// engine拿到解密方法 进行字符串拼接, 切片注入
// 以及其余的混淆, 然后加入通用模版, 得到浏览器端最终运行的js代码
}
```
这样我们的解密method无需与加密method进行mapping因为加密方法当场用掉了解密方法拼接到js中了两者都消费完了。所以不需要再mapping了。浏览器端解密完毕就直接得到key。如果得不到那就是解密失败会被验证模块拦截掉。
## 附加价值BFF集成与解耦
目前为止BFF的反爬任务就基本上完成了。但是我们创建BFF不仅仅是替后端做脏活累活的还需要有更多的价值。我们先从反爬的初心开始讲起。
### 后端保护
反爬虫的初心其实是防止后端收到大流量冲击。现在看来这反倒成了最低级别的需求了。不过我们还是不忘初心看下BFF是如何保护后端的。
我们知道,真正能冲击后端的爬虫,其实反倒不是那些竞对爬虫,而是毕业生爬虫,他们在毕业季拼命爬取数据, 用来做毕业设计,写论文。而且他们没什么技巧,就是死命干。所以简单的规则封杀就可以操作掉。
这类封杀, 如果在SLB层不方便做在BFF层可以轻易实现本质就是一个规则过滤——而我们已经有规则引擎了。因此触发规则之后直接设置state为结束流量就会被忽略掉。后端压力也就得到了降低。 而BFF本身也不用担心因为在选型时选择了Node.js应对高并发是天赋受到的冲击极为有限。
当然有些公司的BFF我们提到过不是Node.js……因为一些特殊原因……这种还是死磕SLB去做规则吧。
### 前端熔断
除了保护后端BFF也可以轻易做到前端的熔断。
我们的反爬验证无非就是key的生成和key的验证那么最彻底的熔断当然是不生成key也不验证key。但是有时候线上问题紧急多个开关操作起来会互相干扰。
所以有时候也可以只操作验证key开关简单的说就是无论key错得多离谱我们的规则引擎都默认设置为通过。
我们前面提到过随机放过的操作可以在集群直接操作。实际上你把概率设置为100%,所有集群都放过,那么也就等于熔断了。快速熔断,关键时刻可以救命,一定要做好。
### 超时处理
超时处理与熔断其实差不多,那么具体的实现逻辑是怎样的呢?
首先生成key的时候不能让用户等太久。如果超时了那么就可以迅速生成一个指定的key让他先走掉。固定key很难通过验证的因此这个报警一定要快因为误伤可能已经产生了。由于我们的误伤操作通常是操作价格并不是拦截因此会给予你一定的时间来处理。
此外验证key也需要做超时处理。假设一个key验证较长时间还没验证完毕那么不管哪里逻辑有问题规则引擎直接给出“验证通过”的结果即可。一定要记住反爬是一个非关键的业务抓不到爬虫不可耻引发生产问题才可耻。
此外还可以设置一些自动熔断的规则。注意,如果这些规则被爬虫抓到,他可能有意触发。所以,还是那句话:具体问题,具体分析。这个需要权衡以及尽可能隐蔽,不要被抓到规律。
## 小结
好了,最后我来给你总结一下。
今天这一讲我们了解了BFF在反爬虫动作中的一系列问题。从选型开始到实现反爬虫的详细动作以及实现反爬虫功能的详细方式都做了详细的讲解。最后我们也补充了BFF在集成和解耦上面的附加价值。详细的内容我也给你准备了一张脑图你可以对照着来复习。
![](https://static001.geekbang.org/resource/image/a2/e4/a258e8c44917ed0e92f83acce8fee1e4.jpg?wh=4500x2317)
但是这里我更想和你强调几个这一讲的重点也是BFF在反爬虫这件事上最关键的几点
1. **自定义Engine**BFF通过自定义Engine来实现key的生成以及验证
2. **通过集群设置随机值:**使用weight定义机器权重实现加权分流并能过控制机器开关实现随机
3. **BFF小技巧**通过自定义随机值来设置熔断、通过设置不验证实现熔断、通过超时处理来提升用户体验。
那么,到这里为止,我们的理论课就告一段落。下一讲,我会带你从头到尾走一遍反爬虫的过程,认真观察这个战场的每个角落。
## 思考题
又到了愉快的思考题时间。 老规矩,你可以任选一个问题和我讨论。
1. 你们的公司出于各种原因BFF没有选择Node.js技术栈。那么你是硬着头皮在这种技术栈做反爬呢还是想办法让BFF转Node.js呢如果你认为不可能成功那么理由是什么呢
2. Express的官方自定义Engine的demo里面会频繁读文件。虽然Node.js是异步的但是也顶不住没完没了的读硬盘啊。如何提升性能呢
3. 集群实现随机你精心配置终于实现了79.4%。然后机器扩容,随机值又变了。那么,如何处理这个问题呢?
期待你在评论区的分享,我会及时回复你。反爬无定式,我们一起探索。
![](https://static001.geekbang.org/resource/image/70/dc/706cf2dcc52858d483e569ab5137d8dc.jpg?wh=1500x1615)