最近由于是特殊时期,这个寒假无比漫长。在开学前夕,许多学校要求学生每天打卡自己的健康情况。
一开始的时候,我是用selenium驱动浏览器帮助自己填写每天的健康状况,并做了一套web程序,可供学校的其它同学使用。一想到有可能会被举报,于是就没有部署到自己的服务器上面供同学们使用,于是将它开源了出来,并写一下博客纪念一下。
后来学校健康打卡网站又双叒叕改版了,全新的界面,只能提交一次。好在这次post提交的结构体比较简单,api不是很复杂,考虑不用selenium来做自动化。原版的健康打卡提交的post结构体很复杂,有点难搞定,于是使用selenium驱动浏览器来自动填写,这种方式的优点就是简单,但是存在很多缺点。驱动浏览器打开和关闭的过程需要耗费一定的时间,而且有时候容易崩溃,当你需要大量的操作时,效率很低。而模拟http请求发送post数据包的方式就非常精简,但是需要你去探索服务端的API,需要一定的技术功底,包括前端和后端的技术。
接下来进入正题:
这次的打卡登录页面比较简单,只有账号密码登录,我必须先通过登录之后才会给我填写健康信息的界面,否则直接访问信息填写界面会被重定向到登录界面。
F12打开chrome的开发者工具进行调试,输入账号密码之后提交,发现客户端只是post一个非常简单的json,只有username和password两个key,username就是我的学号,但是password进行了加密。
吐槽一下这个网站的设计,没有用到cookie,你随时可以重新登录,只要打开登录的url即可,输入账号密码,判断一下今天是否填过表,如果填过了,就给你重定向到填写完成的页面。如果没有,就重定向到填写的页面。而如果你新开一个标签并打开填写页面,就又回到了登录页面,没有识别cookie来判断你的身份的过程。而你在填写完成的页面的标签上面的链接中输入填写的页面,就给你打开填写页面了。因为这个页面其实是有效的,只不过它的程序判断你填过了就不给你重定向到那个页面。
这里需要先研究一下其前端是如何加密的。
点击Chrome开发者工具的Sources标签,左边是这个网站所有的文件,加密的代码就在js文件或html的script标签中,只需要研究这些文件就好了。
我们需要监听登录按钮绑定的click事件,当点击登录后,开始断点调试点击按钮之后执行的代码。我们在Source标签下选择右边面板中的Event Listener Breakpoints,里面选择Mouse,展开选择click。这样浏览器就会监听所有的click事件。在输入账号密码之后进行设置(否则点击输入框会触发click事件)。
点击登录按钮开始调试,我们要找到点击登录按钮之后发生的加密过程。不过你一直单步调试是点不完的。一般还是根据经验来找到你想要的代码片段。
这里讲一个合理的思路:
先找到post用户名密码的代码片段,这里已知点击登录之后会post一个含有username和password的json给服务端,我们在代码文件中搜索username,并找到提交用户名密码的代码段。
基本可以确定是这个函数触发了post请求,当然代码里面不止一个类似的post请求函数,如果不确定是否为这个函数,可以在这个函数里面set一个断点,然后像上面说的一样监听登录按钮的click事件,调试的时候看看会不会在这个函数内的断点内停下,如果在断点处停下了,说明就是这个函数。
接下来,我们要找一下login1函数在哪里被调用,逐步往上找,知道找到密码加密过程。同理,js文件中有多处地方用到了login1函数,可以通过设置断点调试的方式确定login1函数在哪里被调用。
经过分析,可以确定的是,login1函数就是在上图中的黄色高亮部分被调用,而其所在行前两行就是对密码的加密。
这里可能看起来有点迷惑,为什么前端开发的代码命名这么乱七八糟,用t代表密码,而且怎么好像两个login1函数声明一样,不会冲突吗。这个,就不要管那么多啦,因为,这些代码根本不是写出来的,而是通过一些打包工具进行了转换。从这些js文件的文件名都有个bundle可以大概猜出来,应该是用webpack/browserify之类的工具打包的。比如用browserify工具能够让npm包能够在浏览器代码中被使用,而且将代码格式化成一行,变量名采用一个字母的形式…..,使得代码文件的字符变少,提高传输效率,但是没有可读性。
可以知道,前两行中的this.encrypt对密码进行了加密,并保存在了t变量中,我们转到encrypt代码看一下
了解过密码学的话可以推断这里代码使用AES加密算法,变量CRYPTOJSKEY是密钥。
如果使用过nodejs的crypto-js包的话,可以看出这里的函数是使用了crypto-js包中的函数,这个模块包含了许多常用的加密,解密算法可以调用。看到这里的时候我是认为,开发人员不可能自己去实现这个加密算法得啦,估计就是找一下有没有现成的包然后调用。于是我用搜索引擎搜一下Lo.enc.Utf8.parse这个函数,就知道这其实是调用了crypto-js包。参考这篇文章:https://blog.csdn.net/Rice_kil/article/details/103685034。这里的代码用变量Lo来表示crypto-js库的命名空间,估计是代码打包工具干的。
我们还要了解一下这个密钥是哪里来的,搜索变量CRYPTOJSKEY,可以很容易找到
ok,到这一步,我们就了解了密码的加密过程,可以用爬虫模拟登陆过程了。
也可以用postman来模拟测试。返回的json中的jnuid需要记下来,后续填表提交数据的时候需要用到,用来作为身份验证的数据。
OK,我们在表格填写的页面先把信息填好,F12打开开发者工具,然后对数据进行提交,在开发者工具的Network标签中找到提交的post数据包,检查一下post请求的数据的body中都有哪些内容。
我们复制数据包的内容并在VsCode中打开,可以看一下这个数据包有哪些字段。然后用Postman伪造数据包发送给服务端,看看能否得到相同的响应。这个数据包中的mainTable字段就是上面填写表格的内容,还有一个jnuid字段,就是我们登陆成功后返回的身份标识,只需要这两个字段就可以提交数据给到服务端。这里服务端的身份验证设计得比较简单,没有用到cookie,只通过jnuid字段来验证身份。
我们打开Postman,尝试模拟数据包发送给服务端,header中只加了一个Content-Type: application/json,然后将数据包得json内容复制到Body中,发送出去。
最终我们收到了跟浏览器一样得响应,填表成功。
OK,现在我们可以尝试设计自动程序,每天帮自己填表打卡,再也不用每天忍受学校的龟速服务器。
在设计程序时需要用正则表达式捕获网页中的登陆密钥,因为后来发现这个密钥隔一段时间就会变一次。
每天程序填写完表格后,需要将填写完成后的响应信息通过邮件的形式发送给自己查看,因为有可能会填写失败,比如后台api改了。