写在前面

已经过了一年半,网站有所改版,目前是可以正常登录的,获取信息的那几个url不是直接返回JSON了,需要自己去解析数据。


引入

与很多高校一样,西安皇家邮电大学一直钟爱于正方教务管理系统。

前一段时间爬了学校的旧版的教务系统,旧版主页

想了解更多请点击这里:爬取学校教务系统获取学生信息

不知道学校是不是把装空调的拿去买新教务系统了,新教务系统主页

可以看出,学校用的仍然是正方教务系统,不过版本升级了,新教务系统是用Java写的后台,前端用了Bootstrap框架。与旧系统相比,界面美观大方,易用性也明显提高。

准备

模拟登陆前,我们首先要清楚需要提交那些数据。

打开F12,我们在访问主页时,除了加载了JS的HTML还向服务器发送了一个get请求。

直接访问它可以获取一个json:

{"exponent":"AQAB","modulus":"AIMNm8zi5XkWLRDUy7w6bjypS+d8ng7an00UYH8UZMhUvrDAvpuifsFu+rU6dmMFQHpHGo9ZlyEy9GWy6ev3s8ro15869OIKo\/nwexEnb8AD0DO7NaV9jzDjqRjEldAs5ct9pdjo7SxQccJYfSbFbwb6206w1q4EAwlFDvTMJfKj"}

每次获取的内容都不一样,关于这个东西的作用下面再说。

点击登陆时,可以看到向服务器发送了一个POST请求,data域中包含如下数据:

csrftoken为了防止跨站域请求伪造
yhm为输入的用户名
mm并不是我们输入的密码

通过审查主页的元素,可以找到csrftoken(每次都不一样)

与此同时,发现结尾有很多JS文件

还记得我们表单中mm很奇怪吧,那是因为明文被加密过了,加密的方式是RSA,这些js文件就是完成了加密的操作。在login.js可以发现下面几个关键:

// 获取公钥
$.getJSON(_path+"/xtgl/login_getPublicKey.html?time="+new Date().getTime(),function(data){modulus = data["modulus"];exponent = data["exponent"];
});
......
// 创建公钥
var rsaKey = new RSAKey();
rsaKey.setPublic(b64tohex(modulus), b64tohex(exponent));
// 对密码加密
var enPassword = hex2b64(rsaKey.encrypt($("#mm").val()));
$("#mm").val(enPassword);
$("#hidMm").val(enPassword);

加密后的密码要转化为base64的形式填充到data域中。

在登录成功后,我们可以尝试去获取相关信息。通过分析,可以发现获取这些信息的URL和所需要的Data域:

获取学籍信息:

获取课表信息

获取成绩

这里面获取信息的请求返回值都是JSON,可以根据需要,把所需要的数据解析出来。

最后要注意,访问每一个请求别忘携带Cookies。

操作

明白了登录的原理,我们来梳理一下步骤:

  1. 获取csrftoken和Cookies
  2. 请求获取PublicKey
  3. 利用PublicKey对登录密码加密并用Base64编码
  4. 进行登录
  5. 获取所需要的信息

看起来很顺畅的思路,但我遇到了很大的问题,主要是在对密码加密的时候,Java与JavaScript在对数据进行RSA加密有些区别:

JavaScript在加密前对数据进行了随机填充,并用RSA/None/NoPadding的填充方式来加密,每一次得到的每一次结果都不同;Java在RSA加密时默认的填充方式为RSA_PKCS1_PADDING。据说可以在Java中用第三方包来实现NoPadding的填充方式,但是我在Java使用Bouncycastle提供的NoPadding填充方式初始化公钥不成功,提示我:

RSA modulus has a small prime factor

在Java中直接运行JS文件,简单的JS还可以,如果有的JS文件中会有navigator、window,javax.script.ScriptEngine是无法解析的。

最终选择用Java将JavaScript前端加密方式实现。

