小白教小白之签名验证反爬虫初探
> 文章没有多大的技术含量,只是尽量详细地列出步骤,给予想学但是没有入门的同学一个参考,毕竟天才/大牛也是从 1+1 开始的,所以叫“小白教小白”> 因为本人是第一次学,也只体验过文中列出的这一个案例,所以叫“初探”
> 最近在看一本反爬虫的书,记录一下学习内容。既为体验一遍完整的过程,加深记忆;也为分享知识,互相学习,共同进步
> 希望通过此文建立看书我会、我说你会的知识传播链
# 学情导入
- 目标网站:https://fanyi.youdao.com/
## 研究过程
- 在浏览器打开网站,页面如图所示:
[!(https://s1.ax1x.com/2022/05/22/OzWcd0.png)](https://imgtu.com/i/OzWcd0)
- 稍微使用一下,熟悉网站的逻辑:用户在左侧输入框中输入文字后,右侧会给出实时翻译结果。既然是实时结果,就代表它使用了异步请求的方式
- 我们可以在网络请求记录中找到对应的请求:打开开发者工具,切换到“网络”选项卡,在左侧输入内容,右侧就会得到相应的结果。可以看到,刚才产生了几条请求:
[!(https://s1.ax1x.com/2022/05/22/OzftX9.png)](https://imgtu.com/i/OzftX9)
- 根据经验,我们首先怀疑类型为 `xhr` 的请求,点开一看,果然是 `POST` 请求,此时已经基本确定无疑了。点开响应面板,果然看到了我们想看到的东西,此请求的响应内容中包含翻译结果:
[!(https://s1.ax1x.com/2022/05/23/XSumvt.png)](https://imgtu.com/i/XSumvt)
- 既然这样,那就把请求的网址、表单数据都复制过来,试一试:
```py
import requests
url = 'https://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule'
datavalue = {
"i": "编程",
"from": "AUTO",
"to": "AUTO",
"smartresult": "dict",
"client": "fanyideskweb",
"salt": "16532282502370",
"sign": "d4d53c136009fe38f4b465cf19fb6e5c",
"lts": "1653228250237",
"bv": "1744f6d1b31aab2b4895998c6078a934",
"doctype": "json",
"version": "2.1",
"keyfrom": "fanyi.web",
"action": "FY_BY_CLICKBUTTION"
}
r = requests.post(url, data=datavalue)
print(r.text)
```
- 然而,事实并不是这么美好,返回结果是:`{"errorCode":50}`,不是我们想要的 `{"errorCode":0,"translateResult":[[{"tgt":"programming","src":"编程"}]],"type":"zh-CHS2en","smartResult":{"entries":["","programme\r\n","programming\r\n"],"type":1}}`
- 这是怎么回事呢?因为此网站使用了验证签名的反爬虫手段,这就是今天要学的内容
> 尽管我在一本书上看到过,把请求网址中的 `_o` 删掉(即把网址换成 `https://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule`)也能成功得到结果,但是今天是要学习新知识的,假装不知道
# 概念了解
- 签名是根据数据源进行计算或加密的过程,签名的结果是一个具有唯一性和一致性的字符串
- 签名验证是防止恶意链接和数据被篡改的有效方式之一,也是目前后端 API 最常用的防护方式之一。与 cookie、user-agent、host 和 referer 等请求头域不同,用于签名验证的信息通常被放在请求正文中发送到服务器端
- 签名验证有很多种实现方式,但原理都是相同的:由客户端生成一些随机值和不可逆的 MD5 加密字符串,并在发起请求时将这些值发送给服务器端。服务器端使用相同的方式对随机值进行加密计算以及 MD5 加密,如果服务器端得到的 MD5 值与前端提交的 MD5 值相等,就代表是正常请求,否则返回 403
# 上手实验
- 在刚才复制 `POST` 请求的表单数据的时候,有很多参数。其中有一些以短小精悍著称,我们望而知意,对它们根本就产生不了一丝的兴趣,比如:`i, from, doctype` 等;还有一些奇奇怪怪的参数,成功引起了我们的注意,比如:`salt, sign, lts` 等
- 我们贯彻胡适先生“大胆假设,小心求证”的思想,在观察和猜测后,得出一些结论:
- salt, sign, lts, bv 这四个参数可能是随机生成的用于反爬虫的字符串
- sign, bv 的值都是长度为 32 位的随机字符串,应该是 MD5 加密后得到的值
- salt 和 lts 的值相似度很高,前者比后者多了 1 位数字。经过多次测试发现,lts 的值是用户在左侧输入文字后自动翻译时生成的 13 位时间戳;salt 比 lts 多出来的一位数在 0 到 9 中随机生成
- 使用不同的浏览器观察请求正文中的 bv 字段值,测试发现,使用相同浏览器发出请求时,bv 字段值是相同的,而使用不同浏览器发出请求时,bv 字段的值是不同的。这说明 bv 的值可以复用,并且它与 ua 或者浏览器版本信息有关
> 我们对 `salt, lts, bv` 这三个参数有了一些猜测,还剩下 `sign` 。既然这种反爬手段叫“签名验证”,那么叫 `sign` 的参数自然是最后出场,所占篇幅也是最多的
> 以下就要去 js 文件里找 sign 的生成规则了。虽然我只会 python ,但这就够了,看得多了明白的就多了,多碰壁就有了经验。所以只要 python 入门,应该都会
- sign 字段的值在每一次触发翻译操作时都会变化。在观察请求记录时,可以发现网页加载了名为 fanyi.min.js 的文件
> 接下来书里没有说怎么找,直接给出了关键代码。所以应该是根据经验找的吧。。。下面的是我根据答案推导做题步骤:
- 点开这个文件,发现好像只有一行,其实不完全是,因为这一行有 223230 列,有 223229 个字符。莫慌张,全选复制,找个网站格式化一下,把返回的内容复制到本地解释器里,发现有 8752 行
> 如果没用过的话,可以先用这个网站:https://tool.oschina.net/codeformat/js/
- ctrl+f 查找 `POST`,发现有 9 个,一个个的看。发现第 7847 行的这个似乎是我们要找的,因为 url 是我们请求的 url:
[!(https://s1.ax1x.com/2022/05/23/XSJKrd.png)](https://imgtu.com/i/XSJKrd)
- 第 7850 行表明这个 post 请求的 data 参数是函数 e 给出的,然后接下来就卡住了。。。但是我发现书中给的关键代码,是所有相似代码部分中 data 的参数最多的,即 7473 行的这个:
[!(https://s1.ax1x.com/2022/05/23/XSaMNR.png)](https://imgtu.com/i/XSaMNR)
- 根据这段代码,我们可以大胆猜测:请求正文中的字段和对应的值是由 JavaScript 代码生成,为了找到具体的代码,我们搜索关键字 sign 。然后书中就说:最终发现一个用于生成 `sign, bv, salt, lts` 的方法:
> 我不知道是怎么发现的,,,
[!(https://s1.ax1x.com/2022/05/23/XSwJTH.png)](https://imgtu.com/i/XSwJTH)
- 我们可以对代码进行如下分析:
- lts 的计算语句是`"" + (new Date).getTime()`,其作用是获取当前时间的时间戳
- bv 的计算语句是`n.md5(navigator.appVersion)`,其作用是获取用 MD5 加密的浏览器信息
- salt 的计算语句是`r + parseInt(10 * Math.random(), 10)`,其作用是将当前时间戳和 0~9 的随机数字组合成新的字符串
- sign 的计算语句是`n.md5("fanyideskweb" + e + i + "Ygy_4c=r#e#4EX^NUGUc5")`,其作用是获取组合字符串的消息摘要值(即 MD5 值)
> 书上就到此为止了,下面是我东拼西凑来的
- 在实操中,一开始找不到 sign 计算语句中的 e 是怎么来的,可能第一次学的同学也会卡在这里,说明一下:
- 回到开发者工具中,把代码格式化:
[!(https://s1.ax1x.com/2022/05/24/XPnGHP.png)](https://imgtu.com/i/XPnGHP)
- 找到这行代码的位置,单击左侧行号添加断点:
[!(https://s1.ax1x.com/2022/05/24/XPnRCF.png)](https://imgtu.com/i/XPnRCF)
- 保持开发者工具开启,刷新页面,输入翻译内容,网页自动给出翻译结果的时候,运行到断点会暂停,将鼠标移动到 e 的上方,会自动提示此时 e 的值,说明 e 是我们输入的内容:
[!(https://s1.ax1x.com/2022/05/24/XPuaa6.png)](https://imgtu.com/i/XPuaa6)
- 接下来只要使用 python 代码实现网站 fanyi.min.js 文件中的 JavaScript 代码逻辑:
```py
from time import time
from random import randint
from hashlib import md5
import requests
HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Edg/94.0.992.50',
'referer': 'https://fanyi.youdao.com/',
'cookie':'JSESSIONID=abca9UpKky8Ee-wQefdcy; OUTFOX_SEARCH_USER_ID_NCOO=223195183.89224002; _ga=GA1.2.917515912.1651731008; OUTFOX_SEARCH_USER_ID="-1666008210@10.110.96.157"; fanyi-ad-id=305838; fanyi-ad-closed=0; ___rl__test__cookies=1653377328195'
}
lts = str(int(time() * 1000))
salt = lts + str(randint(1,9))
e = "编程"
sign = md5(("fanyideskweb" + e + salt + "Ygy_4c=r#e#4EX^NUGUc5").encode('utf-8')).hexdigest()
url = 'https://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule'
datavalue = {
"i": "编程",
"from": "AUTO",
"to": "AUTO",
"smartresult": "dict",
"client": "fanyideskweb",
"salt": salt,
"sign": sign,
"lts": lts,
"bv": "1744f6d1b31aab2b4895998c6078a934",
"doctype": "json",
"version": "2.1",
"keyfrom": "fanyi.web",
"action": "FY_BY_REALTlME"
}
r = requests.post(url, data=datavalue, headers=HEADERS)
print(r.text)
```
> headers 中的三个参数必不可少。cookie 的有效期挺长的,我在网上复制了一个去年 6 月份发的文章的 cookie,都能用
- 运行后成功得到结果
- 不积硅步,无以至千里。学问积年而成,每日不自知 楼主说得很好,这个主要是到网页里面去断点,分析的过程,给个赞可以试试百度翻译,那个比这个稍微难搞些 还是明文开起来舒服,现在都在搞混淆js,看着累~(虽然有AST) 学习了,感谢分享 受教了,黑马里也有一个这样的栗子,我记得他是用js2py解决
页:
[1]