背景
去年在公司写过一个爬虫工具,用于抓取自动化报告通过率、自动发送报告。由于当时是第一次接触爬虫,难免会遇到各种问题,解决方案全都是按照网上的一些爬虫文章示例,照猫画虎写的。虽然能正常使用,但其实很多地方都没弄明白。最近学习了一些前端和后台的原理,了解了cookie与session的机制,总算弄明白了爬虫登录过程中的一个疑问。
用户登录请求中的authenticity_token
编写爬虫第一步,在登录公司的自动化平台时就遇到了一个难题,登录请求中必须包含一个authenticity_token字段。令人头大的是,完全不知道这个字段从何而来,而且该字段还每次都不一样,参考的爬虫登录示例也没教啊!真是急坏苯宝宝了😭
后来翻了好多CSDN的爬虫贴,了解到知乎的登录请求中也包含这样一个字段,而作者的处理方式就是先访问一次登录页,然后从登录页中查找一个隐藏的authenticity_token字段。
借助F12发现,公司的自动化平台登录页中也包含了这样一个隐藏字段,试之,果然成功了......
#登录源码:
def login(login_url = 'http://****.com/users/sign_in', username, password):
#请求头
my_headers = {
'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36',
'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding' : 'gzip',
'Accept-Language' : 'zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4'
}
#获取token
sss = requests.Session()
r = sss.get(login_url, headers = my_headers)
reg = r'<input name="authenticity_token" type="hidden" value="(.*)" />'
pattern = re.compile(reg)
result = pattern.findall(r.content)
token = result[0]
#postdata
my_data = {
'commit' : '登录',
'utf8' : '%E2%9C%93',
'authenticity_token' : token,
'user[email]': username,
'user[password]':password
}
#登录后
r = sss.post(login_url, headers = my_headers, data = my_data)
return sss
"多年后的一个平静的下午,当我无意间浏览了一片CSRF攻击的帖子,突然眼前一亮......老衲终于明白了这个authenticity_token的含义了!!!终于彻底理解了当年困扰我两小时的难题了!!!"
其实,该token的作用就是防御CSRF攻击,关于什么是CSRF,还得先了解下Session id。
关于Session id的机制
HTTP请求的一大特点就是无状态,这也就导致服务端无法区分请求来自哪个客户端。为了记录每个用户的状态,跟踪用户的整个会话,web程序普遍采用了cookie与session技术。(由于cookie与session的内容过多,在此不表,详细原理可以参考一片文章:Cookie与Session机制)
关于cookie与session,最需要了解的几点是:
- session机制运行依赖于session id,用于服务端跟踪每个会话,而session id存在于本地的cookie当中;
- session id会随浏览器进程关闭的关闭而清除,也就表示一次完整的会话结束了。当下次再次访问该网站还需要登录,重新建立一个会话;
- 现在绝大多数浏览器都支持子窗体,子窗体能共享父窗体的session id,而另起的浏览器进程无法访问该session。这也是为什么当我们在某网站登录后,在新的页签下打开该网站依然是登录状态,而另起一个浏览器进程访问却是非登录状态。
根据session机制以上特点,就引申出了一个问题:CSRF攻击。
什么是跨站请求伪造(CSRF)攻击?
用户每次点击一个链接、提交一个表单,其本质就是对服务端发起一次请求。而CSRF攻击的原理就是:攻击者诱导用户点击一个链接,用户在不知情的情况下提交了一次表单请求。而表单的内容则是攻击者事先准备好的。
简单举个栗子🌰:
- 用户小明登录了论坛A,同时也打开了一个危险网站B(同一个浏览器中);
- 网站B上有一个链接,该链接的实质内容是针对论坛A的一个发帖请求(比如广告贴)。
- 小明处于好奇点击了该链接,造成的结果就是:小明在完全不知情的情况下在论坛A成功发表了一篇帖子。
备注: 以上攻击成功实施的关键在于,小明已经登录论坛A,并且点击跳转后的浏览器子窗体是可以访问父窗体的session id的。
假如小明复制该链接,然后手动打开一个新的浏览器粘贴访问该链接,则会提示用户处于非登录状态,该发帖请求会被拒绝。原因是新打开的浏览器无法获取前一个浏览器中的session id,服务端会将该请求当成一个新的会话,需要重新登录后才能成功执行发帖请求。
CRSF攻击防御
既然大家都了解CRSF攻击,自然有相应的防御措施,其中比较常用的就是采用token验证。
工作机制就是:用户在发送表单时还需要携带一个token值。该token一般是填写表单页中的一个隐藏字段,每次访问都不同。通过该token的验证,服务端就能知道用户的表单请求是否从表单填写页面跳转而来了。
简单举例:
- 当小明主动发帖时,必定要先点击发帖编辑页面A,当填写完帖子内容后再点击【发帖】按钮。此时会将小明填写的表单内容连带页面A中隐藏的一个token发送给服务端。服务端验证token通过后才表示发帖成功。
- 当危险网站诱导小明点击危险链接时,由于该链接实质就是一个发帖的post请求,跳过了访问发帖编辑页面A的过程,自然也就无法获取有效token,最终服务端会认为该发帖请求不合法。
简单来说,服务端每次通过请求数据中的token来验证表单请求是否由用户主动发送的,从而有效防御了CRSF攻击。
至此,也就明白了为什么登录页面时需要携带一个authenticity_token参数了,同时也理解了为什么需要访问登录页面获取该token。😄