最近开学后相信很多同学都发现课表小程序等无法使用了,这是因为学校更新了正方教务系统,导致了大批基于旧版正方系统的校园小程序无法继续爬取课表。

之前曾在GitHub上看到过本校师兄开发的school-api–一个基于旧版正方的python SDK,但新版无法使用。因此花了两天时间研究了下新版正方的登录(能登录后续的就EZ啦~)

既然都弄了,因此计划开发一个新的SDK。我比较懒 暂命名为new-school-sdk,项目目前还在开发中,先将登录的流程、验证码识别的思路罗列出来。(拿到了cookies 还有啥不能干嘛)

项目Github地址: https://github.com/Farmer-chong/new-school-sdk

登录前期准备

通过观察发现,有以下几个难点:

  1. 新版正方使用Java进行开发且并非前后端分离,因此只要我们能拿到cookie即能完成登录。
  2. 验证码的识别
  3. 正方使用了Rsa对数据进行加密

针对上述问题,开始一一解决

验证码部分

前置工作准备好后,开始从服务器获取验证码并进行验证

获取验证码

网络抓包发现,验证码是异步获取的,每次刷新都会发送一个请求到/zfcaptchaLogin

请求报文内容有:

1
2
3
4
"type": "refresh"
"rtk": "56f88546-d402-4afd-88b5-82a203258da8"
"time": "1630645045207"
"instanceId": "zfcaptchaLogin"

响应报文内容:

1
2
3
4
5
6
7
imtk: "29730cb5-d7ff-4fc9-aa9d-e3efc0a07f55"
mi: "a8f191af-f267-4681-881a-54558298db09.png"
msg: ""
si: "1ccd27c6-6208-41f9-adfb-13beafa6d954.png"
status: "success"
t: 1630645937179
vs: "not_verify"

观察请求报文发现需要typertktimeinstanceId这几个字段。

其中rtk未知,因此开始寻找其出现的地方。通过查找发现rtk出现在一个js文件中,初步猜测rtk是一个令牌,由服务器随机生成的。
因此我们要先获取rtk令牌,然后利用正则表达式将其值提取出来。

下载验证图片

但现在仍然无法获取验证图片的原始数据,再观察imgsrc属性,得知响应报文中的misi分别别是验证码滑块。并且需要的url参数我们也已经获取了。

/zfcaptchaLogin发送一个GET请求,请求参数如下:

1
2
3
4
5
type: image
id: 上一步响应体中的si
imtk: 上一步响应体中的imtk
t: 时间戳
instanceId: zfcaptchaLogin

滑动验证码识别

上文中有提到,参考这篇文章: https://blog.dairoot.cn/2021/06/26/zf-sliding-captcha/

大致流程如下:

  1. 将图片灰度化
  2. 识别出一段颜色差小于阈值的线(竖的)
  3. 识别出来的这段线不能太短(要和缺口差不多高)

因此即可计算出该线的x轴坐标,因此得到滑块偏移量。

模拟人手拖动&发起验证请求

从上一步中,我们得到了偏移量XY,接下来就要开始模拟人手拖动滑块的过程了。人手滑动验证码时,一般都是先快后慢的一个速度曲线,因此利用物理学公式分段设置加速度a,前半段a > 0,后半段a < 0
当前速度用v表示,初速度用v0,位移用x,时间用t,它们之间满足如下关系:
x = v0 * t + 0.5 * a * t^2
v = v0 + a * t
移动算法的代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def _get_track(self, distance, y):
"""模拟人手滑动
通过设置前快后慢的加速度,模拟人手滑动
Args:
distance ([int]): [移动距离]
y ([int]): [滑块Y值]
Returns:
[list]: [坐标数组]
"""

start = 1200
current = 0
track = []
# 减速阈值
mid = distance * 4 / 5
# 计算间隔
t = 0.2
# 初速度
v = 0
while current < distance:
# 加速->加速度为 2; 减速->加速度为-3
a = 2 if current < mid else -3
v0 = v
# 当前速度 v = v0 + at
v = v0 + a * t
# 移动距离 x = v0t + 1/2 * a * t^2
move = v0 * t + 1 / 2 * a * t * t
# 当前位移量
current += move
# 加入轨迹
track.append({"x": start + int(current), "y": y, "t": int(time.time() * 1000)})
time.sleep(0.01)
return track

至此,我们得到了发起请求的所有数据,因此向/zfcaptchaLogin发送一个POST请求,请求体如下:

1
2
3
4
5
6
type: verify
rtk: 56f88546-d402-4afd-88b5-82a203258da8
time: 1630650071446
mt: 将模拟滑动的内容通过base64编码
instanceId: zfcaptchaLogin
extend: 将UA进行编码

当验证通过时,得到如下的响应体:

1
2
3
msg: ""
status: "success"
vs: "verified"

登录部分

获取RSA公钥

通过查看页面源码和点击登录后抓包,登录发送一个请求到/xtgl/login_slogin.html,然后返回一个302的跳转。
其中请求报文内容如下:

1
2
3
4
5
csrftoken: csrftoken
language: zh_CN
yhm: 登录账号
mm: 加密后的密码
mm: 加密后的密码

此处csrftokenmm两个字段是未知的。其中csrf令牌是为了防止攻击的,一般包含在form表单中,由后端生成。因此我们可以直接从页面中提取。如下图:

mm字段,通过对前端异步请求部分的代码进行分析后,发现是利用RSA进行加密,从抓包中可以发现一个发送到/login_getPublicKey.html地址的GET请求。其响应体内容如下:

1
2
exponent: "AQAB"
modulus: "AIdzVtHXJLlh5vOlWFiRnWHc1xaChgqY1u4LNpaMjVUByVHwdvMMmlw4np8u/B3esIS2hsdQ7nRkrzWYYbkTWo8bm2LGS0H3/h1GVjLWaMrn1uj6lMYz0Y0O0AMUc19y23XRnSM7Q/d9V7tk6oS1HwyUKJwA7aSTgyenhNj26XrL"

因此得到了RSA指数,但这里的modulus长度为 172。大概率是正方修改过加密,在JavaScript文件的注释中也可以看到。
本来是打算自己重写一个python版的实现,后来在GitHub上发现已有前人栽树,我乘凉就好啦!

PyRsa仓库: https://github.com/hibiscustoyou/pyrsa

到现在为止,整个登录流程的未知项就全解决了!🛫🍯

开始登录

再次观察数据包的流程,得知登录各项的顺序并做优化:

! 注意,在登录发生302跳转的时候,cookie会发生改变 !

  1. 访问登录页面,获取csrf和原始的cookies
  2. 获取rsa公钥
  3. 处理滑块验证
  4. 发起登录,得到登录后的cookie

成功截图:

后记

sdk开发中,希望大佬们多多给意见或者一起开发哈!