SJZU的宿舍区宽带是交由中国移动来做的,官方给的口径是每个账号限制两个设备,每次使用都需要连接宿舍的公用WiFi然后登录认证。

短则24小时,长则48小时,就会自动脱离登录状态,需要重新登录。打开浏览器,弹出一个认证页面,输入账号密码,登录成功,然后才能正常上网。
这真是够难受的,设备一多还要互相顶号。
打开热点测试了一下,似乎并没有过多在限制设备上做文章。
于是产生了一个非常朴素的想法:
想办法让登录这件事变得自动化。
于是就有了 Portal Loginer。
最初只是想给 Windows做一个自动登录插件。后来随着AI发展,既然认证逻辑已经搞明白了,干脆再写一个 OpenWrt版本,让其他设备都连上网,顺便还可以整体富强。最后感觉独乐乐不如众乐乐,于是写了一个Windows版本的自动登录,分享给可能需要的同学。
本来只是想少点几下鼠标,结果又给自己找了一堆事情干。
目前两个项目都已经开源:
两个项目基于沈阳建筑大学移动校园宽带的认证系统开发,其他学校是否适用需要单独验证。
本文主要记录它们背后的认证原理、协议分析、实现过程,以及一些实际折腾中踩过的坑。
校园宽带认证到底是怎么回事?
我们平时在家里连接 Wi-Fi,路由器通过 DHCP 获取地址或者通过 PPPoE 拨号,成功后一般就能直接访问互联网。
校园网络通常有一个接入控制系统(AC),配合 Portal 认证机制管理用户。
设备接入网络后,可能已经拿到了 IPv4 地址、网关和 DNS,却仍然处于未认证状态。
这时如果向一个普通 HTTP 网站发起请求,网络侧可能不会把请求正常转发到目标网站,而是返回一个重定向响应,把浏览器引导到校园认证页面。
一个典型的过程是:
设备连接校园网络 → DHCP 获取地址 → 访问 HTTP 网站 → AC 返回认证重定向 → 打开 Portal 页面 → 输入账号密码 → 认证成功 → 放行网络。
如果是 HTTPS 网站,那么恭喜你,弹出这个窗口将再费劲一点。这个体验是非常差劲的。
从浏览器入手,分析登录协议
打开认证页面,按 F12 进入开发者工具,在 Network(网络)中观察认证过程中发出了哪些请求。
重点不是页面长什么样,而是:
- 认证页面从哪里来?
- 用户账号、密码提交到了哪个接口?
- 提交前密码是否经过处理?
- 登录成功的依据是什么?
- 有没有额外的确认请求?
实际实现中最有价值的是下面几个环节。
1. HTTP 302:找到认证入口
当设备还没有通过校园认证时,访问 HTTP 探测地址,接入控制系统可能返回类似这样的响应:
HTTP/1.1 302 Found
Location: http://192.168.199.200/index.html?bras=xxx&clientip=10.x.x.x&clientmac=xx:xx:xx:xx:xx:xx&vlan=xxx
(以上为脱敏后的示意,并非可直接使用的登录地址。)
注意这个 Location。
它不仅告诉浏览器应该跳转到哪里,还携带了一系列与当前接入会话有关的参数。
例如:
| 参数 | 作用 |
|---|---|
| bras | 接入认证相关的设备或标识 |
| clientip | 当前客户端 IPv4 地址 |
| wlanuserip | 无线用户地址信息 |
| wlanacname | 无线接入控制器相关标识 |
| clientmac | 客户端 MAC 地址 |
| paip | 认证系统使用的附加地址信息 |
| vlan | 当前接入网络的 VLAN 信息 |
这些参数并不适合简单写死。
换一个网络、重新获取 IP,甚至接入到不同的认证设备后,它们都可能发生变化。
所以真正可靠的自动登录程序,应该从当前认证重定向中动态获取参数,而不是把某一次抓到的 URL 永久保存下来。
这也是整个项目中最基础、但非常重要的一部分。
2. 密码不是直接提交的
继续观察登录请求,可以发现提交给服务器的密码并不是用户输入的原始密码。
认证页面中的 JavaScript 使用了 CryptoJS 进行处理。
继续沿着加密函数和相关脚本分析,可以还原出这个校园认证系统使用的密码处理方式:
随机前缀 + 原始密码 + 零填充 + AES-128-ECB 加密。
具体来说:
- 生成四个随机字符,添加到密码前面。
- 将结果补零到 16 字节的整数倍。
- 使用认证页面提供的 16 字节密钥执行 AES-128-ECB。
- 把最终密文转换成十六进制字符串,提交给服务器。
这里有一个很容易踩的坑。
它使用的是零填充(Zero Padding),不是很多加密库默认使用的 PKCS#7 Padding。
如果只是调用一个标准 AES 接口,不检查填充规则,得到的密文可能完全不同。
另外,原协议还有一项特殊兼容逻辑:符合条件的 32 位字母数字 token 可以直接提交,不再进行上述 AES 处理。
所以实现时还需要区分普通明文密码和已有 token。
3. 密钥从哪里来?
认证页面的 JavaScript 中存在相关的密钥初始化逻辑,典型形式是:
CryptoJS.enc.Utf8.parse("这里是16位密钥")
最早当然可以直接提取密钥,写到程序里。
但是这样做会带来另一个问题:如果校园网更新了认证页面,修改了密钥怎么办?
程序很可能会在某一天突然无法登录。
于是项目中又加入了从认证页面相关脚本中识别密钥的机制。
程序会按顺序检查几个已知的 JavaScript 路径,尝试寻找符合当前协议格式的密钥定义。
识别成功后使用新的密钥;没有识别到时,保留已有配置,不随便拿某段恰好为 16 位的字符串当作密钥。
需要说明的是,这不是一个能够理解任意 JavaScript 的通用反混淆器。它针对的是当前认证页面中已知的密钥表达形式。
如果认证系统以后改变了加密方式,仍然需要重新分析。
4. 登录居然不止一个请求
把密码算出来还不够。
根据实际协议和项目实现,完整认证事务大致是:
获取认证参数 → 访问 Portal 页面建立 Cookie → POST 登录接口 → POST 确认接口 → 再次检测网络。
其中两个比较重要的接口是:
/api/login.php:提交登录请求。/api/ack_auth.php:执行认证确认。
它们使用同一认证会话中的 Cookie 等信息。
登录接口存在多个兼容的成功返回值,例如 0、121、122 和 3;确认接口则需要单独检查其返回结果。
但是这还不是全部。
认证服务器告诉你成功,不代表互联网一定已经恢复。
例如登录接口返回成功,确认请求也成功了,但实际上网络仍然无法访问。比如获取的登录设备参数有误。
因此程序在完成认证后,还会重新进行联网探测。
只有探测取得可靠的在线结果,才真正将状态切换为已登录。
怎么知道校园网真的在线了?
既然想实现无人值守的自动登录,就需要知道什么时候应该登录。
最直觉的方式是定时访问某个网站。
但是实际实现起来,比想象中麻烦。
1. 为什么使用 HTTP 204?
项目默认采用小米的联网检测地址:
http://connect.rom.miui.com/generate_204
正常情况下,它会返回:
HTTP 204 No Content
也就是请求成功,但没有响应正文。
这种地址很适合检测联网状态,因为返回结果相对明确,也不需要下载完整网页。
对于当前校园网环境,可以将一次可靠的直连 HTTP 204 作为网络已经放行的证据。
而在未认证时,AC 可能拦截请求,返回指向 Portal 页面的 HTTP 302。
于是就形成了两种很有意义的结果:
204:确认在线。
有效的 Portal 重定向:需要认证。
不过,还有第三种情况。
2. 超时不代表掉线
假设校园网其实一直正常,但检测网站突然抽风,或者代理节点暂时连接不上。
如果程序的逻辑只是:
“请求失败?那一定是校园网掉登录了,赶紧重新登录!”
这显然不合理。
甚至可能导致反复注销、认证,最后把原本正常的网络折腾坏了。
所以在后来的版本中,我把网络判断区分得更加明确。
只有确认当前认证系统返回了有效的 Portal 重定向,才将它视为需要登录的证据。
如果只是 HTTP 超时、500 错误、普通网页跳转或者其他无法判断的情况,就进入探测未确认状态。
这种情况下保留现有会话,稍后再检查,而不是马上重新登录。
为了避免单个检测地址出现问题,项目现在还有华为和 vivo 的备用探针。
每次先检测主探针,结果不明确时再依次尝试备用地址。只要得到了可靠结论,就结束本轮探测。
正常情况下不会每次都发送三个请求。
3. 自动恢复也不能无限重试
如果校园账号密码真的错误,或者认证服务器出现故障,程序总不能每隔几秒就疯狂提交一次登录。
所以项目采用了指数退避机制。
例如初始重试间隔为 10 秒:
10 秒 → 20 秒 → 40 秒 → 80 秒 → 160 秒 → 最多 300 秒。
这样既能够及时处理短暂的异常,也不会在长时间故障时不断给认证服务器施加压力。
至此,一个能够无人值守运行的认证状态机就基本成形了。
先把它放进路由器:Portal Loginer
第一个落地的项目是 OpenWrt / KWrt 插件。
我希望它能直接运行在路由器上,因为这样整个局域网就不需要每台电脑单独处理校园认证了。
只要负责连接校园宽带的路由器能够保持认证,后面的设备就可以正常使用网络。
插件没有单纯使用一个定时执行的 Cron 脚本,而是做成了常驻的后台服务。
主要由几部分组成:
- Shell:负责网络检测、认证协议和状态控制。
- procd:负责后台服务的生命周期管理。
- UCI:保存用户配置。
- rpcd:提供设置、状态和操作接口。
- LuCI:提供可视化管理页面。
- hotplug:配合 WAN 接口变化处理服务启动。
用户只需要在 LuCI 页面填写账号和密码,启用自动登录即可。
1. 路由器刚开机时,网络其实没准备好
这是一个非常有意思的问题。
我使用的校园宽带接入环境中,路由器上游还有网络设备。
当光猫和路由器同时启动时,路由器系统可能已经运行,WAN 却还没有得到 DHCP 地址。
甚至可能出现有了地址,但默认路由还没有准备好的情况。
如果程序这时候就开始请求登录接口,结果当然是各种失败。
因此在认证之前,程序会检查 WAN 接口当前的实际状态,包括 IPv4 地址和路由是否就绪。
网络发生变化后,还会等待一段稳定时间。
如果发现客户端 IP 已经改变,就不能直接拿旧会话缓存里的认证参数继续登录。
旧 IP 对应的认证信息,不一定能用在新 IP 上。
这里还遇到过开机时系统校时的问题。
路由器刚启动,系统时间可能还不准确,等 NTP 同步完成后,时间会突然跳变。
如果拿系统日期时间计算“已经等待了多久”,就可能出现莫名其妙的结果。
所以这类稳定等待、运行耗时等逻辑改成了使用系统的单调运行时间。
2. 最麻烦的居然是 OpenClash
本来觉得登录协议搞定后,这个项目应该就差不多了。
结果 OpenClash 给我上了一课。
我日常在路由器上使用 OpenClash,开启了透明代理和相关 DNS 处理。
这就产生了一个非常尴尬的问题:
校园网认证需要联网,而代理本身又需要校园网先认证才能正常联网。
如果登录检测请求也被 OpenClash 转发到代理节点,事情就可能变得很奇怪。
例如:
校园网实际上已经认证成功,但代理节点出现故障。
此时探针请求失败,程序可能误以为校园网掉线。
反过来,如果校园网已经掉线,认证请求又被送进不可用的代理,自动登录就会陷入死循环。
更麻烦的是,单纯使用 curl --noproxy '*' 并不能解决所有问题。
这个参数可以绕过普通的 HTTP/SOCKS 显式代理,却不能保证绕过由防火墙规则实现的透明代理。
OpenClash 的 Fake-IP DNS 处理也可能影响认证请求。
3. 不修改 OpenClash,也能实现独立认证
早期尝试过使用 DSCP 标记、直连规则和额外的 DNS 配置解决问题。
确实可以在特定环境中工作。
但这种方式存在明显缺点:插件需要用户手动配合修改 OpenClash,而且不同版本、不同固件和不同防火墙环境不一定兼容。
从 1.4.1 开始,改成了另一种方案。
OpenClash 本身就有让特定本机进程流量绕过代理的机制。
项目会先检查实际防火墙规则是否支持相应的 GID 绕过方式,再通过独立的网络请求子进程执行探测和认证。
这里使用的是 GID 65534。
只让负责网络请求的子进程切换到对应进程组,主服务本身不需要改变权限身份。
同时,认证程序还会独立查询 DNS,优先使用 WAN 上游 DNS,并过滤无效地址和 Fake-IP。
拿到真实 IPv4 后,再让 curl 连接这个地址,同时保留原始域名及必要的 Host、TLS 验证行为。
这样,认证请求不需要依赖 OpenClash 当前选择了哪个节点,也不需要自动修改用户的 OpenClash 订阅、代理规则或者 DNS 配置。
但前提是当前 OpenClash 和防火墙确实具备所需的绕过能力。对于不支持的环境,程序会提示异常,而不是假装已经绕过代理。
既然路由器能用,为什么不做一个电脑客户端?
后来又冒出了一个想法。
既然认证协议已经实现,为什么不把它移植到 Windows 和 macOS?
不是每个人都使用 OpenWrt。
很多人只是拿着笔记本连接校园网,仍然需要打开浏览器登录。
如果能做成一个开机自动认证、平时不需要操作的小程序,应该会方便很多。
但我不太想做那种为了一个登录功能,就需要常驻一个完整浏览器内核的客户端。
我的目标很简单:
轻量、稳定、没有存在感。
所以桌面版没有直接套用一个 Electron 外壳,而是选择使用 Rust 重新实现认证核心。
1. 从 Shell 到 Rust,不是复制代码那么简单
OpenWrt 和 Windows 的运行环境差异实在太大。
OpenWrt 依赖 UCI、ubus、procd、Shell、curl 和系统防火墙规则。
Windows 则有自己的网络接口、服务管理器、用户权限和凭据保护机制。
所以这个项目并不是把原来的 Shell 脚本塞进一个窗口里运行。
而是保留认证协议与状态机行为,重新实现平台相关部分。
目前 Rust 项目主要分成四个模块:
| 模块 | 主要职责 |
|---|---|
| portal-core | 协议解析、密码处理、网络探测和认证状态机 |
| portal-service | Windows 后台服务、配置、凭据和调度 |
| portal-desktop | 托盘、设置界面和系统通知 |
| portal-update | 更新检查、文件下载与完整性校验 |
这样做最大的好处,是把校园认证逻辑和操作系统相关代码分离。
未来适配 macOS 时,认证协议本身不需要全部重写,主要需要实现对应的平台服务、网络信息获取、凭据存储和用户界面等部分。
当然,这只是架构上方便迁移,不代表 macOS 已经完成。
2. 真正的无感运行,需要后台服务
我不希望每次开机还得等用户登录 Windows、打开客户端,才能开始认证。
所以 Windows 版本使用独立的系统后台服务。
服务可以随系统启动,并在网络就绪后进行检测和认证。
托盘和设置窗口只是与后台服务交互的界面。
换句话说:
设置窗口关闭了,不代表认证服务停止了。
托盘程序也不需要自己承担完整的认证工作。
后台监听网络变化、Wi-Fi 状态、路由以及睡眠唤醒等事件,在适当的时候触发认证。
为了尽量降低日常资源占用,认证逻辑使用事件驱动和定时检查,不需要让设置窗口一直运行。
正常在线状态下也会减少不必要的日志写入。
3. 密码怎么办?
桌面版使用 Windows DPAPI 保护保存的密码,并配合文件访问权限控制。
4. 还有一个问题:开机时可能根本没有网络
程序可以在 Windows 登录前启动后台服务,但不意味着 Wi-Fi 一定已经成功连接。
因此它不会主动接管 Wi-Fi 连接,而是在系统已经建立网络连接、取得地址和路由之后再尝试认证。
还可以设置只在指定 SSID 下进行认证,避免连接家里 Wi-Fi 或其他网络时也发送校园认证请求。
对于 TUN 和各种透明代理,Windows 上没有与 OpenClash 完全等价的通用绕过方式。
所以桌面版不承诺能够绕过所有 VPN 或 TUN 流量接管,遇到无法确认的环境仍然需要谨慎处理。
自动更新也折腾了不少
Windows 客户端还有一个我很在意的功能:自动更新。
本来把安装包放在 GitHub Releases 上就可以了。
但实际使用时,国内校园网络访问 GitHub 的表现并不总是理想。
如果认证客户端自己还需要用户开代理才能更新,那体验多少有些别扭。
所以现在采用了 GitHub API 镜像配合文件镜像的方案。
程序首先通过镜像读取项目 Release 信息,获取版本和文件的大小、SHA-256。
随后会对候选下载镜像进行小片段测速,选择合适的来源下载更新包,并验证下载结果。
更新过程支持失败恢复和版本回滚,尽量保留已有账号、密码与用户设置。
这里需要特别说明:
SHA-256 可以检查下载内容是否与取得的摘要一致,但如果摘要本身也是由不可信镜像提供,就不能单凭它证明文件来源绝对可信。
项目发布
Portal Loginer:OpenWrt / KWrt 插件
适合使用 OpenWrt、KWrt 等兼容固件,并由路由器负责校园宽带接入的环境。
在 Release 中下载 .ipk 安装包,通过路由器的软件包管理页面安装。
安装后进入:
服务 → Portal 自动登录
填写校园账号、密码,保存并启用即可。
插件支持自动检查网络、认证失效后重新登录、日志和状态查看,以及在受支持环境中自动适配 OpenClash。
默认检查间隔为 30 秒,通常不需要修改其他设置。
Portal Loginer Client:Windows 桌面客户端
目前提供 Windows x64 客户端。
下载 Release 中的 PortalLoginer-版本号-Setup.exe,安装后连接校园网络,填写账号密码并登录。
成功后即可关闭设置窗口,让程序在后台运行。
支持开机自动认证、断线恢复、托盘管理、指定 Wi-Fi、系统通知及更新检查等功能。
目前还没有进行 Windows 系统代码签名,因此首次运行可能遇到系统安全提示,请确认下载来源。
写在最后
AI时代,普通人的代码水平也真的大有可为。掌握原理比会写代码更重要,我原来绝对写不出这样的东西,而现在只需要当一个讨厌的PM就可以了。
发现 Bug 或者有改进建议,欢迎提 Issue。
也希望以后再也不用盯着那个校园宽带认证页面了。
特此记录。 (๑•̀ㅁ•́ฅ)


