让我大吃一堑的前后分离 web 站模拟登录

阅读本文大概需要 11 分钟。

一、背景

scrapy 模拟登录相信大家都会,而且非常的熟练。但是技术一直在进步(尤其是前端领域),近几年前后端分离的趋势越来越明显,很多 web 站都采用前后端分离的技术。以前保存用户身份信息靠 Cookie,那前后分离这种技术组合靠什么校验用户身份呢?

二、登录操作

前后端分离的项目,一般都是 react、vue 等 js 库编写的,进而涌现出了一批优秀的前端框架或组件,如阿里巴巴前端团队的 AntDesign,饿了么前端团队的 ElementUI 等。由于前后端分离的原因,后端必定有 API,所以最好的爬取策略不是在页面使用 CSS 定位或者 Xpath 定位,而是观察网络请求记录,找到 api 以及请求时发送的参数并用 Python 进行构造、模拟请求。

输入图片说明

以这里的登录为例,通过css定位其实也可以,但是有不稳定的风险。所以还是看api和参数比较稳妥,前端变化的几率比后端高出太多。在页面中打开调试工具,然后定位到『网络』选项卡,接着打开登录页并输入用户名密码并登录。

需要打码的纯洁登录框

在请求记录中找到并选中方法为 post 的那条记录就可以查看此请求的详细信息,比如请求地址、请求头和参数。请求详情如下图所示:

需要打码的纯洁登录请求详情

请求参数如下图所示:

需要打码的纯洁登录请求参数

可以看到请求参数中有用户名、密码以及用户名类型(比如手机号或邮箱)。得到完整的请求信息后就可以根据请求地址、请求头和参数来构造登录用的代码,Scrapy 常用登录代码如下:

    def start_requests(self):
        """ 重载start_requests方法 通过is_login方法判断是否成功登录 """
        login_url = "http://xxx.yyy.ccc.aa/api/v1/oauth/login"
        login_data = {
            "username": "abcd@easub.com",
            "password": "faabbccddeeffggd5",
            "type": "email"
        }

        return [scrapy.FormRequest(url=login_url, formdata=login_data, callback=self.is_login)]

    def is_login(self, response):
        """
        根据返回值中的message值来判断是否登录成功
            如果登录成功则对数据传输页发起请求,并将结果回传给parse方法
            如果登录失败则提示
        由于后面的用户权限验证需要用到token信息,所以这里取到登录后返回的token并传递给下一个方法
        """
        results = json.loads(response.text)
        if results['message'] == "succeed":
            urls = 'http://xxx.yyy.ccc.aa'
            access_token = results['data']['access_token']
            print("登录成功,开始调用方法")
            yield Request(url=urls, callback=self.parse, meta={"access_token": access_token})
        else:
            print("登录失败,请重新检查")

如果返回信息的 json 里面 message 值为 succeed 即认为登录成功并调用 parse 方法。

三、用户权限验证

登录完毕后想执行其他的操作,比如上传(post)数据的话,我应该怎么做?

首先要跟刚才一样,需要通过真实操作观察请求记录中对应记录的请求详情,根据 api 的地址和所需参数请求头等信息用代码进行构造,模拟真实的网络请求发送场景。下图为提交表单的请求详情信息:

纯洁的网络请求详情

跟上面类似,根据返回的参数和请求头构造代码,结果会如何?

结果返回的状态码是 401,由于 scrapy 默认只处理 2xx 和 3xx 状态的请求、4开头和5开头的都不处理,但是我们又需要观察401状态返回的内容,这怎么办呢?

我们可以在settings.py中空白处新增代码:

""" 状态码处理 """
HTTPERROR_ALLOWED_CODES = [400, 401]

然后在下一个方法中观察response回来的数据(这个地方当时作为萌新的我是懵逼的,所以委屈各位读者大佬跟我一起懵逼)。

后来查询了401的意思:未获得授权,也就是用户权限验证不通过。经过多方资料查找,发现请求头中有这么一条:

输入图片说明

它就是用于用户权限验证的,authorization 的值分为两部分 type 和 credentials 。前者是验证采用的类型,后者是具体的参数值。这里的类型可以看到用的是 Bearer 类型。我又去观察登录时候的返回值,发现登录成功后的返回值除了 succeed 之外,还有其他的一些返回值,里面包括了一个叫 access_token 的字段,看样子它是 JWT 登录方式用来鉴权的 token 信息,经过比对确认 authorization 用的也正好就是这个 token 作为值。

