今日校园自动登录教程

我之前写的脚本都是保持会话的那种,登录一次永久有效。这不是放假了吗,就重新完成了自动登录版。

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

现在是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将网页转图片

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

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

posted @ 2021-01-11 06:51  言成言成啊  阅读(1786)  评论(1编辑  收藏  举报