我之前写的脚本都是保持会话的那种,登录一次永久有效。这不是放假了吗,就重新完成了自动登录版。
此处是废话,可以直接大纲目录跳过去。
现在是2021年1月11日的凌晨2点,连着加班了4天,从无到有的写出来了一个自动登录脚本,还是蛮开心的。
网上很早就有大佬写的自动登录,但是他们的学校是以NOCLOUD的方式加入今日校园的,像比较牛逼的合肥工业大学,这种的都是将这些功能对接到自己学校官网了。
而像长春工大这种的,我看是CLOUD方式加入的,所以他们的那些NOCLOUD自动登录就不好使。
但是写完了,就感觉接下来的生活没啥动力了,我可能需要给自己定个新的目标了。
言归正传!
源码,放张运行截图。
一、接口
以下接口更新于1月11,后续像接口啥的不好使了,那就是今日校园升级了。
查询加入今日校园的所有学校
https://static.campushoy.com/apicache/tenantListSort
查询学校的详细信息
https://mobile.campushoy.com/v6/config/guest/tenant/info?ids=参数
参数是在上面的链接中搜索你的学校,获取的ID值。以长春工业大学为例,ccut即我们所需的参数
获取到了参数,我们就可以查询学校的详细信息。通过下图可知,长春工业大学的加入方式是CLOUD,登录地址idsUrl后面的那串地址。
xxx学校的云端登录地址,在这里像xxx.campusphere.net我就用host来代替了,下面同理
https://host/iap
二、分析
今日校园是CAS单点登录系统,说白了,就是多个系统中,用户登录一次各个系统即可感知用户已登录。所以呢,云端跟手机app之前是共享某些关键数据的,比如cookie。因此,我们可以通过获取网页端的cookie,来实现手机app的提交。
我们提交问卷表时,需要携带正确的MOD_AUTH_CAS,而这个cookie是登录之后获取的。所以自动登录的最终目标就是获取MOD_AUTH_CAS。
手动登录一次,然后分析抓包的数据。
登录的接口
https://host/iap/doLogin
登录的请求体是
username=学号&password=密码&mobile=&dllt=&captcha=验证码&rememberMe=false<=lt值
可知,我们登录所需的是学号、密码、验证码和lt。
lt在请求过程中,匹配的前提是,你携带conversation请求。再通过抓包分析,我们需要访问下面的这个地址,来获取lt和conversation
https://host/iap/login?service=https://host/portal/login
返回结果如图所示
如此,我们就获取到了Conversation和lt。
接下来,就需要考虑验证码,需要携带lt和conversation来获取的,否则是不匹配的。
https://host/iap/generateCaptcha?ltId=lt
验证码是在错误三次的时候,才会异步请求验证码,界面弹出验证码选项。
我一开始的做法是不携带验证码登录,错误之后,再携带验证码,就跟常规登录流程一样。后来发现大可不必这么麻烦,我们第一次就主动请求验证码,然后携带登录,这样就方便多了。
学号、密码、验证码和lt以及Cookie中的Conversation准备就绪之后,我们就可以构造请求体,向登录接口发送请求了。
成功登录之后,会返回一个跳转链接。
访问这个链接,我们就可以获取到MOD_AUTH_CAS,目标达成!
总结步骤啦
- 获取lt与Conversation
- 识别captcha
- 构造body
- 获取MOD_AUTH_CAS
三、重点
通过上面分析来看,其实不难,难得是识别验证码。
这验证码的识别,原来门道这么多,比方说一个简单的数字验证码,就要经过将图片预处理(类似于人在调节亮度、对比度之类的这种操作)、然后将图片中数字分割、训练、最后再进行识别。识别还要进行一个像素一个像素的比较,取相同点最多的。
我简直头大了,这要是我自己写的话,不得搞一年??
后来就试了一下百度的AI识别验证码,不得不说,真牛逼。但是呢,还要注册绑定个人信息,才能给用,算了,太麻烦了。
就在网上看了看,发现了Java一个比较牛逼的库,tess4j,使用他的前提是,你还得下载他的识别训练库
那就用他了,我一开始是想让java直接识别网页的验证码,但是格式不支持。没想到好的办法。最后的实现思路是
- 下载验证码
- 识别
- 矫正格式
附上识别验证码的工具类CaptchaDecoding.java
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import net.sourceforge.tess4j.Tesseract;
import net.sourceforge.tess4j.TesseractException;
/**
*
* CaptchaDecoding 用来识别验证码
*
* @author kit chen
* @github https://github.com/meethigher
* @blog https://meethigher.top
* @time 2021年1月10日
*/
public class CaptchaDecoding {
/**
* 云端下载验证码
*
* @param url
* @param headers
* @return
*/
public static File downloadCaptcha(String url, Map<String, String> headers) {
InputStream is = null;
FileOutputStream fos = null;
try {
URL realUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection();
// 必须设置false,否则会自动重定向到目标地址
conn.setInstanceFollowRedirects(false);
if (headers != null) {
Set<Entry<String, String>> set = headers.entrySet();
for (Entry<String, String> header : set) {
conn.setRequestProperty(header.getKey(), header.getValue());
}
}
conn.connect();
is = conn.getInputStream();
fos = new FileOutputStream("captcha.jpg");
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
fos.write(buffer, 0, length);
}
} catch (Exception e) {
System.out.println("读取验证码出错!");
e.printStackTrace();
} finally {
try {
if (is != null)
is.close();
if (fos != null)
fos.close();
} catch (Exception e2) {
}
}
return new File("captcha.jpg");
}
/**
* 识别验证码
*
* @param file
* @return
*/
public static String parseCaptcha(File file) {
Tesseract tess = new Tesseract();
//开发环境运行时设置
tess.setDatapath(ClassLoader.getSystemResource("tessdata").getPath().substring(1));
//jar包运行时设置
// String tesspath = System.getProperty("user.dir");
// tess.setDatapath(tesspath+"/tessdata");
tess.setLanguage("eng");
try {
return tess.doOCR(file).replace(" ", "");
} catch (TesseractException e) {
System.out.println("解析验证码出错!");
e.printStackTrace();
return null;
}
}
}
好像没啥特别难的了。
最后附上登录的工具类Login.java吧。难倒是不难,主要是分析以及试错耗费了不少时间。
import java.net.HttpURLConnection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sf.json.JSONObject;
/**
*
* Login 用来登录获取cookie的工具类
*
* @author kit chen
* @github https://github.com/meethigher
* @blog https://meethigher.top
* @time 2021年1月7日-2021年1月10日
*/
public class Login {
private static String host = Data.host;
private static String id = Data.id;
private static String pw = Data.pw;
// 最大试错次数
private static int maxError = 10;
// 这个值用来登录时携带,服务端有验证
private static String lt;
// 用来存放cookie
private static String cookie;
// 用来获取MOD_CAS_AUTH,返回值中ticket后面的值就是
public static String doLogin = host + "/iap/doLogin";
// 用来登录
public static String login = host + "/portal/login";
// 用来获取lt
public static String getLt = host + "/iap/login?service=" + host + "/portal/login";
// 用来验证lt
public static String checkLt = host + "/iap/security/lt";
// 用来获取验证码
public static String getCaptcha = host + "/iap/generateCaptcha?ltId=";
// 用来存放MOD_AUTH_CAS
public static String MOD_AUTH_CAS = null;
// 用于验证登录状态
public static String task = host + "/portal/task/queryTodoTask";
/**
* 通过正则截取字符串
*
* @param s
* @param regex
* @return
*/
public static String getSub(String s, String regex) {
// "(?<==)\S+$",正则用来提取=号之后的东西
Matcher matcher = Pattern.compile(regex).matcher(s);
while (matcher.find()) {
return matcher.group(0);
}
return null;
}
/**
* 请求头
*
* @param cookie
* @return
*/
public static Map<String, String> getHeaders(String cookie) {
Map<String, String> map = new LinkedHashMap<String, String>();
map.put("User-Agent",
"Mozilla/5.0 (Linux; Android 11; MI 11 Build/QKQ1.190825.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/33.0.0.0 Mobile Safari/537.36 okhttp/3.8.1");
map.put("Content-Type", "application/x-www-form-urlencoded");
map.put("Host", host);
map.put("Connection", "Keep-Alive");
map.put("Accept-Encoding", "gzip");
// 这个必须带着,不然登录时,要多一步获取cookie的步骤
map.put("X-Requested-With", "XMLHttpRequest");
map.put("Cookie", cookie);
return map;
}
/**
* 获取验证码
*
* @param url
* @return
*/
public static String getCaptcha(String url) {
String s = CaptchaDecoding.parseCaptcha(CaptchaDecoding.downloadCaptcha(url, null));
return s.substring(0, 5);
}
/**
* 获取LT
*
* @param conn
* @return
*/
public static String getLt(HttpURLConnection conn) {
return getSub(conn.getHeaderField("Location"), "(?<==)\S+$");
}
/**
* 获取响应头中的cookie
*
* @param conn
* @return
*/
public static String getCookie(HttpURLConnection conn) {
return conn.getHeaderField("Set-Cookie").split(";")[0];
}
/**
* 生成登录请求体
*
* @param captcha
* @return
*/
public static String getLoginBody(String captcha) {
if (captcha == null)
captcha = "";
return "username=" + id + "&password=" + pw + "&mobile=&dllt=&captcha=" + captcha + "&rememberMe=false&" + "lt="
+ lt;
}
/**
* 进行登录
*
* @param param
* @return
*/
public static String login(String param) {
JSONObject object = JSONObject.fromObject(HttpUtil.sendPost(doLogin, param, getHeaders(cookie)));
// 下面这串代码是开发时为了验证异步请求。结果证明需要。使用时直接注释,不用管
// HttpURLConnection postConn = HttpUtil.postConn(doLogin,param,getHeaders(cookie));
// System.out.println("输出:"+postConn.getHeaderField("Location").replace(host+"/portal/login?", ""));
String string = null;
if ("REDIRECT".equals(object.get("resultCode"))) {
string = "success";
HttpUtil.sendGet(object.getString("url"), getHeaders(""));
MOD_AUTH_CAS = getSub(object.getString("url"), "(?<==)\S+$");
} else if ("CAPTCHA_NOTMATCH".equals(object.get("resultCode"))) {
string = "captchaError";
} else if ("LT_NOTMATCH".equals(object.get("resultCode"))) {
string = "ltError";
} else if ("FAIL_UPNOTMATCH".equals(object.get("resultCode"))) {
string = "upError";
} else {
string = "error";
}
return string;
}
/**
* 获取成功登录状态的cookie
*
* @return
*/
public static String getAccess() {
String captcha, body;
System.out.println("获取登录数据...");
HttpURLConnection conn = HttpUtil.getConn(getLt, null);
lt = getLt(conn);
System.out.println("获取lt:" + lt);
cookie = getCookie(conn);
System.out.println("获取cookie:" + cookie);
int i = 1;
String loginResult = null;
while (i <= maxError) {
captcha = getCaptcha(getCaptcha + lt);
System.out.println("识别captcha:" + captcha);
body = getLoginBody(captcha);
System.out.println("生成body..." );
System.out.print("正在尝试第" + i + "次登录:");
loginResult = login(body);
if ("success".equals(loginResult)) {
break;
} else if ("captchaError".equals(loginResult)) {
System.out.println("captcha识别不正确!");
} else if ("ltError".equals(loginResult)) {
System.out.println("lt不匹配!");
} else if ("upError".equals(loginResult)) {
System.out.println("账户密码不匹配!");
} else {
System.out.println("检查账户是否冻结、今日校园官方系统是否异常、lt或账号密码是否为空,或者直接联系开发者meethigher@qq.com!");
}
i++;
}
if ("success".equals(loginResult)) {
System.out.println("登录成功!");
return MOD_AUTH_CAS;
} else {
System.out.println("登录失败!");
}
return null;
}
/**
* 验证是否已经失效
*
* @return
*/
public static boolean isOff() {
String result = HttpUtil.sendPost(task, "", getHeaders("MOD_AUTH_CAS=" + MOD_AUTH_CAS));
if (result.indexOf("WEC-REDIRECTURL") > 0) {
return true;
} else {
return false;
}
}
}
四、傻瓜版使用教程
本来我想做个网页端的,用于接收账户密码等个人信息,服务器自行运行,这样算是全透明的,但是考虑到工程量较大,意义也不大,就放弃了。
傻瓜版的话,下载我的源码中的easy版,这个适用于不会编程的小伙伴。
里面有三个文件,分别是cpdaily.jar包、tessdata语言识别包、collection.properties配置文件。
将他们随便放到一个文件夹中(如果运行有误,那就更换为路径没有中文的文件夹)
配置文件中,输入你的账号密码、学校的host、发件邮箱账号密码、收件邮箱、签到地址、提交的关键字(我们学校是单选,所以就关键字了)
切记配置文件中的中文用Unicode编码,不要用中文。
打开cmd,运行下面的命令即可(如果电脑没有java环境,自己百度即可,java8或java1.8或者更高即可)
java -jar cpdaily.jar
五、致谢
写在最后,我写这篇教程的意义,不是为了让你照抄代码,说实话,我的码品也不太行,抄代码没意思。我分享的是思路。如果思路搞明白了,那么问卷、查寝、签到、请假的登录不就都可以实现了吗?哈哈。
这叫做授人以鱼不如授人以渔!