模拟登陆新版正方教务管理系统(获取学籍信息、课表和成绩)

引入

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

前一段时间爬了学校的旧版的教务系统,旧版主页
旧教务系统
想了解更多请点击这里:爬取学校教务系统获取学生信息

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


准备

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

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

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

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

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

点击登陆时,可以看到向服务器发送了一个POST请求,data域中包含如下数据:
data域
csrftoken为了防止跨站域请求伪造
yhm为输入的用户名
mm并不是我们输入的密码

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

与此同时,发现结尾有很多JS文件
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和Cookies
    private 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-8
            System.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-8
                ba[--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 pad
            temp[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进制转Base64
    public 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实现)

猜你喜欢

转载自blog.csdn.net/ldx19980108/article/details/81866351