public class ConnectJWGL {private final String url = "http://www.zfjw.xupt.edu.cn";private Map<String,String> cookies = new HashMap<>();private String modulus;private String exponent;private String csrftoken;private Connection connection;private Connection.Response response;private Document document;private String stuNum;private String password;public ConnectJWGL(String stuNum,String password){this.stuNum = stuNum;this.password = password;}public void init() throws Exception{getCsrftoken();getRSApublickey();beginLogin();}// 获取csrftoken和Cookiesprivate void getCsrftoken(){try{connection = Jsoup.connect(url+ "/jwglxt/xtgl/login_slogin.html?language=zh_CN&_t="+new Date().getTime());connection.header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0");response = connection.timeout(5000).execute();cookies = response.cookies();document = Jsoup.parse(response.body());csrftoken = document.getElementById("csrftoken").val();}catch (Exception ex){ex.printStackTrace();}}// 获取公钥并加密密码private void getRSApublickey() throws Exception{connection = Jsoup.connect(url+ "/jwglxt/xtgl/login_getPublicKey.html?" +"time="+ new Date().getTime());connection.header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0");response = connection.cookies(cookies).ignoreContentType(true).timeout(5000).execute();JSONObject jsonObject = JSON.parseObject(response.body());modulus = jsonObject.getString("modulus");exponent = jsonObject.getString("exponent");password = RSAEncoder.RSAEncrypt(password, B64.b64tohex(modulus), B64.b64tohex(exponent));password = B64.hex2b64(password);}//登录public boolean beginLogin() throws Exception{connection = Jsoup.connect(url+ "/jwglxt/xtgl/login_slogin.html");connection.header("Content-Type","application/x-www-form-urlencoded;charset=utf-8");connection.header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0");connection.data("csrftoken",csrftoken);connection.data("yhm",stuNum);connection.data("mm",password);connection.data("mm",password);connection.cookies(cookies).ignoreContentType(true).method(Connection.Method.POST).execute();response = connection.execute();document = Jsoup.parse(response.body());if(document.getElementById("tips") == null){System.out.println("登陆成功");return true;}else{System.out.println(document.getElementById("tips").text());return false;}}
}

RSA加密

public class RSAEncoder {private static BigInteger n = null;private static BigInteger e = null;public static String RSAEncrypt(String pwd, String nStr, String eStr){n = new BigInteger(nStr,16);e = new BigInteger(eStr,16);BigInteger r = RSADoPublic(pkcs1pad2(pwd,(n.bitLength()+7)>>3));String sp = r.toString(16);if((sp.length()&1) != 0 )sp = "0" + sp;return sp;}private static BigInteger RSADoPublic(BigInteger x){return x.modPow(e, n);}private static BigInteger pkcs1pad2(String s, int n){if(n < s.length() + 11) { // TODO: fix for utf-8System.err.println("Message too long for RSAEncoder");return null;}byte[] ba = new byte[n];int i = s.length()-1;while(i >= 0 && n > 0) {int c = s.codePointAt(i--);if(c < 128) { // encode using utf-8ba[--n] = new Byte(String.valueOf(c));}else if((c > 127) && (c < 2048)) {ba[--n] = new Byte(String.valueOf((c & 63) | 128));ba[--n] = new Byte(String.valueOf((c >> 6) | 192));} else {ba[--n] = new Byte(String.valueOf((c & 63) | 128));ba[--n] = new Byte(String.valueOf(((c >> 6) & 63) | 128));ba[--n] = new Byte(String.valueOf((c >> 12) | 224));}}ba[--n] = new Byte("0");byte[] temp = new byte[1];Random rdm = new Random(47L);while(n > 2) { // random non-zero padtemp[0] = new Byte("0");while(temp[0] == 0)rdm.nextBytes(temp);ba[--n] = temp[0];}ba[--n] = 2;ba[--n] = 0;return new BigInteger(ba);}
}

Base64与十六进制的相互转化

public class B64 {public static String b64map="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";private static char b64pad = '=';private static String hexCode = "0123456789abcdef";// 获取对应16进制字符public static char int2char(int a){return hexCode.charAt(a);}// Base64转16进制public static String b64tohex(String s) {String ret = "";int k = 0;int slop = 0;for(int i = 0; i < s.length(); ++i) {if(s.charAt(i) == b64pad) break;int v = b64map.indexOf(s.charAt(i));if(v < 0) continue;if(k == 0) {ret += int2char(v >> 2);slop = v & 3;k = 1;}else if(k == 1) {ret += int2char((slop << 2) | (v >> 4));slop = v & 0xf;k = 2;}else if(k == 2) {ret += int2char(slop);ret += int2char(v >> 2);slop = v & 3;k = 3;}else {ret += int2char((slop << 2) | (v >> 4));ret += int2char(v & 0xf);k = 0;}}if(k == 1)ret += int2char(slop << 2);return ret;}// 16进制转Base64public static String hex2b64(String h) {int i , c;StringBuilder ret = new StringBuilder();for(i = 0; i+3 <= h.length(); i+=3) {c = parseInt(h.substring(i,i+3),16);ret.append(b64map.charAt(c >> 6));ret.append(b64map.charAt(c & 63));}if(i+1 == h.length()) {c = parseInt(h.substring(i,i+1),16);ret.append(b64map.charAt(c << 2));}else if(i+2 == h.length()) {c = parseInt(h.substring(i,i+2),16);ret.append(b64map.charAt(c >> 2));ret.append(b64map.charAt((c & 3) << 4));}while((ret.length() & 3) > 0) ret.append(b64pad);return ret.toString();}
}

获取学籍信息、课表、成绩

