我之前寫的腳本都是保持會話的那種,登錄一次永久有效。這不是放假了嗎,就重新完成了自動登錄版。
此處是廢話,可以直接大綱目錄跳過去。
現在是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
五、致謝
寫在最后,我寫這篇教程的意義,不是為了讓你照抄代碼,說實話,我的碼品也不太行,抄代碼沒意思。我分享的是思路。如果思路搞明白了,那么問卷、查寢、簽到、請假的登錄不就都可以實現了嗎?哈哈。
這叫做授人以魚不如授人以漁!
