校园网断线自动重连:劫持判定与登录接口复现
项目速览:校园网 Portal 认证频繁断线,断线后必须人工打开浏览器重新认证。本工具在后台监测网络状态,判定断线后自动完成重新登录,支持开机自启与无窗口运行,已适配锐捷、Dr.com 两类认证系统,代码全中文注释便于其他院校二次适配。
项目名称 CampusNet-AutoLogin(已开源) 起止时间 2025.09 – 2026.12 我的角色 独立完成:需求分析、协议逆向、编码、文档 技术栈 Python、HTTP 协议分析、锐捷 / Dr.com 认证系统、Windows 计划任务 解决什么问题 校园网频繁断线,每次都要人工重新认证 最终结果 断线后自动恢复,开机自启、无窗口后台运行,适配两家主流认证系统 技术亮点 准确判断“网到底通没通”、登录失败的退避策略(避免账号被锁定)、多厂商认证系统的适配架构
宿舍的校园网有个规律:每次断线,都发生在你正在下载东西、正在开线上会、正在推代码的时候。
锐捷、Dr.com 这类 Portal 认证系统,断线之后不会自动重连,必须打开浏览器、等它跳转到认证页、点一下登录。一天要点七八次,一年下来上千次——这种事不该由人来做。
所以我写了个工具,叫 CampusNet-AutoLogin。看着简单,但里面有个核心问题值得单独讲:怎么判断"网到底通没通"。判断不准,工具就会该重连的时候不重连,不该重连的时候一直重连。
一、第一个错误:用 ping 判断网络
最初的版本很天真:ping 一个公网地址,通了就说明没事。
它错得很彻底。校园网断线之后,常见状态是:
ping 223.5.5.5 → 通
curl https://xxx → 302 跳转到 http://10.x.x.x/eportal/...
ICMP 能通,HTTP 被劫持。 因为认证系统是在网关层做拦截的——它不掐断你的 IP 层连通性,而是把所有 80/443 流量重定向到认证页面。你 ping 得越勤,越会得出"网络正常"的错误结论。
同理,DNS 也不可信。部分认证系统会接管 DNS,把任意域名解析到认证服务器,于是"能解析"也失去了参考价值。
结论:判断"通不通",必须看应用层能不能拿到预期内容,不能看网络层能不能发包。
二、正确做法:请求一个"应该返回固定答案"的探测地址
思路很简单:挑一个返回值高度确定的地址,请求它,然后看返回码。
PROBE_URL = "http://connect.rom.miui.com/generate_204"
这类地址叫 captive portal 探测地址,专门为这个场景设计:
- 正常:返回
204 No Content,body 为空 - 被劫持:返回
302,Location指向认证页;或者返回200但 body 是一坨 HTML 登录页
所以判定逻辑是三重校验,而不是只看状态码:
def is_online() -> bool:
try:
resp = session.get(PROBE_URL, timeout=5, allow_redirects=False)
except requests.RequestException:
return False
# 1. 状态码必须是 204
if resp.status_code != 204:
return False
# 2. body 必须为空(防止 200 + 劫持页)
if resp.content:
return False
# 3. 不能被重定向到内网地址
if resp.headers.get("Location", "").startswith(("http://10.", "http://192.168.")):
return False
return True
三个条件缺一不可。只判状态码会漏掉"200 + 登录页"的情况,只判 body 会漏掉"204 但被中间设备伪造"的情况。
allow_redirects=False 是关键。如果让它自动跟随重定向,requests 会默默跳到认证页,最后拿到一个 200,你就又得出"网络正常"的错误结论——和 ping 那个坑一模一样,只是换了一层。
探测地址为什么不用自己的域名
用 generate_204 这类公共地址有个隐性优势:它不依赖你的域名解析是否正常。用自己的服务器地址探测,一旦 DNS 被劫持,探测本身就失效了。而这类地址是被各大系统内置的,劫持设备一般会放行或至少行为可预期。
三、登录:把浏览器的一次点击拆成一次 HTTP 请求
判定断线之后,要做浏览器做过的事。流程是:
1. GET 认证页 → 从中提取必要参数(有的系统需要先拿 csrf/session)
2. POST 登录接口 → 带上账号、密码、以及认证页里带出来的参数
3. 校验返回 → 有的返回 JSON,有的返回一段 JS 回调,有的直接 302
4. 二次探测 → 用第一步的 is_online() 确认真的通了
第 3 步和第 4 步都很重要。
第 3 步:不同厂商返回格式差异很大。锐捷 ePortal 一般返回带回调的 JS 片段,Dr.com 可能返回 JSON。判定成功的依据不是"HTTP 200",而是返回体里的业务字段——很多系统登录失败也返回 200,只在 body 里写一句"密码错误"。如果只看状态码,会陷入"登录成功 → 实际没通 → 立刻再登录"的死循环。
第 4 步:登录接口返回成功,不等于网络已通。中间可能有几秒的生效延迟,也可能认证成功了但网关会话没建立。所以登录后要用同一套探测逻辑复验,通了才算成功。
这里有个设计上的好处:is_online() 只有一个实现,探测和复验共用。如果判断逻辑有 bug,至少是"一致地错",不会出现"探测说断线、复验说通了"这种互相矛盾的死锁。
四、重连:比"能不能连"更难的是"连的节奏"
自动登录工具真正的坑不在技术,在节奏。写错了会有两种极端:
A. 重连太慢:断线后要等 5 分钟才恢复,体验和手动点没区别。
B. 重连太频:认证系统有失败次数限制,连续失败会锁定账号 15 分钟——工具一激动,把账号锁了。
所以调度要满足三个约束:
# 1. 基础轮询间隔:够快,但不足以造成压力
POLL_INTERVAL = 30 # 秒
# 2. 失败退避:指数增长 + 上限,避免连续撞锁
backoff = min(BASE * (2 ** fail_count), MAX_BACKOFF) # 30s → 60s → 120s → ...
# 3. 成功后退避归零,但要立刻进入下一轮探测
fail_count = 0
还有两个细节容易被忽略:
防抖:探测失败一次不等于断线,可能是探测地址本身抖动。连续失败 2 次再触发登录,能过滤掉大部分误判。这个成本很低,收益很明显——误判一次登录,就多一次被锁的风险。
状态保持:登录成功后要记录"上次登录成功的时间"。如果 10 秒内又判定断线,说明不是认证问题(可能是网线拔了、交换机挂了),这时候不该继续重试登录,而应该退回去做基础连通性检查。
一个粗糙但有效的判断:
if time.time() - last_success < 60 and not is_online():
# 刚登录成功就又断,多半不是认证的事
log("疑似物理链路或上游故障,暂停重连")
return
五、开机自启与"看不见地跑"
工具要长期在后台待着,理想状态是:用户永远不知道它在跑,只在断线时发现网自己好了。
Windows 上的选择:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 启动文件夹放快捷方式 | 简单 | 会闪黑窗,登录前不启动 |
| 计划任务(触发器=登录时) | 不闪窗,可设置失败重启 | 配置略繁琐 |
.vbs 包一层调 pythonw |
一行搞定,完全无窗口 | 依赖脚本宿主,杀软可能误报 |
| 注册为 Windows 服务(NSSM) | 最稳,开机即启,不分用户 | 需要额外工具,调试麻烦 |
最后选的是计划任务 + pythonw.exe:pythonw 是 GUI 子系统的解释器,不会分配控制台窗口,从根本上避免黑窗闪一下的问题;计划任务负责"登录后自动拉起"和"进程意外退出后重启"。
Linux 下对应的是 systemd user service,思路完全一样,不展开。
六、为了"别的学校也能用",把差异抽出来
这个工具是开源的,而不同学校的认证系统差别不小:接口路径不同、参数名不同、密码是否需要额外处理、返回格式不同。
如果把这些都写死在主流程里,别人拿到只能重写。所以做了一层 provider 抽象:
class PortalProvider:
name = ""
def detect(self, html: str) -> bool: ... # 这个认证页归我管吗
def build_login(self, cfg, page): ... # 组装登录请求
def parse_result(self, resp) -> bool: ... # 返回体算不算成功
主流程只认这三个方法,不认识任何具体厂商。新增一个学校,只需要加一个 provider 文件,不动核心逻辑。
配套的是配置外置:账号密码、接口地址、探测 URL 全在配置文件里,代码里不出现任何学校特有的字符串。密码本地保存,不上传、不打日志(日志里统一打码)。
这个抽象是否值得,取决于你对"自己用"和"别人用"的定位。如果只自己用,直接写死最快;但只要打算开源,"能改"和"好改"是两个完全不同的门槛——README 写十行说明,不如代码里留一个能照着填的空。
七、复盘:三个判断
回头看,这个工具最有价值的部分不是"自动登录"这个功能,而是三个判断:
1. 判断网络状态要看应用层,不是网络层。 ping 通、DNS 能解析、TCP 能握手,都不代表你能拿到正确的网页。劫持的本质是"看起来都对,内容不对"。
2. 判断成功要看业务字段,不是 HTTP 状态码。 200 是传输层的成功,不是业务的成功。这个坑在后端开发里同样常见——接口返回 200 但 {"code": 500},只看状态码的代码会认为一切正常。
3. 自动化的难点在"什么时候做",不在"怎么做"。 把一次点击变成一次 HTTP 请求是半小时的事,把重连节奏调到既不迟钝又不锁号,是反复调参和踩坑的事。
仓库已经开源,入口在 ranqian-bit。代码全中文注释,如果你也在被同样的问题折磨,改个配置应该就能跑起来。

浙公网安备 33010602011771号