 // 获取学籍信息public void getStudentInformaction() throws Exception {connection = Jsoup.connect(url+ "/jwglxt/xsxxxggl/xsxxwh_cxCkDgxsxx.html?gnmkdm=N100801&su="+ stuNum);connection.header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0");response = connection.cookies(cookies).ignoreContentType(true).execute();JSONObject jsonObject = JSON.parseObject(response.body());System.out.println("--- 基本信息 ---");System.out.println("学号:" + jsonObject.getString("xh_id"));System.out.println("性别:" + jsonObject.getString("xbm"));System.out.println("民族:" + jsonObject.getString("mzm"));System.out.println("学院:" + jsonObject.getString("jg_id"));System.out.println("班级:" + jsonObject.getString("bh_id"));System.out.println("专业:" + jsonObject.getString("zszyh_id"));System.out.println("状态:" + jsonObject.getString("xjztdm"));System.out.println("入学年份:" + jsonObject.getString("njdm_id"));System.out.println("证件号码:" + jsonObject.getString("zjhm"));System.out.println("政治面貌:" + jsonObject.getString("zzmmm"));}// 获取课表信息public void getStudentTimetable(int year , int term) throws Exception {connection = Jsoup.connect(url+ "/jwglxt/kbcx/xskbcx_cxXsKb.html?gnmkdm=N2151");connection.header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0");connection.data("xnm",String.valueOf(year));connection.data("xqm",String.valueOf(term * term * 3));response = connection.cookies(cookies).method(Connection.Method.POST).ignoreContentType(true).execute();JSONObject jsonObject = JSON.parseObject(response.body());if(jsonObject.get("kbList") == null){System.out.println("暂时没有安排课程");return;}JSONArray timeTable = JSON.parseArray(jsonObject.getString("kbList"));System.out.println(String.valueOf(year) + " -- " + String.valueOf(year + 1) + "学年 " + "第" + term + "学期");for (Iterator iterator = timeTable.iterator(); iterator.hasNext();) {JSONObject lesson = (JSONObject) iterator.next();System.out.println(lesson.getString("xqjmc") + " " +lesson.getString("jc") + " " +lesson.getString("kcmc") + " " +lesson.getString("xm") + " " +lesson.getString("xqmc") + " " +lesson.getString("cdmc") + " " +lesson.getString("zcd"));}}// 获取成绩信息public void getStudentGrade(int year , int term) throws Exception {Map<String,String> datas = new HashMap<>();datas.put("xnm",String.valueOf(year));datas.put("xqm",String.valueOf(term * term * 3));datas.put("_search","false");datas.put("nd",String.valueOf(new Date().getTime()));datas.put("queryModel.showCount","20");datas.put("queryModel.currentPage","1");datas.put("queryModel.sortName","");datas.put("queryModel.sortOrder","asc");datas.put("queryModel.sortName","");datas.put("time","0");connection = Jsoup.connect(url+ "/jwglxt/cjcx/cjcx_cxDgXscj.html?gnmkdm=N305005&layout=default&su=" + stuNum);connection.header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0");response = connection.cookies(cookies).method(Connection.Method.POST).data(datas).ignoreContentType(true).execute();connection = Jsoup.connect(url+ "/jwglxt/cjcx/cjcx_cxDgXscj.html?doType=query&gnmkdm=N305005");connection.header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0");response = connection.cookies(cookies).method(Connection.Method.POST).data(datas).ignoreContentType(true).execute();JSONObject jsonObject = JSON.parseObject(response.body());JSONArray gradeTable = JSON.parseArray(jsonObject.getString("items"));for (Iterator iterator = gradeTable.iterator(); iterator.hasNext();) {JSONObject lesson = (JSONObject) iterator.next();System.out.println(lesson.getString("kcmc") + " " +lesson.getString("jsxm") + " " +lesson.getString("bfzcj") + " " +lesson.getString("jd"));}}

完整代码请点击这里:Semi-automatic-Crawl-JWGL (欢迎Fork、提Issues)

写在最后

这个小东西也花了我好长时间,主要还是卡在RSA加密那一块,我一开始并不知道不同平台对RSA的实现方式不同,也不知到RSA有多种填充方式,还需要多研究一下这方面的问题。然后这个小东西目前仅仅只是登陆,关于信息的获取与分析接下来也会着手去做的。


参考

CUMT教务系统模拟登录
RSA加密算法 - - 维基百科
javascript使用RSA加密提交数据
RSA使用js加密,使用java解密
新浪微博数据抓取(java实现)

模拟登陆新版正方教务管理系统相关推荐

  1. 模拟登陆新版正方教务管理系统【可以获取学生基本/课表信息】

    写在前面 博主登陆现在还可以正常使用,但是后面登陆成功,获取信息啥的有问题 登陆还是按照学长的来,模拟登陆新版正方教务管理系统 开始 学校教务系统改版,我直接copy博主代码获取学籍那里一直是获取到的 ...

