Portal Loginer自动登录校园宽带 从认证逆向到 OpenWrt 插件与 Windows 客户端

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 加密。

具体来说:

  1. 生成四个随机字符,添加到密码前面。
  2. 将结果补零到 16 字节的整数倍。
  3. 使用认证页面提供的 16 字节密钥执行 AES-128-ECB。
  4. 把最终密文转换成十六进制字符串,提交给服务器。

这里有一个很容易踩的坑。

它使用的是零填充(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-serviceWindows 后台服务、配置、凭据和调度
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 插件

GitHub 源码

下载最新 Release

适合使用 OpenWrt、KWrt 等兼容固件,并由路由器负责校园宽带接入的环境。

在 Release 中下载 .ipk 安装包,通过路由器的软件包管理页面安装。

安装后进入:

服务 → Portal 自动登录

填写校园账号、密码,保存并启用即可。

插件支持自动检查网络、认证失效后重新登录、日志和状态查看,以及在受支持环境中自动适配 OpenClash。

默认检查间隔为 30 秒,通常不需要修改其他设置。

Portal Loginer Client:Windows 桌面客户端

GitHub 源码

下载最新 Windows 安装包

目前提供 Windows x64 客户端。

下载 Release 中的 PortalLoginer-版本号-Setup.exe,安装后连接校园网络,填写账号密码并登录。

成功后即可关闭设置窗口,让程序在后台运行。

支持开机自动认证、断线恢复、托盘管理、指定 Wi-Fi、系统通知及更新检查等功能。

目前还没有进行 Windows 系统代码签名,因此首次运行可能遇到系统安全提示,请确认下载来源。

写在最后

AI时代,普通人的代码水平也真的大有可为。掌握原理比会写代码更重要,我原来绝对写不出这样的东西,而现在只需要当一个讨厌的PM就可以了。

发现 Bug 或者有改进建议,欢迎提 Issue。

也希望以后再也不用盯着那个校园宽带认证页面了。

特此记录。 (๑•̀ㅁ•́ฅ)

本文作者 : Sukanu Xian
本文采用 CC BY-NC-SA 4.0 许可协议。转载和引用时请注意遵守协议、注明出处!
本文链接 : https://blog.ssf.moe/810.html
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