news 2026/11/4 5:21:39

Android模拟登录学信网查四六级:OkHttp与Cookie会话维持实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android模拟登录学信网查四六级:OkHttp与Cookie会话维持实战

简介:这是一份面向Android初学者与进阶开发者的实战示例源码,围绕模拟登录学信网查询四六级成绩这一具体场景,串联网络请求、数据解析与用户认证等核心技能点。项目共50个文件,以class编译产物、xml布局与配置、png图片资源及jar依赖库为主,另含java源码、apk安装包与dex文件,压缩包约1.46MB,结构完整可直接导入研究。已有739人学习下载,说明其在同类练手项目中具备一定参考价值。代码中可看到OkHttp或HttpURLConnection发起请求、Jsoup解析HTML页面提取成绩的思路,配合EditText与按钮完成登录交互,并涉及HTTPS传输、密码加密存储、INTERNET权限声明及try-catch异常处理等细节。对于想理解Android网络编程全流程、补齐UI事件处理与安全隐私认知的开发者,这份源码提供了可运行、可拆解的实践样本,便于对照学习与二次改造。

1. 模拟登陆学信网查四六级:一份能跑通的 Android 源码到底解决了什么

每年四六级出分那几天,总有人问我能不能做个 App 自动查成绩。市面上的查分工具要么要手动填一堆信息,要么查完就闪退。这份 Android 模拟登陆学信网查询四六级的源码,核心就干一件事:用代码把「打开网页、填账号密码、过验证、拿成绩」这条链路在手机端跑通。它适合两类人——想学 Android 网络请求与 Cookie 管理的开发者,以及需要把查分能力嵌进自己工具里的从业者。源码基于 Java 与 OkHttp 实现,覆盖了登录态维持、验证码处理、成绩页解析三个关键环节。下面我按「先搞懂原理、再动手复现、最后避开坑」的顺序拆开讲,每一步都落到能抄的代码和参数上。

2. 模拟登陆的底层链路:从 Cookie 到会话维持

2.1 为什么不能只发一个 POST 就完事

很多人第一次做模拟登陆,直觉是「找到登录接口,把账号密码 POST 过去,拿到响应就完事」。真跑起来会发现,第二次请求成绩页直接返回未登录。原因在于学信网这类站点用的是标准的 Session + Cookie 机制:登录请求成功后,服务端在响应头里下发Set-Cookie,后续所有请求必须带上这个 Cookie,服务端才认你是同一个人。OkHttp 默认不会自动帮你管理 Cookie,你得自己挂一个CookieJar。

常见做法是给 OkHttpClient 配一个内存级的 CookieJar,把每次响应的 Cookie 存下来,下次请求时按域名取出来塞进请求头。这里有个细节:Cookie 有path和domain属性,简单粗暴地全量存取在单域名场景下能用,但一旦涉及跳转多个子域就会翻车。我一般会按 host 做一层过滤,只存目标域名的 Cookie。

// 内存 CookieJar:按 host 存取,避免跨域污染 public class MemoryCookieJar implements CookieJar { private final Map<String, List<Cookie>> store = new HashMap<>(); @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) { // 只保留目标域名的 Cookie,防止跳转时串味 if (url.host().contains("chsi.com.cn")) { store.put(url.host(), cookies); } } @Override public List<Cookie> loadForRequest(HttpUrl url) { List<Cookie> cookies = store.get(url.host()); return cookies != null ? cookies : new ArrayList<>(); } }

这段代码的关键在saveFromResponse里的 host 判断。学信网登录过程会经过account.chsi.com.cn和my.chsi.com.cn等不同子域,如果不做过滤,A 子域的 Cookie 被带到 B 子域,轻则登录态丢失,重则触发风控。loadForRequest返回空列表而不是 null,是因为 OkHttp 内部对返回值做了非空假设,返回 null 会在某些版本上抛 NPE,这个坑我踩过。

2.2 登录请求的参数构成与请求头伪装

光有 Cookie 还不够。服务端会校验User-Agent、Referer、Content-Type这几个头,缺一个都可能被判定为非浏览器请求。尤其是Referer,登录接口通常要求它指向登录页本身,否则直接 403。参数方面,除了账号密码,往往还有隐藏字段(比如lt、execution这类由登录页动态生成的令牌),这些值必须先从登录页 HTML 里抓出来再回填。

// 第一步:GET 登录页,抓隐藏字段 Request pageReq = new Request.Builder() .url("https://account.chsi.com.cn/passport/login") .header("User-Agent", UA) // 伪装成移动端浏览器 .header("Accept", "text/html") .build(); Response pageResp = client.newCall(pageReq).execute(); String html = pageResp.body().string(); // 用正则抓 lt / execution 等隐藏字段(实际项目建议用 Jsoup) String lt = extractByRegex(html, "name=\"lt\" value=\"([^\"]+)\""); String execution = extractByRegex(html, "name=\"execution\" value=\"([^\"]+)\""); // 第二步:POST 登录表单 RequestBody form = new FormBody.Builder() .add("username", account) .add("password", pwd) .add("lt", lt) .add("execution", execution) .add("_eventId", "submit") .build(); Request loginReq = new Request.Builder() .url("https://account.chsi.com.cn/passport/login") .header("User-Agent", UA) .header("Referer", "https://account.chsi.com.cn/passport/login") .header("Content-Type", "application/x-www-form-urlencoded") .post(form) .build(); Response loginResp = client.newCall(loginReq).execute();

这里UA建议用移动端 Chrome 的字符串,因为学信网对移动端和 PC 端返回的页面结构不同,源码里统一按移动端处理,解析规则才对得上。extractByRegex只是示意,真实项目里 HTML 结构一变正则就废,用 Jsoup 的select更稳。_eventId=submit是表单提交的固定标记,漏了它服务端不认。登录响应如果是 302 跳转,OkHttp 默认会自动跟随,但你要在followRedirects(true)的前提下确认最终落地的 URL 是不是登录成功页,而不是又弹回登录页——后者说明账号密码或令牌有问题。

2.3 验证码:绕不开的那道坎

学信网登录在异常环境下会弹图形验证码,这是模拟登陆最容易翻车的地方。源码里的处理策略分两层:先判断登录页 HTML 里有没有验证码图片的src,有的话把图片下载下来交给人工输入,没有就直接提交。这不是什么高深方案,但胜在稳定——自动识别验证码在字符扭曲、干扰线多的场景下准确率很难保证,硬上 OCR 反而增加不确定性。

// 检测验证码并下载,交给 UI 层弹框输入 if (html.contains("captcha")) { String captchaUrl = extractByRegex(html, "id=\"captchaImg\" src=\"([^\"]+)\""); Request capReq = new Request.Builder() .url("https://account.chsi.com.cn" + captchaUrl) .header("Cookie", cookieHeaderFromJar()) // 验证码与当前会话绑定 .build(); byte[] imgBytes = client.newCall(capReq).execute().body().bytes(); // 交给 Activity 显示,用户输入后回填到表单的 captcha 字段 }

注意验证码请求必须带上当前会话的 Cookie,否则服务端生成的验证码和你提交时校验的不是同一张,必然失败。这个细节在源码里体现为cookieHeaderFromJar(),它从 CookieJar 里拼出name=value; name2=value2格式的字符串。人工输入虽然不够「自动」,但在个人工具场景下完全够用,比折腾打码平台省心。

3. 成绩页解析:从 HTML 到结构化数据

3.1 登录成功后的跳转与成绩入口定位

登录成功后,服务端一般会 302 到个人中心页。你要做的不是直接请求成绩查询页,而是先访问个人中心,确认登录态有效(页面里包含用户名或「退出」字样),再去请求四六级成绩页。直接跳成绩页在某些会话状态下会被拦截,因为服务端要求先有个人中心的访问记录。源码里的顺序是:登录 POST → 跟随跳转 → 校验个人中心 → 请求成绩页。

