今日校園自動登錄教程


我之前寫的腳本都是保持會話的那種,登錄一次永久有效。這不是放假了嗎,就重新完成了自動登錄版。

此處是廢話,可以直接大綱目錄跳過去。

現在是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。

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,目標達成!

總結步驟啦

  1. 獲取lt與Conversation
  2. 識別captcha
  3. 構造body
  4. 獲取MOD_AUTH_CAS

三、重點

通過上面分析來看,其實不難,難得是識別驗證碼。

這驗證碼的識別,原來門道這么多,比方說一個簡單的數字驗證碼,就要經過將圖片預處理(類似於人在調節亮度、對比度之類的這種操作)、然后將圖片中數字分割、訓練、最后再進行識別。識別還要進行一個像素一個像素的比較,取相同點最多的。

我簡直頭大了,這要是我自己寫的話,不得搞一年??

后來就試了一下百度的AI識別驗證碼,不得不說,真牛逼。但是呢,還要注冊綁定個人信息,才能給用,算了,太麻煩了。

就在網上看了看,發現了Java一個比較牛逼的庫,tess4j,使用他的前提是,你還得下載他的識別訓練庫

那就用他了,我一開始是想讓java直接識別網頁的驗證碼,但是格式不支持。沒想到好的辦法。最后的實現思路是

  1. 下載驗證碼
  2. 識別
  3. 矯正格式

附上識別驗證碼的工具類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

五、致謝

  1. Java識別驗證碼
  2. captcha-ock
  3. tess4j
  4. tessdata
  5. 使用Tesseract OCR來實現圖片文字識別
  6. 利用Tess4J進行驗證碼識別
  7. java將網頁轉圖片

寫在最后,我寫這篇教程的意義,不是為了讓你照抄代碼,說實話,我的碼品也不太行,抄代碼沒意思。我分享的是思路。如果思路搞明白了,那么問卷、查寢、簽到、請假的登錄不就都可以實現了嗎?哈哈。

這叫做授人以魚不如授人以漁


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM