首先,我们学校的教务系统是青果的教务系统,KINGOSOFT 教务系统应该是青果的新版本的教务系统。不知道为什么是英文,也许是英文显得比较厉害一点。
其次,我们学校的教务系统长这个样子
这个样子
以及这个样子的
我们要做到模拟登录,那么就应该是模拟真实的用户来进行登录,那么我们就来先分析一下他的登录逻辑。
doLogon()
的方法,那么我们就进入这个方法看看,这个方法到底做了些什么事情这是 doLogon()
方法的 js 代码
function doLogon() {
// 输入信息验证
if (!validate()) {
return false;
}
// 验证码正确性验证
var username = j$("#yhmc").val();
var password = j$("#yhmm").val();
var randnumber = j$("#randnumber").val();
var passwordPolicy = kutil.isPasswordPolicy(username, password);
var url = _webRootPath + "cas/logon.action";
password = hex_md5(hex_md5(password)+hex_md5(randnumber.toLowerCase()));
/**
var params = {
"yhmc" : username,
"yhmm" : password,
"randnumber": randnumber,
"isPasswordPolicy" : passwordPolicy
};
*/
var p_username = "_u"+randnumber;
var p_password = "_p"+randnumber;
username = base64encode(username+";;"+_sessionid);
var params = p_username+"="+username+"&"+p_password+"="+password+"&randnumber="+randnumber+"&isPasswordPolicy="+passwordPolicy ;
//alert("params="+params);
params = getEncParams(params);
//alert("encparams="+params);
doPreLogon();
kutil.doAjax(url, params, doPostLogon);
function doPreLogon(){
j$("#msg").html("正在登录......");
j$("#login").attr("disabled", true);
j$("#reset").attr("disabled", true);
}
function doPostLogon(response) {
var data = JSON.parse(response);
var status = data.status ;
var message = data.message ;
if ("200" == status) {
var result = data.result ;
window.document.location.href = result ;
} else {
reloadScript("kingo_encypt",_webRootPath+"custom/js/SetKingoEncypt.jsp");
if("407" == status){
alert(message);
showMessage("");
}else{
showMessage(message);
}
j$("#login").attr("disabled", false);
j$("#reset").attr("disabled", false);
refreshImg();
if ("401"==status) {
j$("#randnumber").val("");
j$("#randnumber").focus();
} else {
j$("#yhmc").val("");
j$("#yhmm").val("");
j$("#randnumber").val("");
j$("#yhmc").focus();
}
}
}
}
function validate() {
var username = j$("#yhmc").val();
var password = j$("#yhmm").val();
var randnumber = j$("#randnumber").val();
if (kutil.isNull(username)) {
showMessage("请输入用户名!");
j$("#yhmc").focus();
return false;
}
if (kutil.isNull(password)) {
showMessage("请输入密码!");
j$("#yhmm").focus();
return false;
}
if (kutil.isNull(randnumber)) {
showMessage("请输入验证码!");
j$("#randnumber").focus();
return false;
}
return true;
}
我们接下来,一句一句的慢慢分析
// 输入信息验证
if (!validate()) {
return false;
}
这里调用了一个 validate()
方法,可以看到,如果想要继续执行下去,这个方法就应该返回 true
,否则的话,就会直接 return 结束这个 doLogon()
方法那么我们就看下这个 validate()
方法里面做了些什么事情。
function validate() {
var username = j$("#yhmc").val();
var password = j$("#yhmm").val();
var randnumber = j$("#randnumber").val();
if (kutil.isNull(username)) {
showMessage("请输入用户名!");
j$("#yhmc").focus();
return false;
}
if (kutil.isNull(password)) {
showMessage("请输入密码!");
j$("#yhmm").focus();
return false;
}
if (kutil.isNull(randnumber)) {
showMessage("请输入验证码!");
j$("#randnumber").focus();
return false;
}
return true;
}
到这里可以看到 validate()
方法显示获取了用户输入的用户名、密码和验证码,然后使用封装好的 kutil
对象中的 isNull
方法进行了验证,只有当得到的值都为 false
的时候,才能执行最后的 return true
。
那我们就来看看,这个 isNull
方法到底是怎么验证的。我们继续使用调试器中的全局搜索找到这个方法。
/**
* 判断某个变量值是否为空值
*
*/
my.isNull = function(initValue){
if (initValue == null || initValue.length == 0) {
return true;
} else {
return false;
}
}
可以看见这个方法只是一个单纯的判断字符串是否为空,如果为空则返回 true,否则返回 false。
到这里,这个就很明确了,当三个输入框(用户名、密码、验证码)中得任何一个没有输入内容的时候,都会结束这个 doLogon()
方法,并且给出相应的提示。
我们接着分析后面的代码,在用户输入的用户名、密码、验证码都不为空的情况下,就会执行后面的代码
// 验证码正确性验证
var username = j$("#yhmc").val();
var password = j$("#yhmm").val();
var randnumber = j$("#randnumber").val();
var passwordPolicy = kutil.isPasswordPolicy(username, password);
var url = _webRootPath + "cas/logon.action";
可以看见,这里又在一次获取到了三个输入框的值,并赋值给 username
、password
、randnumber
,代表的意思就是用户名、密码、验证码。
然后 passwordPolicy
这个变量的值是通过执行了 kutil
对象中的 isPasswordPolicy
方法获取到的,我们在全局中搜索这个方法,然后找到了下面的代码
/**
* 检测密码是否符合密码策略
* 1、用户密码不能与登陆帐号一致。
* 2、用户密码必须为6位或6位以上字符长度,并且包含有字符和数字。
* return 1-符合; 0-不符合
*/
my.isPasswordPolicy = function(username, password){
if (password == "" || password == null || username == password){
return "0" ;
}
var passwordlen = new String(password).length ;
if (passwordlen < 6){
return "0" ;
}
/* 2019.2.16 因密码复杂度要求可以设置不需要包括字母和数字而去掉
var letter = new String(password).replace(/[^A-Za-z]/g, ""); // 保留字母
var letterlen = letter.length ;
if (letterlen == passwordlen) {
return "0";
}
var digit = new String(password).replace(/[^0-9]/g, ""); // 保留数字
var digitlen = digit.length ;
if (digitlen == passwordlen) {
return "0";
}
*/
return "1" ;
}
可以看见,这个方法中,主要是校验密码是否符合要求的。包括密码不能为空、密码不能和用户名相同以及密码的长度不能小于 6。如果符合的话就返回 1
, 否则就返回 0
。如果我们想要模拟登录的话,这个值就应该为 1
。
这样在前端校验密码是否符合要求,应该是为了减轻后台服务器的压力。毕竟密码都符合要求,那么密码肯定是错的。
那么,接下来,我们就来看看,在获取到了输入框中用户输入的值之后,做了些什么事情吧。
password = hex_md5(hex_md5(password)+hex_md5(randnumber.toLowerCase()));
/**
var params = {
"yhmc" : username,
"yhmm" : password,
"randnumber": randnumber,
"isPasswordPolicy" : passwordPolicy
};
*/
var p_username = "_u"+randnumber;
var p_password = "_p"+randnumber;
username = base64encode(username+";;"+_sessionid);
var params = p_username+"="+username+"&"+p_password+"="+password+"&randnumber="+randnumber+"&isPasswordPolicy="+passwordPolicy ;
//alert("params="+params);
params = getEncParams(params);
//alert("encparams="+params);
doPreLogon();
kutil.doAjax(url, params, doPostLogon);
可以看见,这里就开始对获取到的数据进行加密了。那么我们就来逐行看看这里的数据是如何加密的吧。
password = hex_md5(hex_md5(password)+hex_md5(randnumber.toLowerCase()));
这里先将密码(password)使用 hex_md5
方法进行加密,然后将验证码(randnumber)转换成了小写(这里其实没有必要,因为青果的验证码是都是数字,但是我们现在是在分析青果是怎么做的)。接着将这两个加密后的字符串拼接在一起之后,再使用 hex_md5
方法进行了一次加密。
可以看到,在这一句中 hex_md5
方法是关键,那么这个方法到底是什么呢,看名字我觉得应该是 md5 加密,那么我们就查找到这个方法,看看究竟是不是 md5 加密吧。
emmm~~~看不懂。那么就是当作他是 MD5 加密了(敷衍了····)。因为我之前没有接触 MD5加密,所以我是通过百度了 MD5 加密,然后使用 java 实现 MD5 加密。然后我通过自己写 html 调用这个这个方法和 java 写的方法得到的加密结果是否一致。然后确定了这确实就是 MD5加密。
那么这句代码就很明显了。总共进行了三次 MD5加密。
1、使用 MD5加密用户输入的密码
2、将用户输入的验证码转成小写之后,使用了 MD5 进行了加密
3、上面两个加密的结果进行拼接,然后再进行一次 MD5 加密
这时,我们就得到了加密后的密码(password)
var p_username = "_u"+randnumber;
var p_password = "_p"+randnumber;
这里,声明了两个变量 p_username、p_password。他们的值是使用的是字符串 _u
和 _p
分别拼接上验证码(randnumber)得到的。
username = base64encode(username+";;"+_sessionid);
这里开始对用户名进行加密了,这里先在用户输入的用户名后面拼接了两个字符串 ;;
和 _sessionid
,这个 _sessionid
就是 session,我们之后在模拟登录的时候,再说说这个值是怎么获取到的。
然后将拼接好的字符串使用 base64encode
这个方法进行了加密,看名字,这个方法应该是 base64 加密。我们看看这个方法是不是如我们想的一样,是 base64 加密呢。
可以看见,这个方法确实如猜想的一样,是 base64 加密
var params = p_username+"="+username+"&"+p_password+"="+password+"&randnumber="+randnumber+"&isPasswordPolicy="+passwordPolicy ;
//alert("params="+params);
params = getEncParams(params);
这里就是将我们得到的加密后的数据进行了拼接成了上面的格式,需要注意的是。isPasswordPolicy 这个字段是校验密码是否符合条件的,如果符合的话,这个值应该是 1
,我们在模拟登录的时候,这个值可以直接设置为 1
拿到拼接好之后的 params
,使用了 getEncParams
方法,再进行了一次加密。那~~这个方法是怎么加密的呢。我们再进去看看。
这里我把这里用到的主要方法提取到下面进行分析
var _deskey = '6531555511500043279';
var _nowtime = '2019-04-17 22:31:40';
function b64_encode(data) { return base64encode(utf16to8(data)); }
function md5(data) { return hex_md5(data); }
function des_encode(data) { return strEnc(data, _deskey, null, null); }
function getEncParams(params) {
var timestamp = _nowtime;
var token = md5(md5(params)+md5(timestamp));
var _params = b64_encode(des_encode(params));
_params = "params=" + _params + "&token="+token+"×tamp="+timestamp;
return _params;
}
function md5(data) { return hex_md5(data); }
var timestamp = _nowtime;
var token = md5(md5(params)+md5(timestamp));
这里需要注意的是:_nowtime
这个变量是当前时间的,_deskey
这个变量我没有完全懂是怎么生成的,只知道中间的13位是 _nowtime
这个时间转成到毫秒的时间戳的结果。前后的三位数字没有找到到底是什么。
所以在模拟登录的时候,这两个变量的值,只能从这个jsp页面中获取。这个页面的地址是 http://jw.tgc.edu.cn/custom/js/SetKingoEncypt.jsp
这里,和加密用户名密码的时候差不多,也是进行了三次 MD5 加密。将 params 和 timestamp 分别进行MD5加密,然后将拼接后的字符串再进行一次 MD5 加密。得到了 token
function b64_encode(data) { return base64encode(utf16to8(data)); }
function des_encode(data) { return strEnc(data, _deskey, null, null); }
var _params = b64_encode(des_encode(params));
可以看到这里将传递进来的 params
进行了两次加密,显示使用 des_encode
进行加密。然后再进行一次 b64_encode
加密。
b64_encode
我们已经知道这个是 base64 加密了。那么 des_encode
是什么呢。我们查找到这个方法看看是什么。
function des_encode(data) { return strEnc(data, _deskey, null, null); }
这个方法又调用了 strEnc() 这个方法,传递了总共传递了 4 个参数。第一个是传递过来的 params 参数,第二个参数是 _deskey,这个值在前面分析了,是服务器动态生成的,只能从这个 jsp 页面获取。
那么这个 strEnc
方法是什么呢,我们继续全局搜索看看这个方法是什么。
可以看到这个方法起始是 Des 加密。那么来总结一下这个 传递进来的 params 是怎么加密的:
这样就得到了加密后的 params
_params = "params=" + _params + "&token="+token+"×tamp="+timestamp;
return _params;
这里开始将这些值进行拼接,这里的作用应该是为了拼接登录请求。因为在登录的时候,发送的表单数据格式是下面这个样子
doPreLogon();
function doPreLogon(){
j$("#msg").html("正在登录......");
j$("#login").attr("disabled", true);
j$("#reset").attr("disabled", true);
}
这里这是做的只是一个提示的功能
kutil.doAjax(url, params, doPostLogon);
可以看见这里开始发送请求了,这个会发送携带之前拼接好的参数Post到 cas/logon.action
这个地址。
在执行之后,有这个 doPostLogon
回调函数会被调用。
到这里我们的登录逻辑就分析完成了,我们在看看这个回调函数会做些什么事情。
function doPostLogon(response) {
var data = JSON.parse(response);
var status = data.status ;
var message = data.message ;
if ("200" == status) {
var result = data.result ;
window.document.location.href = result ;
} else {
reloadScript("kingo_encypt",_webRootPath+"custom/js/SetKingoEncypt.jsp");
if("407" == status){
alert(message);
showMessage("");
}else{
showMessage(message);
}
j$("#login").attr("disabled", false);
j$("#reset").attr("disabled", false);
refreshImg();
if ("401"==status) {
j$("#randnumber").val("");
j$("#randnumber").focus();
} else {
j$("#yhmc").val("");
j$("#yhmm").val("");
j$("#randnumber").val("");
j$("#yhmc").focus();
}
}
}
首先在发送请求之后,我们会得到这样的一条 JSON 数据,如果状态码是 200 ,说明登录成功,那么就会跳转到 result 中的地址去。
{"message":"操作成功!","result":"\/MainFrm.html?random=0.8587982073702812","status":"200"}
如果状态码是 402,说明密码错误,会得到这样的一条数据
{"message":"您24小时内已输错1次密码,若超过5次,您的帐号将被锁定!","result":null,"status":"402"}
如果状态码是 401,说明验证码错误,会得到这样的一条数据
{"message":"验证码有误!","result":null,"status":"401"}
那么到这里,整个登录的逻辑就已经分析完成。第一次学习分析,还是很难受,虽然他的所有 js 都是明文的,但是在看到加密的时候还是会很懵逼,因为之前没有做过在前端对数据进行加密。在看到 MD5 加密和 Des 加密的时候有点懵逼,不知道怎么去实现这样的一个加密方法。以及这两种加密在前端和后端加密的过程是不一样的。
之前也有记笔记的习惯。但是一直都是放在本地的。这次分析青果的登录逻辑,虽然算不得什么,但是对于我来时,是之前没有做过的事情,所以把他放在博客上面。
下面我准备使用 Java 实现这样的一个模拟用户登录的过程,然后再获取成绩之类的数据。