// 校验登录态:个人中心页包含「退出登录」即视为成功 Request homeReq = new Request.Builder() .url("https://my.chsi.com.cn/archive/index.action") .header("User-Agent", UA) .build(); String homeHtml = client.newCall(homeReq).execute().body().string(); if (!homeHtml.contains("退出")) { throw new IllegalStateException("登录态无效,检查账号或验证码"); } // 请求四六级成绩页 Request scoreReq = new Request.Builder() .url("https://my.chsi.com.cn/archive/cet/query.action") .header("User-Agent", UA) .header("Referer", "https://my.chsi.com.cn/archive/index.action") .build(); String scoreHtml = client.newCall(scoreReq).execute().body().string();

Referer在这里再次出现,成绩页对来源页有校验,直接从空白页请求会被拒。校验登录态用「退出」这个关键词是个土办法但有效,比解析复杂的 DOM 结构省事。如果站点改版把「退出」换成图标,这个判断就失效,所以更稳的做法是检查 HTTP 状态码加页面标题,源码里留了注释提示这一点。

3.2 用 Jsoup 抽取成绩字段

成绩页是服务端渲染的 HTML,表格结构相对固定。用 Jsoup 按table的行列定位,比正则可靠得多。源码里抽的是考试时间、总分、听力、阅读、写作翻译四个字段,对应表格里的固定列。

Document doc = Jsoup.parse(scoreHtml); Element table = doc.selectFirst("table.cet-table"); // 按 class 定位成绩表 if (table == null) { // 表不存在通常意味着没有成绩记录或页面结构变了 return Collections.emptyList(); } List<ScoreItem> items = new ArrayList<>(); for (Element row : table.select("tr").subList(1, table.select("tr").size())) { Elements cols = row.select("td"); if (cols.size() < 5) continue; // 跳过表头或异常行 ScoreItem item = new ScoreItem(); item.examTime = cols.get(0).text().trim(); item.total = parseIntSafe(cols.get(1).text()); item.listening= parseIntSafe(cols.get(2).text()); item.reading = parseIntSafe(cols.get(3).text()); item.writing = parseIntSafe(cols.get(4).text()); items.add(item); }

subList(1, ...)跳过表头行,cols.size() < 5是防御性判断——页面里可能混入合并单元格或空行,不跳过会数组越界。parseIntSafe处理「-」或空字符串的情况,直接Integer.parseInt遇到非数字会抛异常,成绩页里缺考或未出分的字段经常是横杠。这套解析逻辑的边界在于:一旦站点把表格换成 div 布局,selectFirst("table.cet-table")就返回 null,此时应该降级到按文本特征匹配,而不是直接崩溃。

3.3 数据落库与 UI 展示的衔接

解析出来的ScoreItem列表,源码里用 Room 存了一份本地缓存,目的是出分高峰期反复请求容易被限流,缓存能让用户离线看历史成绩。Room 的实体定义和 DAO 不复杂,关键是examTime做主键,避免重复插入。

