本文章在:2018年06月13日 16:57:50 发表于: CSDN
2018年11月17日 迁移至新博客服务器。

因为毕设选题选的是和学校教务系统相关的,要做一个Android端的助手类App。所以前段时间也开始研究起了我们学校使用的青果教务管理系统。

一开始的想法还是挺标准的:登录 —— 抓目标地址的网页 —— 解析 —— 显示。但是事实上这第一步登录那儿就把我卡住了。。。

20180613144557808.png

那行吧,那咱就抓包分析下,为啥登陆不进去这个系统。


通过谷歌浏览器抓包发现登录地址是:http://210.42.72.73:888/jwweb/_data/index_LOGIN.aspx,而登录请求发送了大量奇怪的数据

20180613150404250.png

登录form只有3个输入框,但是这里提交了这么多数据,想想就觉得不科学!所以我们接着查看它的HTML源文件:

20180613150928942.png

这里我们很快就找到了这些隐藏的参数位置,一一核对发现,其中“__VIEWSTATE”字段的值时在该页面加载好了的时候就已经被赋好了的,猜测应该是服务器在客户端访问时就分配好了的一个ID;“txt_asmcdefsddsd”表示的是学号。这里就出现一个问题了,我们输入的账号、密码、验证码三个字段,最后提交的时候仅有学号字段有值,密码和验证码字段并没有值,而且一并提交的诸如:dsdsdsdsdxcxdfgfgfgfggfdgtyuuyyuuckjg等不明所以的字段在一开始是空的,但是提交时却又是有值的。这究竟是为什么呢?

让我们顺着后面的JS继续看,这里很明显地调用了chkyzm()和chkpwd()两个方法,方法体如下:

20180613153530687.png

chkpwd()将输入的密码用MD5编码,取前30位转大写,然后再拼接上输入的账号以及学校代码(这里的11072就是学校编码,不同学校这里的编码不一样)再使用MD5编码一次,然后截取前30位转大写。最后将值写入到“dsdsdsdsdxcxdfgfg”字段中。chkyzm()方法也类似,是将你输入的验证码使用MD5编码后取前30位转大写,然后再拼上学校代码后进行MD5编码,再截取前30位转大写,最后将值写入到“fgfggfdgtyuuyyuuckjg”字段中。这两段JS也很好地为我们解释了为什么一些字段一开始没有值,但是当表单提交时却又出现了莫名其妙的值。

同样,其他的一些字段,都能从页面里面的JS脚本里面看到对应的编码方法。

那么,知道了这些加密方法之后,我们就可以开始着手进行设计自己的登录程序了。但是在此之前,为了保险起见,还是先测试一下。这里我是用的是Google Chrome上的Poster插件。在吧各种所需要的请求头请求体都填写完整后(验证码是通过浏览器访问验证码页面而直接获得的),点击提交,发现大部分时候还是能成功登录的。但是有些时候就会登录失败。

想了半天都没有突破性的解决方案,后来在一个偶然间,搜到了以前学霸学长的一篇博客,里面揭露的登录失败问题的根本原因:青果教务系统通过判断请求登录页和验证码页的时间差来反爬虫。。。详情请看

So这也提醒了我,后面去模拟登陆的时候要尽可能地减小这个时间差。


前面扯了那么多,其实都是事前准备,下面就要开始撸代码了。我使用的是Okhttp 3。


20180613161010185.png

登录系统之后才能进行后续操作,而服务器是通过session鉴别你的身份,在我们本地,这个“身份证”则被存在Cookie中,所以我们这里使用CookieJar初始化OKhttpClient,让它能持久化管理Cookie。

我们需要一个标准的MD5工具类来完成MD5加密的操作:

/
对字符串md5加密
@param 加密前的字符串 @return MD5加密后的字符串
*/
private static String getMD5(String str) { try { // 生成一个MD5加密计算摘要
MessageDigest md = MessageDigest.getInstance("MD5"); // 计算md5函数
md.update(str.getBytes()); // digest()最后确定返回md5 hash值,返回值为8为字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符
// BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值
return new BigInteger(1, md.digest()).toString(16);
} catch (Exception e) {
System.out.println("MD5加密出现错误");
} return "error";
}


然后就需要将我们输入的账号密码验证码进行“加密”:

