PHP/JAVA抓取新版正方教务系统获取课程表(及RSA加密密码实现)

PHP抓取新版正方教务系统获取课程表(及RSA加密密码实现)


前言

相比旧版的教务系统,唯一好处是不用输入验证码方便爬虫登录。但登录时用到RSA加密密码发送请求。

登录请求分析

在登录页面上填上随便写的账号密码,点击登录,浏览器开发者工具网络请求如下:

首先它点击登录后,提交一个表单,Form Data一共有4个数据

提交的数据解释
csrftoken为了防止跨站域请求伪造 。在登录页源码里有,每次刷新都会变更
yhm输入的用户名(学号)
mm输入的密码,被加密过。我们主要关注这一个加密过程

 csrftoken获取

解析登录页面的表单标签,每次都不一样,取出 input[name=csrftoken] 的值即可。

RSA公钥获取

在开发者工具的网络请求列表,可以看到这个请求链接,发送当前时间戳,并且每次modulus的数据不同,用该数据对登录密码进行加密,如何加密就是后面的重点要叙述的内容了。

原网站的js分析

	$.getJSON(_path+"/xtgl/login_getPublicKey.html?time="+new Date().getTime(),function(data){
		modulus = data["modulus"];
		exponent = data["exponent"];
	});


	if($("#mmsfjm").val() == '0'){
		$("#hidMm").val($("#mm").val());
	}else{
		var rsaKey = new RSAKey();
		rsaKey.setPublic(b64tohex(modulus), b64tohex(exponent));
		var enPassword = hex2b64(rsaKey.encrypt($("#mm").val()));
		$("#mm").val(enPassword);
		$("#hidMm").val(enPassword);   
	}

加密过程

首先将modulus,exponent转base64,然后再转16进制,再通过RSA算法生成公钥,
用公钥将密码生成密文,最后从16进制转回base64的加密字符串。

至此,我们分析完成,在PHP里面要做的步骤:
1、获取到csrftoken
2、发送时间戳获取到PublicKey
3、生成RSA加密的密码
4、POST请求登录

密码RSA加密方式

第一种方式:可以在前端通过js处理加密,然后传给后端

这种方式其实就是把正方教务系统登录页加密相关的js下载到自己项目中,用官网一样方式来处理加密,这种方式可靠性高,接入成本低,易实现,有个缺点是如果我们的应用有绑定功能就显得有点麻烦。

具体实现请看这篇博文:

新版正方教务系统模拟登录登录密码RSA加密破解

第二种方式:后端代码(PHP/JAVA)执行js

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

第三种方式:后端通过标准的RSA加密处理

看起来很顺畅的思路,但是遇到很多坑,主要是在对密码加密的时候,PHP与JavaScript在对数据进行RSA加密有些区别,由于正方在RSA加密钱和加密后有对数据有自己的预处理逻辑。

JavaScript在加密前对数据进行了随机填充,并用RSA/None/NoPadding的填充方式来加密,每一次得到的每一次结果都不同;JPHP在RSA加密时默认的填充方式为OPENSSL_PKCS1_PADDING。我选择OPENSSL_NO_PADDING的填充方式初始化公钥并没成功。

第四种方式:后端代码JavaScript前端加密方式实现

这种方式应该算是一种优雅的方式,但是按js的代码逻辑来写一遍后端逻辑,成本是非常大的,可靠性并比一定高,这里还是有牛人已经实现了,这里分享出来。

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();
    }
}

完整的源码:Semi-automatic-Crawl-JWGL

博主采用的方案一已用于生产环境。PHP实现方式,博主暂未亲自验证,使用时注意自己多调试!

要体验项目的小伙伴,请关注公众号:

 

  • 5
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值