@Entity(tableName = "score_cache") public class ScoreEntity { @PrimaryKey @NonNull public String examTime; // 考试时间作为唯一键 public int total; public int listening; public int reading; public int writing; } @Dao public interface ScoreDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void upsert(List<ScoreEntity> list); @Query("SELECT * FROM score_cache ORDER BY examTime DESC") List<ScoreEntity> queryAll(); }

OnConflictStrategy.REPLACE保证同一场考试重复查询时覆盖旧数据而不是报错。UI 层用LiveData观察 DAO 查询结果,网络请求成功后写库,界面自动刷新,不需要手动回调。这套「网络优先、缓存兜底」的模式在查分类工具里很常见,出分当天服务器压力大,缓存能显著减少无效请求。

4. 避坑排查:模拟登陆最容易翻车的五个地方

4.1 现象:登录返回 200 但后续请求全是未登录

原因通常是 Cookie 没存上,或者存了但没带对。OkHttp 的CookieJar如果返回的列表里 Cookie 的domain和请求 URL 的 host 不匹配,OkHttp 会静默丢弃。解决方法是打印loadForRequest的返回值,确认 Cookie 的 domain 字段和实际请求 host 一致;不一致就在saveFromResponse里用cookie.newBuilder().domain(url.host()).build()重写 domain。

4.2 现象:验证码明明输对了还是提示错误

原因是验证码请求和登录请求用的不是同一个会话。验证码图片的 GET 请求如果没带 Cookie,服务端会新开一个会话生成验证码,而你登录时用的是旧会话,校验自然对不上。解决方法是确保验证码请求走同一个OkHttpClient实例(共享 CookieJar),并且在请求头里显式带上当前 Cookie。

4.3 现象:成绩页解析出来是空的

先看scoreHtml里有没有「登录」字样——有的话说明登录态在请求成绩页时已经失效,多半是中间某次请求触发了会话刷新。再看表格的 class 名是否变了,站点改版是常态。解决办法是把解析逻辑里的选择器抽成常量,改版时只改一处;同时加一层「页面标题包含成绩查询」的前置校验,标题不对就直接报错而不是返回空列表。

4.4 现象:真机上请求超时,模拟器正常

模拟器走的是宿主机网络,真机走移动网络,差异通常在 DNS 和 TLS 握手。学信网的部分子域在移动网络下解析较慢,默认 10 秒超时不够。解决办法是把OkHttpClient的connectTimeout和readTimeout调到 20 秒,并开启retryOnConnectionFailure(true)。另外 Android 9 以上默认禁止明文 HTTP,虽然学信网是 HTTPS,但如果代码里混了 HTTP 的静态资源请求,需要在 manifest 里配usesCleartextTraffic或改用 HTTPS。

4.5 现象:频繁查询后账号被临时限制

这是最容易被忽视的一条。出分高峰期短时间多次请求,服务端会按 IP 或账号做频率限制,表现为返回验证码页或直接拒绝。解决办法是加请求间隔,两次查询之间至少隔 30 秒,并且把结果缓存到本地,用户重复打开 App 时先读缓存再决定是否发起网络请求。源码里在请求入口处加了一个基于System.currentTimeMillis()的时间戳判断,简单但管用。

5. 进阶:把模拟登陆封装成可复用的查询模块

5.1 用拦截器统一处理请求头与重试

前面每个请求都手动加User-Agent和Referer,代码重复且容易漏。更工程化的做法是写一个Interceptor,在chain.proceed之前统一补头,并对 302 跳转做一次登录态校验。

public class HeaderInterceptor implements Interceptor { private final String ua; public HeaderInterceptor(String ua) { this.ua = ua; } @Override public Response intercept(Chain chain) throws IOException { Request origin = chain.request(); Request req = origin.newBuilder() .header("User-Agent", ua) // 只有非登录页请求才补 Referer,避免登录页自身被覆盖 .header("Referer", origin.url().toString().contains("login") ? origin.url().toString() : "https://my.chsi.com.cn/archive/index.action") .build(); Response resp = chain.proceed(req); // 302 到登录页说明会话失效,抛出自定义异常供上层处理 if (resp.code() == 302 && resp.header("Location", "").contains("login")) { resp.close(); throw new SessionExpiredException("会话失效,需重新登录"); } return resp; } }

拦截器里对Referer做了条件判断,登录页请求的 Referer 指向自身,其他请求指向个人中心,这样既满足服务端校验又不会把登录页的 Referer 写死。SessionExpiredException是自定义异常,上层捕获后触发重新登录流程,比在每个请求后判断状态码干净得多。这个拦截器挂到OkHttpClient.Builder().addInterceptor(...)上即可,注意用addInterceptor而不是addNetworkInterceptor,前者能拿到完整的请求链信息。

5.2 验证方法:三步确认模块可用

封装完不能只看「没报错」,要按三步验证。第一步,用错误密码登录,确认抛出的是「账号密码错误」而不是「会话失效」,说明错误分类正确。第二步,登录成功后连续请求两次成绩页,第二次不重新登录,确认 Cookie 维持有效。第三步,手动清空 CookieJar 再请求成绩页,确认抛出SessionExpiredException而不是返回空数据。这三步覆盖了认证、会话、失效三个核心路径,跑通基本就能放心用。

验证项操作预期结果
错误密码用错误密码走登录流程抛出账号密码错误异常
会话维持登录后连续请求两次成绩页两次均返回成绩数据
会话失效清空 CookieJar 后请求成绩页抛出 SessionExpiredException

5.3 一个具体技巧:把登录态持久化到本地

每次打开 App 都重新登录体验很差。可以把 CookieJar 里的 Cookie 序列化到 SharedPreferences,下次启动时反序列化回填。注意 Cookie 有有效期,过期的不要恢复,否则会带着失效 Cookie 请求,反而触发风控。我一般会在恢复时过滤掉expiresAt早于当前时间的 Cookie,只保留有效的。这个技巧让二次打开 App 的查询耗时从十几秒降到两秒以内,体验差别很明显。

从那以后我每次做模拟登陆类需求,都会先把 Cookie 的存取和失效判断单独写成一个可测试的模块,再往上叠业务逻辑——血泪经验是,登录态的问题一旦混在业务代码里,排查起来就是黑匣子。希望这份拆解帮到你,源码里的注释和边界处理值得对着自己的场景再走一遍。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 5:20:03

本科生论文写作利器:千笔工具全解析

1. 本科生论文写作痛点解析第一次写学术论文的本科生往往面临三重困境&#xff1a;格式规范不熟悉、文献引用一团糟、查重降重耗时间。我指导过上百名本科生的毕业论文&#xff0c;发现90%的问题都集中在这些基础环节。格式错误导致反复修改&#xff0c;引用不规范被导师打回&a…

作者头像 李华
网站建设 2026/11/4 5:20:03

具身智能云机器人技术解析与应用实践

1. 具身智能云机器人技术解析具身智能云机器人&#xff08;Embodied AI Cloud Robotics&#xff09;是近年来机器人技术领域最具突破性的发展方向之一。简单来说&#xff0c;这种技术让机器人通过云端获取强大的计算能力和智能算法&#xff0c;就像给传统机器人装上了"云端…

作者头像 李华
网站建设 2026/11/4 5:18:50

移动端安全键盘控件开发实践与优化

1. 项目概述&#xff1a;打造安全易用的手机键盘控件在移动互联网时代&#xff0c;键盘输入是用户与设备交互的基础方式之一。secure-keyboard.js和secure-keyboard.css是一套专为移动端设计的自定义键盘控件解决方案&#xff0c;主要解决传统手机键盘在安全性和易用性方面的痛…

作者头像 李华
网站建设 2026/11/4 5:18:43

SVPWM扇区判断与七段式、五段式实现取舍

调三相永磁同步电机或者异步电机&#xff0c;只要涉及矢量控制&#xff0c;SVPWM 就是那道绕不过去的坎。很多人第一次上手的时候会觉得 FOC 的电流环、Park 变换才是"核心技术"&#xff0c;SVPWM 无非就是把 Uα、Uβ 变成三路占空比&#xff0c;调通了就行。真在台…

作者头像 李华
网站建设 2026/11/4 5:18:42

Oracle SGA调参起不来:pfile与spfile应急恢复指南

简介&#xff1a;这份文档面向 Oracle 数据库运维与开发人员&#xff0c;聚焦 SGA 参数调整后实例无法正常启动的应急排查场景&#xff0c;适合需要快速定位内存参数错误、恢复数据库可启动状态并补强故障处理思路的读者参考。压缩包共 1 个 docx 文件&#xff0c;大小约 14KB&…

作者头像 李华
网站建设 2026/11/4 5:18:32

佳能IR1133 USB打印功能开启全指南

1. 项目概述&#xff1a;这不是U盘坏了&#xff0c;是佳能IR1133“锁住了”USB打印通道你把U盘插进佳能IR1133的侧边USB口&#xff0c;机器屏幕亮了、识别出设备名称、甚至能浏览文件夹——但一进“USB打印”菜单&#xff0c;选项灰掉、提示“未启用”或干脆不显示&#xff1b;…

作者头像 李华