// 加密密码
String Tmm = getMD5(id + getMD5(pwd).substring(0, 30).toUpperCase() + "11072").substring(0, 30).toUpperCase();
System.out.println(Tmm); // 加密验证码
String yzm = getMD5(getMD5(yz.toUpperCase()).substring(0, 30).toUpperCase() + "11072").substring(0, 30)
.toUpperCase();
System.out.println(yzm);

补充完其他请求参数并进行登录(其中乱码是因为需要把typeName的值进行GBK编码):

// 设置请求体
List params = new ArrayList<>();
params.add("��¼");
params.add( "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36undefined5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 SN:NULL");
params.add("ѧ��");
params.add("STU");
params.add(id);
params.add(Tmm);
params.add(yzm); boolean flag = sendHttpClientPost(params, url);

/
模拟登陆 /
private static boolean sendHttpClientPost(List user, String url) {
RequestBody body = new FormBody.Builder().add("Bdl", user.get(0)).add("pcInfo", user.get(1))
.add("typeName", user.get(2)).add("Sel_Type", user.get(3)).add("txt_asmcdefsddsd", user.get(4))
.add("txt_pewerwedsdfsdff", "").add("txt_sdertfgsadscxcadsads", "").add("sbtState", "")
.add("dsdsdsdsdxcxdfgfg", user.get(5)).add("fgfggfdgtyuuyyuuckjg", user.get(6)).build();
Request request = new Request.Builder().addHeader("Accept-Encoding", "gzip, deflate, sdch").addHeader( "User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36")
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8")
.addHeader("Referer", loginURL).addHeader("Origin", mainURL).addHeader("Host", host)
.addHeader("Connection", "keep-alive").post(body).url(url).build(); try {
Response response = mClient.newCall(request).execute(); if (response.isSuccessful()) { // 随便测试一个登陆后才能看见的网页看看能不能访问,能访问则表示成功,否则表示登录失败
Request testRequest = new Request.Builder().url("http://210.42.72.73:888/jwweb/KSSW/stu_ksap_rpt.aspx")
.get().build();
Response testResponse = mClient.newCall(testRequest).execute(); if (testResponse.isSuccessful()) { // 如果请求成功
String res = testResponse.body().string(); if (res.length() == 125) { // 判断返回的结果字符串,如果是提示登录,则说明登录不成功
return false;
} / 将账号密码数据插入数据库中 /
switch (login.isExistValue(account, passwd)) { case Login.INSERT_VALUE: int result = login.saveUserAccountInfo(account, passwd);
System.out.println("\ninsert on :\t" + result); break; case Login.UPDATE_VALUE:
result = login.updateAccountInfo(account, passwd);
System.out.println("\nupdate on :\t" + result); default:
System.out.println("\nno change!"); break;
} return true;// 返回登录成功
}
}
} catch (IOException e) { // TODO Auto-generated catch block
e.printStackTrace();
} return false;
}

写得很简陋,因为就是个demo,所以基本都是Hard Code啦,大家将就看...大笑

再登陆成功之后呢?登陆成功之后那肯定不用我说咯,该干嘛干嘛呀!该抓哪个网页就直接请求地址抓数据就行了呀!

这里需要提醒大家注意的是,因为我们在后续请求中,服务器会根据我们附带在请求中的Cookie内的信息判断我们的身份,没了Cookie或者两次访问所携带的Cookie不一样,就可能导致服务器相应内容的不同甚至服务器直接拒绝显示页面(具体原理可以百度:Cookie和Session),所以我们在登录成功后,后续请求操作请务必使用同一个Client进行操作。这里建议大家使用单例的设计模式,使用getInstance得到对象,保证全局使用的都是同一个Client。

写了一个基于Java模拟登陆的小demo,输入账号密码验证码能够登录,然后查看成绩和考试安排等等。点击查看资源

有兴趣的童鞋可以去试试OCR识别验证码,然后做个免验证码登录~我用百度的API调通了,但是精准识别有点贵。。所以后来就删了(摊手)



PS. 最后因为比较赶时间,所以我的毕业设计并不是像上述代码一样直接与学校教务系统进行交互,针对找到的教务系统的接口进行对接,而是直接使用的学长多年前测试通过的一个系统转接了一下,给自己省下了起码有1周的时间可以去摸鱼啊蛤蛤蛤

20180613165539658.png

关于Android端和Web端的改天摸鱼的时候再来嗦一嗦~