言成言成啊 | Kit Chen's Blog

今日校园自动登录教程

此处是废话,可以直接大纲目录跳过去。

现在是2021年1月11日的凌晨2点,连着加班了4天,从无到有的写出来了一个自动登录脚本,还是蛮开心的。

网上很早就有大佬写的自动登录,但是他们的学校是以NOCLOUD的方式加入今日校园的,像比较牛逼的合肥工业大学,这种的都是将这些功能对接到自己学校官网了。

而像长春工大这种的,我看是CLOUD方式加入的,所以他们的那些NOCLOUD自动登录就不好使。

但是写完了,就感觉接下来的生活没啥动力了,我可能需要给自己定个新的目标了。

言归正传!

源码,放张运行截图。

一、接口

以下接口更新于1月11,后续像接口啥的不好使了,那就是今日校园升级了。

查询加入今日校园的所有学校

1
https://static.campushoy.com/apicache/tenantListSort

查询学校的详细信息

1
https://mobile.campushoy.com/v6/config/guest/tenant/info?ids=参数

参数是在上面的链接中搜索你的学校,获取的ID值。以长春工业大学为例,ccut即我们所需的参数

获取到了参数,我们就可以查询学校的详细信息。通过下图可知,长春工业大学的加入方式是CLOUD,登录地址idsUrl后面的那串地址。

xxx学校的云端登录地址在这里像xxx.campusphere.net我就用host来代替了,下面同理

1
https://host/iap

二、分析

今日校园是CAS单点登录系统,说白了,就是多个系统中,用户登录一次各个系统即可感知用户已登录。所以呢,云端跟手机app之前是共享某些关键数据的,比如cookie。因此,我们可以通过获取网页端的cookie,来实现手机app的提交。

我们提交问卷表时,需要携带正确的MOD_AUTH_CAS,而这个cookie是登录之后获取的。所以自动登录的最终目标就是获取MOD_AUTH_CAS。

手动登录一次,然后分析抓包的数据。

登录的接口

1
https://host/iap/doLogin

登录的请求体是

1
username=学号&password=密码&mobile=&dllt=&captcha=验证码&rememberMe=false&lt=lt值

可知,我们登录所需的是学号、密码、验证码和lt。

lt在请求过程中,匹配的前提是,你携带conversation请求。再通过抓包分析,我们需要访问下面的这个地址,来获取lt和conversation

1
https://host/iap/login?service=https://host/portal/login

返回结果如图所示

如此,我们就获取到了Conversation和lt。

接下来,就需要考虑验证码,需要携带lt和conversation来获取的,否则是不匹配的。

1
https://host/iap/generateCaptcha?ltId=lt

验证码是在错误三次的时候,才会异步请求验证码,界面弹出验证码选项。

我一开始的做法是不携带验证码登录,错误之后,再携带验证码,就跟常规登录流程一样。后来发现大可不必这么麻烦,我们第一次就主动请求验证码,然后携带登录,这样就方便多了。

学号、密码、验证码和lt以及Cookie中的Conversation准备就绪之后,我们就可以构造请求体,向登录接口发送请求了。

成功登录之后,会返回一个跳转链接。

访问这个链接,我们就可以获取到MOD_AUTH_CAS,目标达成!

总结步骤啦

  1. 获取lt与Conversation
  2. 识别captcha
  3. 构造body
  4. 获取MOD_AUTH_CAS

三、重点

通过上面分析来看,其实不难,难得是识别验证码。

这验证码的识别,原来门道这么多,比方说一个简单的数字验证码,就要经过将图片预处理(类似于人在调节亮度、对比度之类的这种操作)、然后将图片中数字分割、训练、最后再进行识别。识别还要进行一个像素一个像素的比较,取相同点最多的。

我简直头大了,这要是我自己写的话,不得搞一年??

后来就试了一下百度的AI识别验证码,不得不说,真牛逼。但是呢,还要注册绑定个人信息,才能给用,算了,太麻烦了。

就在网上看了看,发现了Java一个比较牛逼的库,tess4j,使用他的前提是,你还得下载他的识别训练库

那就用他了,我一开始是想让java直接识别网页的验证码,但是格式不支持。没想到好的办法。最后的实现思路是

  1. 下载验证码
  2. 识别
  3. 矫正格式

附上识别验证码的工具类CaptchaDecoding.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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吧。难倒是不难,主要是分析以及试错耗费了不少时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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或者更高即可)

1
java -jar cpdaily.jar

五、致谢

  1. Java识别验证码
  2. captcha-ock
  3. tess4j
  4. tessdata
  5. 使用Tesseract OCR来实现图片文字识别
  6. 利用Tess4J进行验证码识别
  7. java将网页转图片

写在最后,我写这篇教程的意义,不是为了让你照抄代码,说实话,我的码品也不太行,抄代码没意思。我分享的是思路。如果思路搞明白了,那么问卷、查寝、签到、请假的登录不就都可以实现了吗?哈哈。

这叫做授人以鱼不如授人以渔

发布:2021-01-10 05:32:11
修改:2021-01-12 18:30:23
链接:https://meethigher.top/blog/2021/cpdaily-autologin/
标签:java open 
付款码 捐助 分享
翻墙之后才能评论哦
阅读量