背景
回想高中的时候,我们有兴趣课程(官方叫校本课程),选课期间也会非常火爆,抢课成为了每学期的一大热门,尽管教务一再崩溃,也无法阻挡我们为了心中向往的课而不断刷新的脚步。
于是我为这个写过抢课系统,甚至发现了教务系统可以提前选课的漏洞。就在选课开始的前一夜,我们已经有很多同学因此受益选上了课程,不过老师好像发现了这一点也在不断删除。但晚上估计老师也下班了,仍然有很多同学胜利提前选课。
这是我一直引以为傲的历史,但是抢课软件已经被我封存,不适用于其他年级的同学,我已经毕业,也无法更新。
如今,北师大抢课的盛况,让我感到十分亲切。已经抢过快两个年头,实在不忍心像大部分同学一样课也罢了去挤破头皮选课;而且选课系统还不兼容现代浏览器,非逼我开Windows虚拟机用IE:干脆写一个助手。
之前已经多次探索过教务系统的网页构造,后端应该是Java Struts架构,前端写的也很复杂。毕竟是老的业务系统了。在之前,我成功的实现教务网“逾期评教”,也是迈出了深入教务网的第一步。
寒假,学校信息网络中心把教务系统也纳入统一身份认证,这登录也就无需验证码了,更方便写助手。
起步
一、登录统一身份认证。
URL是:http://cas.bnu.edu.cn/cas/login?service=跳转目标链接 (这里跳转目标以教务系统为例)
表单乍一看就用户名和密码两个字段,十分简单,想当然HTTP POST一下就万事大吉了。不过,我看学校这么成熟的老业务架构,估计没这么简单。于是打开F12,监控一下网络。
code应该是验证码,因为当我多次提交的时候,验证码就会出现。因此在初次提交,code=code即可。
重点就是lt和execution这两个参数了,我尝试过POST时任意填写这两个参数或者复制固定的内容作为参数,均不能登陆成功。
看来这两个参数应该与当前session挂钩,之所以这么说,是因为我每次打开隐身浏览模式(无初始cookie)的时候,第一次进入登录页面,execution一定为’e1s1’,而code一直是随机变化。
想必我必须先GET登录页面,提取出来这两个参数,再在POST的时候附加上才能成功登陆;同时也必须记录cookie。
事实证明是这样。为了方便记录cookie,我使用requests库进行HTTP操作。
二、获取课程列表
我先从“按开课计划选课”中入手。现在做教务助手最大的困难就是,目前不是选课时间。也就是说,我进入教务系统,他不会显示出来列表的。这样我也就无从下手进行调试。
好在,调试中,发现它会先AJAX请求服务器是否在选课时间。服务器返回JSON数据,包含message和status。客户端JavaScript会对返回数据中status进行判断,如果是200则是选课时间,继续向服务器请求课程列表。
我设置断点,修改status=200,结果成功的获取了课程列表。
能在浏览器中获得课程列表,我也就可以在F12中看到它AJAX请求的目标。顺理成章地也就实现了。不过服务器好像有时候会检查HTTP请求的headers中的Referer属性,所以一开始直接将其设置为http://zyfw.bnu.edu.cn即可。
另外POST获取课程列表时,需要提供几个参数xq、xn等,这些需要通过POST另一个目标获得。均可以从客户端JavaScript以及F12网络监视中获得目标。
公选课的课程列表也同理可以获得。
三、退选课程
首先,获取退选列表,与获取课程列表是异曲同工的。关键在于如何退选。
还是之前的套路,先想法子在浏览器这里实现,然后通过监视它的HTTP的请求来获得实现方法。
然而,获取的退选列表最后一列是“操作”,即“退选”。但目前不在退选时间,“退选”理论上应该是超链接的,此时均为普通文本。
这下就不知道调用的接口了。不过JavaScript不多,调试过之前那些页面的JavaScript,对它的套路也算摸得差不多。再加上他的JavaScript命名比较清晰,很快找到了退课的function。
仔细阅读了这个函数的代码,手动为“退选”加上了超链接,并挂接onclick事件,成功调用了退选function。然后看到F12的监视结果,得到了退选的POST目标和参数。
可是,POST的参数经过了加密处理,JavaScript端加密虽然可见,但若翻译成Python,也是不小的工程。它加密函数取名有des的,本以为是标准的des加密,经过痛苦的代码阅读,我利用pyDes算是实现了一遍教务系统JavaScript的加密过程。最终却因为JavaScript中的enc函数并非标准的des加密(也许是我不懂,反正没能调出相同结果),失败了。
翻译所有的加密代码好像成为了唯一的解决方案。可是我查到pyexecjs库,可以实现Python调用执行JavaScript函数。我把JavaScript代码复制下来,直接用Python调用它实现加密。最终证明可行。
不过,显而易见,退选是不成功的,因为不在退选时间。POST返回的数据也是JSON格式,告知不在退选时间。
但是我想到加密代码和POST的参数中出现了timestamp,即当前时间,如果我将其人为设定为退选期间,是否可以成功呢?
经过实验,返回的信息是“操作成功”,课真给退了?!吓得我赶紧刷新看了看,结果没有。看来还是不成功,可是提示信息的判断确实骗了过去。我不知道服务端是有一种怎样奇葩的判断存在。
四、选公选课
有了之前的工作,这个功能显得特别简单。因为也涉及参数加密,之前的解决方案仍然可以使用。
我再次尝试修改timestamp,结果返回的信息是“选课人数已满”,尽管那个课还有20多个空名额。我觉得这就更加奇葩了,真想看看服务端是怎么写的。
README
- 使用Python 3。
- 需要第三方库requests和pyexecjs。
特性 Features
- 登录统一身份认证系统
- 按开课计划查询课程
- 查询公选课
- 退选课程
- 选公选课
TODO
- 选开课计划课程
- 查询选课结果
- 查询考试安排
- 查询考试成绩
- 评教
- 设计和实现选课策略