此处是废话,可以直接大纲目录跳过去。
现在是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来代替了,下面同理
二、分析
今日校园是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在请求过程中,匹配的前提是,你携带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,目标达成!
总结步骤啦
- 获取lt与Conversation
- 识别captcha
- 构造body
- 获取MOD_AUTH_CAS
三、重点
通过上面分析来看,其实不难,难得是识别验证码。
这验证码的识别,原来门道这么多,比方说一个简单的数字验证码,就要经过将图片预处理(类似于人在调节亮度、对比度之类的这种操作)、然后将图片中数字分割、训练、最后再进行识别。识别还要进行一个像素一个像素的比较,取相同点最多的。
我简直头大了,这要是我自己写的话,不得搞一年??
后来就试了一下百度的AI识别验证码,不得不说,真牛逼。但是呢,还要注册绑定个人信息,才能给用,算了,太麻烦了。
就在网上看了看,发现了Java一个比较牛逼的库,tess4j,使用他的前提是,你还得下载他的识别训练库
那就用他了,我一开始是想让java直接识别网页的验证码,但是格式不支持。没想到好的办法。最后的实现思路是
- 下载验证码
- 识别
- 矫正格式
附上识别验证码的工具类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;
public class CaptchaDecoding {
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(); 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"); }
public static String parseCaptcha(File file) { Tesseract tess = new Tesseract(); tess.setDatapath(ClassLoader.getSystemResource("tessdata").getPath().substring(1));
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;
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; private static String cookie; public static String doLogin = host + "/iap/doLogin"; public static String login = host + "/portal/login"; public static String getLt = host + "/iap/login?service=" + host + "/portal/login"; public static String checkLt = host + "/iap/security/lt"; public static String getCaptcha = host + "/iap/generateCaptcha?ltId="; public static String MOD_AUTH_CAS = null; public static String task = host + "/portal/task/queryTodoTask";
public static String getSub(String s, String regex) { Matcher matcher = Pattern.compile(regex).matcher(s); while (matcher.find()) { return matcher.group(0); } return null; }
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"); map.put("X-Requested-With", "XMLHttpRequest"); map.put("Cookie", cookie); return map; }
public static String getCaptcha(String url) { String s = CaptchaDecoding.parseCaptcha(CaptchaDecoding.downloadCaptcha(url, null)); return s.substring(0, 5); }
public static String getLt(HttpURLConnection conn) { return getSub(conn.getHeaderField("Location"), "(?<==)\\S+$"); }
public static String getCookie(HttpURLConnection conn) { return conn.getHeaderField("Set-Cookie").split(";")[0]; }
public static String getLoginBody(String captcha) { if (captcha == null) captcha = ""; return "username=" + id + "&password=" + pw + "&mobile=&dllt=&captcha=" + captcha + "&rememberMe=false&" + "lt=" + lt; }
public static String login(String param) { JSONObject object = JSONObject.fromObject(HttpUtil.sendPost(doLogin, param, getHeaders(cookie)));
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; }
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; }
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识别验证码
- captcha-ock
- tess4j
- tessdata
- 使用Tesseract OCR来实现图片文字识别
- 利用Tess4J进行验证码识别
- java将网页转图片
写在最后,我写这篇教程的意义,不是为了让你照抄代码,说实话,我的码品也不太行,抄代码没意思。我分享的是思路。如果思路搞明白了,那么问卷、查寝、签到、请假的登录不就都可以实现了吗?哈哈。
这叫做授人以鱼不如授人以渔!