简介:这份源码面向具备一定 Android 基础的开发者,围绕模拟登录学信网查询四六级成绩这一实际场景,演示网络请求、数据解析与用户认证的完整实现思路。项目以 HttpURLConnection 或 OkHttp 提交账号密码完成登录验证,再借助 Jsoup 解析返回的 HTML 页面提取成绩信息,同时涵盖 EditText 输入、按钮事件监听、结果展示等 UI 交互,以及 HTTPS 传输、密码加密存储、INTERNET 权限声明和网络异常捕获等安全与健壮性处理。压缩包共 50 个文件,以 class 编译产物、xml 布局与配置、png 图标资源、jar 依赖库及 java 源码为主,另含 apk 安装包与 dex 文件,整体约 1.46MB,目录结构清晰,便于按模块查阅。已有 739 人学习下载,适合希望打通网络编程、数据解析与界面交互链路的开发者作为实践案例参考。
1. 从一次“查分焦虑”说起:这个 Android 模拟登录项目到底能干什么
每年四六级出分那几天,总有人抱着手机反复刷新页面,输准考证号、输密码、等验证码,手一抖还得重来。这个 Android 模拟登录查询四六级源码,干的就是把这一串手动操作搬到 App 里:用户在界面上填好账号信息,点一下按钮,程序在后台完成登录请求、拿到会话状态,再把成绩数据解析出来展示。它适合两类人——想学 Android 网络请求与登录态管理的初学者,以及需要给教务类工具做二次开发的熟手。核心关键词就是“模拟登录”,难点不在界面,而在 Cookie、请求头和重定向的处理。下面我按“能干什么、怎么跑、怎么改”的顺序,把这个项目拆开讲清楚。
2. 项目定位与技术选型:为什么用原生 Android 而不是跨平台方案
2.1 这个项目解决的真实问题
四六级成绩查询本质上是一次带登录态的表单提交。用户先访问登录页,服务器下发 Cookie 和隐藏字段,提交账号密码后服务端校验,通过则重定向到成绩页,页面里再嵌着成绩数据。手动操作时这些步骤由浏览器代劳,写进 App 就得自己维护整条链路。这个项目的价值在于把“登录—保持会话—抓取数据—解析展示”四步串成一个可复用的流程,而不是只做一个查询界面。
从工程角度看,它属于典型的“轻客户端 + 重网络逻辑”结构。界面层只负责收集输入和展示结果,真正的复杂度集中在网络层:请求头要伪装得像浏览器,Cookie 要跨请求携带,重定向要跟随但不能丢状态,返回的 HTML 还要做容错解析。很多新手写这类功能时直接上一个请求库发 POST,结果拿到的永远是登录页而不是成绩页,问题就出在会话没接上。
技术选型上,项目走的是原生 Android 路线。原因很实际:这类查询工具对包体积和启动速度敏感,原生方案依赖少、调试链路短,抓包和断点都直接落在 Java/Kotlin 层。跨平台框架虽然写起来快,但网络层的 Cookie 管理和 WebView 交互容易出现平台差异,排查成本反而更高。常见做法是网络请求用 OkHttp,HTML 解析用 Jsoup,界面用原生控件加少量自定义布局,整体依赖控制在三四个库以内。
2.2 技术栈与依赖清单
把项目拆开看,技术栈可以归成四层,每层职责清晰,替换成本也低。
| 层次 | 技术/库 | 作用 | 可替换性 |
|---|---|---|---|
| 界面层 | Android 原生 View / Activity | 输入框、按钮、结果展示 | 可换 Compose |
| 网络层 | OkHttp | 发请求、管理 Cookie、跟随重定向 | 可换 HttpURLConnection |
| 解析层 | Jsoup | 从 HTML 中提取成绩节点 | 可换正则或 JSON 解析 |
| 存储层 | SharedPreferences | 记住账号、缓存上次结果 | 可换 Room |
依赖配置通常写在模块级构建脚本里,版本号按项目实际锁定,不要随手升到最新,网络库的大版本升级经常带来 Cookie 策略变化。下面是一个典型的依赖声明片段,注释里标了每个库的用途:
dependencies { // 网络请求核心库,负责连接、请求头、Cookie implementation 'com.squareup.okhttp3:okhttp:4.9.3' // HTML 解析,用来从成绩页提取节点 implementation 'org.jsoup:jsoup:1.15.3' // 可选:日志拦截器,调试时打印请求和响应 implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3' }这里要说明参数含义:OkHttp 的版本决定了 CookieJar 的默认行为,4.x 系列需要显式配置持久化 CookieJar 才能在多次请求间共享会话;Jsoup 的版本影响选择器语法支持,老版本对部分 CSS 选择器兼容性差。日志拦截器只在调试构建里加,正式包要去掉,否则请求头里的敏感信息会进日志。
2.3 目录结构长什么样
项目按功能分包,不按类型分包,这样改一个功能时相关代码集中在一处。典型结构如下:
app/src/main/java/com/example/cetquery/ ├── ui/ # 界面层:登录页、结果页 │ ├── LoginActivity.java │ └── ResultActivity.java ├── net/ # 网络层:请求封装、Cookie 管理 │ ├── HttpManager.java │ └── PersistentCookieJar.java ├── parse/ # 解析层:HTML 转成绩对象 │ └── ScoreParser.java ├── model/ # 数据模型 │ └── ScoreItem.java └── util/ # 工具类:加密、校验 └── ParamUtil.java这个结构的好处是网络层和解析层完全独立,换数据源时只动 parse 包,换请求库时只动 net 包。新手容易把所有逻辑塞进 Activity,结果一个文件上千行,改一处牵全身,这个项目从一开始就避开了这个坑。
3. 核心链路拆解:一次模拟登录从点击到出分的完整过程
3.1 第一步:先拿登录页,把隐藏参数和 Cookie 收齐
模拟登录最容易翻车的地方,是很多人直接对着登录接口发 POST,跳过了“先访问登录页”这一步。服务端在登录页里埋了隐藏字段(常见的是流水号、时间戳、校验串),同时下发初始 Cookie,缺了这些,后续提交会被判为非法请求。所以链路第一步永远是 GET 登录页。
// 用 OkHttp 发起首次 GET,目的是拿 Cookie 和隐藏字段 OkHttpClient client = new OkHttpClient.Builder() .cookieJar(new PersistentCookieJar(context)) // 会话持久化 .followRedirects(true) // 跟随重定向 .build(); Request loginPageReq = new Request.Builder() .url(LOGIN_PAGE_URL) .header("User-Agent", UA) // 伪装成浏览器,服务端会校验 .get() .build(); try (Response resp = client.newCall(loginPageReq).execute()) { String html = resp.body().string(); // 交给解析层提取隐藏字段 Map<String, String> hidden = ParamUtil.extractHidden(html); // 此时 Cookie 已由 CookieJar 自动保存 }逻辑说明:cookieJar负责在响应到达时保存 Set-Cookie,在下次请求时自动带上;followRedirects打开是因为登录页可能先跳一次;User-Agent必须设置,很多服务端对空 UA 直接拒绝。参数上,LOGIN_PAGE_URL是登录页地址,UA建议用主流浏览器的完整字符串,不要用默认的 OkHttp 标识。extractHidden是自定义方法,用 Jsoup 选择input[type=hidden]节点,把 name 和 value 收进 Map。
这一步的验收标准很简单:打印出 Cookie 和隐藏字段,数量对得上,说明会话起点没问题。如果 Cookie 为空,先查 CookieJar 有没有正确挂载,再查响应头里有没有 Set-Cookie。
3.2 第二步:组装登录表单,注意编码和字段顺序
拿到隐藏字段后,把它们和用户输入的账号密码合并成表单体。这里有两个细节:一是密码可能需要按服务端规则做一次编码或加密,不能明文直传;二是表单字段的顺序在某些老系统里会被校验,虽然少见,但踩过一次就记住了。
// 合并隐藏字段与用户输入,构建表单 FormBody.Builder form = new FormBody.Builder(); for (Map.Entry<String, String> e : hidden.entrySet()) { form.add(e.getKey(), e.getValue()); // 隐藏字段原样带回 } form.add("username", account); // 账号 form.add("password", ParamUtil.encode(pwd)); // 按规则编码后的密码 form.add("submit", "登录"); // 部分系统校验提交按钮值 Request loginReq = new Request.Builder() .url(LOGIN_SUBMIT_URL) .header("User-Agent", UA) .header("Referer", LOGIN_PAGE_URL) // 来源页,服务端常校验 .post(form.build()) .build();逻辑说明:FormBody会自动设置application/x-www-form-urlencoded内容类型,字段按添加顺序排列;Referer头指向登录页,模拟从页面提交的行为,缺了它部分服务端会返回错误页。参数上,encode方法的具体实现取决于服务端规则,常见的是 MD5 加盐或 Base64,项目里把它单独放在工具类,方便替换。submit字段的值要和页面按钮的 value 一致,这个值从第一步的 HTML 里也能提取到,硬编码容易在页面改版后失效。
提交后不要急着解析成绩,先看响应状态码和最终 URL。如果最终 URL 还是登录页,说明登录失败,原因可能是密码编码不对、隐藏字段缺失或 Cookie 没带上。把这三项逐一打印比对,基本能定位。
3.3 第三步:跟随重定向到成绩页,解析 HTML 节点
登录成功后服务端通常返回 302,跳到成绩页。OkHttp 在followRedirects开启时会自动跟随,但要注意重定向过程中 Cookie 是否持续携带,这依赖 CookieJar 的实现质量。到达成绩页后,用 Jsoup 按选择器提取成绩行。
// 登录成功后请求成绩页(若已自动跳转,可直接解析登录响应) Request scoreReq = new Request.Builder() .url(SCORE_PAGE_URL) .header("User-Agent", UA) .header("Referer", LOGIN_SUBMIT_URL) .get() .build(); try (Response resp = client.newCall(scoreReq).execute()) { String html = resp.body().string(); Document doc = Jsoup.parse(html); // 按表格行提取,选择器要按实际页面结构调整 Elements rows = doc.select("table.score-table tr"); List<ScoreItem> list = ScoreParser.parseRows(rows); }逻辑说明:Jsoup.parse把 HTML 转成可查询的文档树,select用 CSS 选择器定位节点;parseRows负责把每行的单元格映射成ScoreItem对象,包含科目、分数、考试时间等字段。参数上,选择器是最容易失效的部分,页面结构一改就得跟着改,建议把选择器集中放在常量类里,改版时只动一处。解析时要对空节点和异常格式做判空,成绩页偶尔会返回提示信息而不是表格,直接取下标会抛异常。
3.4 第四步:登录态持久化与结果缓存
用户不希望每次打开都重新登录,所以要把 Cookie 持久化到本地。项目里用自定义 CookieJar 把 Cookie 序列化进 SharedPreferences,下次启动时恢复。同时把上次查询结果缓存一份,弱网时先展示缓存再后台刷新。
// 简化的持久化 CookieJar:保存与加载 public class PersistentCookieJar implements CookieJar { private final Map<String, List<Cookie>> store = new HashMap<>(); @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) { store.put(url.host(), cookies); persist(); // 写入 SharedPreferences } @Override public List<Cookie> loadForRequest(HttpUrl url) { List<Cookie> cookies = store.get(url.host()); return cookies != null ? cookies : new ArrayList<>(); } }逻辑说明:saveFromResponse在每次响应后触发,按域名分组保存;loadForRequest在每次请求前调用,把对应域名的 Cookie 带回。参数上,persist和restore是自定义方法,负责序列化,注意 Cookie 有过期时间,加载时要过滤掉已过期的项,否则会带着失效会话发请求,表现为“明明登录过却一直跳登录页”。缓存结果建议加时间戳,超过一定时长就强制刷新,避免展示过期成绩。
4. 避坑与排查:模拟登录最容易翻车的五个地方
4.1 现象:请求一直返回登录页,状态码却是 200
原因通常是会话没接上。要么 CookieJar 没挂载,要么首次 GET 被跳过导致隐藏字段缺失,要么重定向过程中 Cookie 丢失。解决顺序是:先打印每次请求带的 Cookie 和响应 Set-Cookie,确认会话在流转;再检查是否先访问了登录页;最后确认followRedirects和 CookieJar 同时生效。这三步能覆盖九成情况。
4.2 现象:登录接口返回参数错误,但字段看着都对
多半是编码问题。密码做了双重编码,或者表单字段被 URL 编码了两次。FormBody会自动编码,手动再URLEncoder一次就会出错。排查方法是抓一次浏览器真实请求,对比字段值的原始形态,确认编码只发生一次。另外注意字符集,中文验证码或提示信息要用 UTF-8 读取响应体。
4.3 现象:解析成绩时抛空指针或数组越界
页面结构和选择器对不上。服务端改版、返回错误提示页、或者登录态失效返回登录页,都会让选择器匹配不到预期节点。解决方法是解析前先判断页面特征,比如是否包含成绩表格的标识节点,不包含就走错误分支并提示用户重新登录,而不是硬解析。选择器本身也要写得宽松些,避免依赖过深的层级。
4.4 现象:调试包正常,正式包登录失败
常见于日志拦截器和网络配置的差异。调试包可能走了明文流量或代理配置,正式包收紧后请求被拦。检查网络安全配置是否允许目标域名,确认正式包没有误删必要权限。另外混淆规则如果没保留网络库和解析库的类名,反射相关逻辑会失效,表现为运行时找不到类。
4.5 现象:频繁请求后被限制访问
模拟登录本质是自动化请求,频率过高会触发服务端风控。解决方法是加请求间隔、失败重试要退避、不要在短时间内重复提交登录。缓存结果也能减少请求次数。这类限制属于服务端策略,客户端只能通过降低频率和规范请求头来减少误判,不要试图绕过。
5. 二次开发改造:把查询逻辑抽成可复用模块的三个技巧
拿到这份源码后,最值得做的改造不是换界面,而是把网络和解析逻辑抽成独立模块,让它可以被其他页面或其他项目复用。我一般分三步走。
第一步,定义查询接口,把“登录 + 查询”收敛成一个方法。界面层只依赖接口,不关心内部用的是 OkHttp 还是别的库。这样换请求实现时,界面代码一行不用动。
public interface ScoreQuery { // 登录并查询,回调返回结果或错误 void loginAndQuery(String account, String password, Callback<List<ScoreItem>> cb); }第二步,把选择器和接口地址集中到配置类。页面改版时只改配置,不改逻辑。配置类里同时放超时时间、重试次数、UA 字符串,这些参数调优时经常动,散落在各处会很难维护。
第三步,给解析层加单元测试。用几份保存下来的 HTML 样本做输入,断言解析出的成绩条数和字段值。这样页面改版后,跑一遍测试就知道选择器有没有失效,比手动点 App 快得多。测试样本要注意脱敏,不要带真实账号信息。
验证改造是否成功,看两个指标:换一个数据源时改动的文件数是否控制在配置类和解析类;界面层是否完全不出现网络库的类名。做到这两点,这个项目就从“一个查询 Demo”变成了“一套可复用的登录查询骨架”。
血泪经验是,我早期做类似功能时把 Cookie 管理写在 Activity 里,结果页面一重建会话就丢,排查了半天才发现是生命周期问题。从那以后我每次做模拟登录,都强制把会话管理放在独立模块,并且先写一个最小请求脚本验证链路通不通,再往界面里接。希望帮到你。
本文还有配套的精品资源,点击获取