  2. 教务系统自动评教_新版正方教务管理系统自动评教脚本

    本脚本适用于新版正方教务管理系统学生评教页面. 使用注意: 1,本脚本必须在Chrome内核浏览器使用,其余内核(IE,Edge等)无法使用! 2,如果您使用的是360浏览器/QQ浏览器/百度浏览器等 ...

  3. 新版正方教务管理系统API(获取课程表,考试,成绩,通知,自动抢课等)

    一个有关新版正方教务管理系统(如下图展示的主页面即为新版教务系统)的API,可以实现教务系统内基础的查询功能,未来还会添加选课抢课,一键评价等功能. 已实现与待实现 自动登陆.cookies获取 个人 ...

  4. JSoup模拟登录新版正方教务系统(内网-教务系统)获取信息过程详解

    新版正方教务系统登录界面: 目录 一.需求分析 二.模拟登录内网 三.模拟登录教务系统 四.爬取成绩和课表信息 参考文章 一.需求分析   需要访问教务系统,爬取出课表成绩等信息,并在自己所写的APP ...

  5. 用Python登陆新版正方教务系统获取课程表(及RSA加密密码实现)

    前言 最近做一个微信小程序,需要登录教务系统.提前用python尝试一下登录接口,并获取到课表打印出来. 我们学校用到新版正方教务系统,长这个样子. 相比旧版的教务系统,唯一好处是不用输入二维码方便爬 ...

  6. 爬取正方教务管理系统获取学生信息

    新版正方教务系统请点这里:模拟登陆新版正方教务管理系统(获取学籍信息.课表和成绩) 最近想学点爬虫玩玩,拿学校的教务系统练练手.学校与很多高校一样,用的是正方教务管理系统,非常的不好用,经常出现登陆不 ...

  7. go语言爬取新版正方教务系统数据

    go语言爬取新版正方教务系统数据 学完go语言的基础之后已经过了一个多月了,开始想试着利用些时间写写博客,就打算将半个月前练着写的一个go爬虫小项目翻出来写写.由于之前对go爬虫的一些基础知识不熟悉, ...

  8. 使用 PyRsa 库解决新版正方教务的 RSA 加密问题并模拟登陆的 Python 实现

    前言 先前有一个契机,需要模拟登陆学校所使用的正方教务来获取课程表,其所使用的 RSA 加密方法没有现成的 Python 库可使用,目前其他的 CSDN 博客所采用的方法均无法成功登陆,故只能自己研究 ...

  9. 新版正方教务网模拟登录python实现

    新版正方教务网模拟登录 前言 正好大学期末考完,分析一下正方教务网登录,实现模拟登录就可以自己实现抢课和查询成绩等常规操作了 申明 本文以我学校的正方教务网为例,关键信息已打码,理论上适用于所有新版正 ...

最新文章

  1. Python3多线程threading介绍(转载)
  2. Caffe在Ubuntu 14.04 64bit 下的安装------pycaffe 配置
  3. Maven最佳实践:版本管理
  4. SQL SERVER中用户定义标量函数(scalar user defined function)的性能问题
  5. 计划任务工具cron 的配置和说明
  6. 每天一个linux命令(56):netstat命令
  7. 算法代码块总结(持续更新)
  8. winscp连接windows_winscp登陆云主机,winscp登陆云主机如何登陆,教程详情
  9. stage3图书管理系统服务器部署
  10. ajax离开页面方法,jQuery中ajax调用当前页面方法
  11. 智能优化算法改进算法 -附代码
  12. SkewTransform
  13. Hadoop面试题总结(大数据面试)
  14. C语言LMS双麦克风消噪算法,一种双麦克风语音降噪方法与流程
  15. Python collections模块之Counter()详解
  16. Windows10常用功能一键设置优化工具分享
  17. 随身wifi挑选要注意哪些问题才能避免上当
  18. 代码之外——名人哲语
  19. 玩知乎五年,我赚了多少钱?
  20. 输入圆半径计算圆周长、圆面积、圆球表面积,结果保留3位小数。 注意:输入的半径可能是整数,也可能是小数。

热门文章

  1. GlobalSign 发布了即将要修改代码签名证书的重要通知
  2. 史上最强!目标检测数据集标注工具网页版
  3. 2022年最好的谷歌浏览器扩展程序
  4. 【通用】Windows Server 2012突然无法复制文件到阿里云服务器
  5. jy-12-SPRINGMYBATIS02——云笔记06-刘苍松
  6. 如何在终端里面使用ping来查局域网活动ip
  7. Visual Assist 使用小结
  8. 在CentOS下安装clickhouse
  9. 重磅福利!ICCV 2019全部论文合集共1075篇!会议信息全收录!
  10. ERDAS将DN值转换为TOA光谱辐射亮度时 出现建模错误原因