那么代码就应该在第一次登录时候,取出access_token的值,并传递下去,用于后面请求的鉴权,所以代码改为:

    def is_login(self, response):
        """
        根据返回值中的message值来判断是否登录成功
            如果登录成功则对数据传输页发起请求,并将结果回传给parse方法
            如果登录失败则提示
        由于后面的用户权限验证需要用到token信息,所以这里取到登录后返回的token并传递给下一个方法
        """
        results = json.loads(response.text)
        if results['message'] == "succeed":
            urls = 'http://xxx.yyy.ccc.aa'
            access_token = results['data']['access_token']
            print("登录成功,开始调用方法")
            yield Request(url=urls, callback=self.parse, meta={"access_token": access_token})
        else:
            print("登录失败,请重新检查")

下面的pase方法中,将 authorization 设定到 header 中以对数据进行请求:

header = {
            "authorization": "Bearer " + access_token
        }

这样就解决了用户权限的问题,不再出现401

四、postman发送请求特殊格式数据(json)

在 parse 方法中根据浏览器观察到的参数进行构造:

datas = {
                "url": "https://www.youtube.com/watch?v=eWeACm7v01Y",
                "title": "看上去可爱其实很笨的狗#动物萌宠#",
                "share_text": "看上去可爱其实很笨的狗#动物萌宠#[doge]",
                "categories": {'0': '00e2e120-37fd-47a8-a96b-c6fec7eb563d'}
        }

由于categories里面是个数组,所以在构造的时候也可以直接写数据,然后用 scrapy.Formdata 来进行 post。发现返回的状态是这次是 400,并且提示:categories 必须是数组。

再次观察请求头信息,发现请求头信息中还有:

依然纯洁到要打码

我将这个叫做 content-type 的字段和参数加入到 header 中:

        header = {
            "authorization": "Bearer " + access_token,
            "content-type": "application/json",
        }

这样关于 categories 必须是数组的提示就没有了。

但是返回的状态码依然是 400,而且提示变成了 "url不能为空"。

这到底又是怎么一回事?

多方探查都没有结果。

真是伤心

后来我又想起了,既然这里的文本类型 是 application/json,那么提交出去的文本应该是 json 类型数据,而不是 python 的 dict 字典。

于是打开 json 在线解析,对传递的参数进行观察,发现这样的数据并不满足json 格式:

输入图片说明

后来尝试对它进行更改:

输入图片说明

在外层增加了一对{},然后又将 categories 的值加上了双引号,才是正确的 json 格式(我是真的又菜又蠢)。

将这样的数据拿到 postman 中进行测试,发现是不行的。又经过我不断的测试,最终确定了 postman 的请求格式为:

输入图片说明

输入图片说明

输入图片说明

我是对 Auth、Headers 和 Raw 进行设置(请跟我一起懵逼),才终于成功发送 post,返回正确的信息!!!

五、Scrapy 发送 Json 格式数据

在 postman 测试通过后,说明这样的做法是可行的,但是代码上怎么编写呢?

用之前的 scrapy.Formdata 是不行的,它的 formdat= 默认使用 dict 格式,如果强行转成 json 格式也是会报错的。

经过群里咨询和搜索,发现要用 scrapy.http 的 Requst 方法(平时经常用的这个):

access_token = response.meta['access_token']
        urls = "http://aaa.bbb.xxx.yy/api/v1/material/extract"
        datas = {
                "url": "https://www.youtube.com/watch?v=eWeACm7v01Y",
                "title": "看上去可爱其实很笨的狗#动物萌宠#",
                "share_text": "看上去可爱其实很笨的狗#动物萌宠#[doge]",
                "categories": {'0': '00e2e120-37fd-47a8-a96b-c6fec7eb563d'}
        }
        header = {
            "authorization": "Bearer " + access_token,
            "content-type": "application/json",
        }
        yield Request(url=urls, method='POST', body=json.dumps(datas), headers=header, callback=self.parse_details)

这样发送请求,终于成功了!!!

为什么成功了?

首先看一看 json.dumps 函数的用途是什么: json.dumps() 用于将 dict 类型的数据转成 str。

虽然没有摸清楚消息发送失败的根本原因(有可能是目标网站后端对数据格式进行校验,也有可能是 Scrapy 框架会在发送请求前对参数进行处理所以导致的问题),但是已经可以猜出个大概。同时也在本次爬虫任务中学习到了一些知识。

从本文中我们学会了三个知识: 第 1 是萌新要多问、多测试,没有解决不了的计算机问题; 第 2 是爬取使用前后端分离技术的 Web 站时应该优先选择从 API 下手; 第 3 是网络请求详情中看到的参数格式并非是你认为的参数格式,它有可能是经过编码的字符串;

原文发布于微信公众号 - 进击的Coder(FightingCoder)

原文发表时间:2018-12-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券