校园网POST自动认证:抓包还原、断线重连与守护进程
校园网认证这件事,说小很小,说烦是真烦。每天打开电脑,浏览器弹出一个认证页面,输入账号密码,点登录,一天掉线三五次,每次都得重来一遍。如果手上还有一台常年开着的开发机、一台树莓派、几台虚拟机,那这套动作一天要重复十几次。这篇内容就是把“手动点登录”这件事彻底自动化掉——核心手段就是用 POST 请求复现认证流程,让脚本代替人去填表、提交、并在掉线后自动重连。关键词很明确:校园网、POST 自动认证、断线重连、守护进程。
它解决的不是什么高深问题,而是“高频、低价值、必须人肉介入”的重复劳动。适合三类人看:一是宿舍里被认证页面折磨到崩溃的普通用户,照着步骤能跑起来就行;二是想借这个场景练手的初学者,Portal 认证是一个极好的 HTTP 抓包与协议还原练习,比看教程敲一遍“爬虫入门”有用得多;三是已经在做运维、嵌入式、自动化相关工作的朋友,里面关于守护进程、日志脱敏、开机自启那几段可以直接搬走用。下面我按“搞懂它怎么运作 → 抓出关键参数 → 写出可跑的脚本 → 让它稳定活下来 → 出问题怎么查”这条线,把整个过程摊开讲。
1. 先把认证流程拆明白再动手
1.1 一次手动登录背后到底发生了什么
很多人一上来就去抓包,抓完发现一堆参数看不懂,卡在这里就放弃了。我的建议是先别急着开工具,先在脑子里建立一个模型:你点“登录”的那一瞬间,浏览器至少完成了三件事——获取认证入口地址、拼装表单参数、向网关提交并接收结果。这三件事对应三段不同的网络行为,抓包时要把它们分清楚,否则会把“跳转”当成“提交”,把“页面渲染”当成“接口返回”。
具体来说,连上校园 Wi-Fi 或网线后,你的设备拿到一个内网地址。此时访问任何外部站点,网关并不会直接放行,而是把你的请求拦下来,返回一个重定向,把你引导到认证服务器上的一个页面。这个页面的地址里通常带着一串查询参数,比如你的终端 IP、接入设备的标识、原始访问的目标地址等等。这些参数就是后续提交时要用到的“上下文”,丢一个都可能导致认证失败。这就是为什么很多人直接抄别人的脚本跑不通——别人的 IP 和你不一样,接入设备标识也不一样。
认证页面加载完之后,里面嵌着一个表单,表单的提交地址往往和页面地址不是同一个域名或路径。你填账号密码点提交,浏览器把表单数据以 POST 形式发到这个地址,网关校验通过后返回一段结果(可能是 JSON,也可能是跳转到一个成功提示页),同时在你这个 IP 上打一个“已认证”的标记,之后你的流量才被放行。理解了这个“先重定向、再提交、后放行”的三段式结构,抓包时你就知道该盯哪几个请求了。
这里有个很容易被忽略的点:认证状态是绑定在 IP(有时还叠加 MAC)上的,不是绑定在你的设备身份上。这意味着两件事。第一,你换一个 IP(比如从 Wi-Fi 切到有线、或者 DHCP 重新分配)就需要重新认证一次。第二,如果同一网段里有人手动填了和你一样的 IP 配置,会导致双方都认证不上或者互相踢下线,这是宿舍里常见的“玄学掉线”原因之一。知道这一点,后面写断线检测逻辑时你就明白,光判断“网络通不通”是不够的,得判断“我这个 IP 的认证标记还在不在”。
1.2 三条技术路线,为什么最后选了模拟 POST
自动化认证这件事,业内有三种常见做法,各有各的适用场景,我在不同阶段都用过,这里做个横向对比,方便你按自己的情况选。
| 路线 | 实现方式 | 资源占用 | 稳定性 | 维护成本 | 适用情况 |
|---|---|---|---|---|---|
| 模拟 POST 请求 | 用 HTTP 客户端直接提交表单 | 极低,几 MB 内存 | 高,但惧怕接口变更 | 中,接口变了要重抓 | 绝大多数场景首选 |
| 浏览器自动化 | 无头浏览器加载页面并点击 | 高,几百 MB | 中,页面改版即失效 | 高 | 页面有复杂 JS 加密或验证码 |
| 图形界面模拟 | 模拟鼠标键盘点击 | 中 | 低,窗口焦点一变就崩 | 高 | 实在没办法时的兜底 |
第一种是首选,因为它最轻、最快、最容易做成后台常驻。缺点也很直白:一旦认证页面改版、参数增减、加密方式变化,脚本立刻失效,需要重新抓包调整。第二种的好处是“所见即所得”,页面上人怎么点,脚本就怎么点,对 JS 加密、动态渲染的兼容性好得多,代价是资源占用高,跑在宿舍那台常年开机的旧笔记本上还行,跑在树莓派上就有点吃力,而且无头浏览器在很多环境里会因为缺少图形依赖而启动失败。
第三种我是真不推荐,除非前两条路都走死了。它的脆弱程度超出想象:系统弹个通知、分辨率变一下、输入法切一下,坐标就全偏了。而且它必须依赖一个可见的桌面会话,远程或者无人值守时完全没法用。
我最终落在第一条路上,还有一个很实际的原因:校园网认证接口通常极其简陋。它不需要考虑多终端适配、不需要考虑无障碍、不会有复杂的风控,所以前端那点加密手段往往就是一层简单的哈希,看一眼 JS 就能还原。投入产出比高得离谱。反过来说,如果你抓包后发现密码字段是一长串看不懂的密文,且 JS 被严重混淆,那再考虑退到第二条路。
注意:这套自动化的前提是使用你自己的账号、你自己名下的终端设备。把自己的账号密码交给脚本去跑,本质上和你在浏览器里手动输入没有区别,但请不要把脚本分享给他人共用账号,也不要把凭据硬编码后推到公开仓库,这是最容易被忽略的安全问题。
1.3 方案的整体骨架长什么样
在正式动手前,先把最终形态想清楚,避免写到一半推翻重来。我的脚本整体是四层结构:参数采集层负责在未认证状态下捕获重定向,拿到 IP、接入设备标识等上下文;认证执行层负责拼装表单并提交,解析返回结果;状态检测层负责周期性判断当前是否在线;守护调度层负责串联前三层,处理异常、记录日志、控制重试节奏。
分层的好处是每一层都能独立调试。新手最容易犯的错是把所有逻辑塞进一个大函数里,一旦失败,根本不知道是参数取错了、提交格式错了、还是判断在线状态的逻辑本身有 bug。分成四层之后,你可以单独测“参数采集”,打印出来和人家的成功请求对比;也可以单独测“状态检测”,手动断网看它能不能正确识别。这种可拆解性在排查问题时价值极大。
另外提前说清楚一个设计取舍:认证和检测要共用同一个会话对象。有些网关会在认证成功后下发 Cookie,后续的状态检测如果用一个全新的会话来发请求,可能被当成未认证的新客户端,从而误判掉线,触发无意义的重连。共用会话还能复用底层的 TCP 连接,减少握手开销,在检测频率较高时(比如每 10 秒一次)这个差别是能感知到的。
2. 抓包还原:把认证参数一个一个抠出来
2.1 抓包前的环境准备
工欲善其事。抓包这一步我建议在电脑上做,不要在手机或开发板上做,因为你需要看完整的请求头、请求体、响应头和响应体。浏览器自带的开发者工具就够了,不需要额外装软件。打开方式是按 F12 或者右键检查,切到“网络”面板,勾上“保留日志”和“停用缓存”两个选项——这两个勾非常关键,不勾的话页面一跳转,之前的请求记录就全被清空了,你会对着空白面板怀疑人生。
准备工作还有一个容易被忽略的细节:先把当前认证状态清掉。如果你现在是已认证状态,访问任何网站都直接通,根本不会触发重定向,自然抓不到认证流程。正确做法是找到认证页面主动点一次“注销/下线”,或者干脆等它自然超时。有些网关的注销入口藏得很深,可以先访问认证服务器的根路径试试。
然后清空网络面板的记录,点一次注销,再随便访问一个外部站点,看着页面被重定向到认证页,这时候网络面板里就会出现完整的一串请求。整个过程保持面板打开,不要刷新页面。我第一次做的时候就是因为中途刷新了一下,日志清空,白忙活二十分钟。
工具方面,浏览器开发者工具适合看单次请求的细节;如果你需要看多次重连之间的差异、或者想批量比对参数变化,那就需要一个能保存请求记录的工具,比如把请求导出成 HAR 文件,或者用命令行工具把请求完整打印出来。我在调参阶段习惯把每次请求的完整 URL 和请求体写进一个日志文件,方便事后 grep 对比,这个习惯帮我定位过好几次“某个字段悄悄变了”的问题。
2.2 从重定向里提取上下文参数
第一段要抓的是重定向。你在未认证状态下访问一个外部地址,比如某个常见的公共 DNS 探测页,网关会返回一个 302,响应头里的 Location 字段就是认证页面的完整地址。这个地址长这样(参数名做了抽象处理):
http://认证服务器地址/portal页面?userip=10.x.x.x&wlanacname=AC-XXX&nasip=10.x.x.x&wlanparameter=xxxx&url=http://你原本要访问的地址这里面的每一个参数都不是摆设。userip是你的终端在校园网内网里的地址,网关靠它定位“给谁放行”;wlanacname和nasip标识你连的是哪个接入设备,多校区、多楼栋的校园网里这个值千差万别,硬编码必挂;wlanparameter通常是加密过的会话标识,有时效性,隔一段时间就失效,不能缓存复用。
我的做法是:不硬编码任何一个参数,而是每次认证前先主动发一次请求,故意让它被重定向,然后从 302 响应里用正则把这几个参数抽出来。这样即使换了楼栋、换了设备、IP 变了,脚本也能自适应。这一步是整个方案健壮性的基石,比后面写多漂亮的代码都重要。
具体实现上有个坑:Python 的 requests 默认会自动跟随重定向,你拿到的就是最终页面,看不到中间的 302。所以要么在请求时把allow_redirects设为False,手动读headers['Location'];要么关闭自动重定向后自己决定下一步。我一开始就是被这个默认行为坑了,打印出来全是 HTML,找不到 Location 头,还以为是网关不返回重定向。
提示:如果你发现访问外部地址时根本没有 302,而是直接返回了一个认证页面的 HTML,说明这个网关用的是另一种拦截方式(返回 200 但内容是认证页)。这时候就从 HTML 里用正则提取参数,思路是一样的,只是提取源从响应头变成了响应体。
2.3 表单提交接口与字段含义
第二段要抓的是真正的提交请求。在认证页面上填好账号密码,点登录,网络面板里会多出一条 POST 请求。这条请求有几个必须记录下来的信息:请求 URL(提交的目标地址,通常和页面地址不同)、Content-Type(多半是表单编码,少数是 JSON)、请求体的完整字段列表、以及响应体的格式。
请求体里的字段可以分成三类,理解分类能帮你在接口变更时快速定位问题。第一类是身份类:账号、密码(或者密码的哈希)。第二类是位置类:终端 IP、接入设备标识、接入设备 IP,这些基本都来自上一步提取的上下文,是动态的。第三类是装饰类:加密方式标记、客户端类型、版本号、时间戳、随机数、回调函数名等等,这些字段看着没意义,但缺一个就可能被拒绝,必须原样带上。
我见过有人为了“精简”请求,把看起来没用的字段一个个删掉试,最后发现某个叫jsVersion的字段是必须的。这种逆向式的精简在接口稳定的情况下可以省点事,但在调试阶段纯属浪费时间。我的建议是先原样照抄,跑通了再谈删减。跑通之后你甚至可以把每个字段单独去掉试一次,记录哪些是必需的,做成一张表放着,以后接口变更时能快速判断哪些字段是新增的关键项。
响应体的格式需要特别关注,因为它决定了你的成败判断逻辑。常见有两种:一种是返回 JSON,里面有个明确的成功标志位,比如{"result": "ok", "msg": "认证成功"};另一种是返回 HTML 页面,成功和失败各自渲染不同的内容。前者好处理,解析字段就行;后者需要用关键字匹配,比如判断有没有“成功”字样、有没有“密码错误”字样。我遇到过最难受的一种是无论成功失败都返回同样的状态码和同样的结构,只能靠响应体里某个很隐晦的字段区分,这种就要多试几次失败场景(故意输错密码),把失败时和成功时的响应分别存下来做 diff。
2.4 哪些字段必须动态生成
这一步是很多人卡住的关口。表单里那串看起来最像乱码的字段,往往就是要动态生成的。判断方法很简单:连续抓两次同样的登录请求,把两次的请求体逐字段对比,凡是值发生变化的,就是动态字段;值不变的,可以固定下来。
常见的动态字段有这么几类。第一类是时间戳和随机数,这类通常直接生成即可,不用逆向。第二类是账号密码的散列,需要看前端 JS 里是怎么算的,常见的是对密码做一次哈希再拼接一个盐值,盐值可能是固定的,也可能是从页面里取的。第三类是带签名的校验串,把多个字段按特定顺序拼起来再哈希,这种最麻烦,必须把 JS 里的拼接顺序和哈希算法完整还原。
还原 JS 的方法:在开发者工具里搜提交接口的路径,或者搜表单字段名,定位到负责提交的那段脚本。如果代码没被混淆,基本能直接读懂;如果被混淆了,就重点找哈希函数的调用点,把入参和算法抠出来,用 Python 复现。我做这个项目的时候,密码字段的处理就是一次哈希,算法在页面里明明白白写着,直接用现成的库实现,两行代码搞定。
还有一个隐性的动态字段容易被忽略:认证会话的有效期。有些网关在你打开认证页面时会种一个会话,这个会话有生命周期,比如几分钟内不提交就失效。如果你的脚本是先打开页面、隔了很久才提交,就会失败。解决办法是把“取参数”和“提交”这两个动作放在紧邻的时间窗口内完成,不要中间插入耗时的操作。
3. 用 HTTP 客户端把认证流程跑通
3.1 运行环境与依赖准备
环境这块没什么特殊要求,Python 3.8 以上就行,主流发行版自带的版本都够用。依赖也很轻,一个 HTTP 客户端库加一个哈希库就够,后者标准库自带。安装命令大概是这样:
pip install requests如果你的环境里 pip 装包受限(有些内网机器确实如此),其实用标准库里的 HTTP 客户端也能实现,只是要自己处理 Cookie 和编码,麻烦一点。我倾向用第三方库,因为会话管理、编码处理、超时控制这些都现成,少写很多样板代码。
代码组织上,我建议至少分三个文件:一个配置文件放账号密码和接口路径(这个文件要排除在版本控制之外),一个核心模块放认证和检测逻辑,一个入口脚本负责调度。如果只是自己用,写在一个文件里也行,但请至少把凭据从代码里抽出来放到配置或者环境变量里,这是最基本的习惯。我见过太多人把账号密码写死在脚本里,然后传到公开仓库,第二天发现账号被别人拿去用了。
关于凭据存放,我的具体做法是用环境变量,配合一个简单的加载逻辑。这样做的好处是脚本本身可以随便复制、随便分享,凭据不进代码;坏处是调试时如果忘了设环境变量会报错。所以我还在代码里加了一个兜底:环境变量读不到时,从同目录下的配置文件读,两个都没有就抛异常并给出明确提示。这种设计比“读到空字符串然后一路错到认证失败”要好排查得多。
3.2 核心认证函数怎么写
核心逻辑其实就三步:取参数、拼表单、发请求。但每一步都有细节。先看整体结构:
import os import re import time import hashlib import logging import requests PORTAL_PROBE = "http://www.example.com" # 用于触发重定向的探测地址 SUBMIT_PATH = "/portal/login" # 提交接口路径,需按实际填写 HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0 Safari/537.36", "Content-Type": "application/x-www-form-urlencoded", } def grab_context(session, timeout=5): """未认证状态下访问探测地址,从重定向里捞上下文参数""" resp = session.get(PORTAL_PROBE, headers=HEADERS, allow_redirects=False, timeout=timeout) location = resp.headers.get("Location", "") if not location: # 网关没有返回 302,可能是另一种拦截方式 return None ctx = {} for key in ("userip", "wlanacname", "nasip", "wlanparameter"): m = re.search(r"[?&]{}=([^&]+)".format(key), location) ctx[key] = m.group(1) if m else "" return ctxallow_redirects=False这一行是整个函数的关键,前面提过,不设置的话拿不到 Location 头。返回的字典里就是后续提交要用的位置类参数。如果location为空,说明这次探测没有触发重定向,可能是当前已经认证成功,也可能是网关换了拦截方式,两种情况要区分对待——先检查是不是已经在线,再考虑格式问题。
接着是提交函数:
def do_login(session, ctx, account, password): """拼装表单并提交,返回是否成功""" hashed = hashlib.md5(password.encode("utf-8")).hexdigest() payload = { "account": account, "password": hashed, # 按实际加密方式调整 "userip": ctx.get("userip", ""), "wlanacname": ctx.get("wlanacname", ""), "nasip": ctx.get("nasip", ""), "wlanparameter": ctx.get("wlanparameter", ""), "jsVersion": "1.0", # 装饰类字段,原样保留 "ts": str(int(time.time() * 1000)), } url = "http://" + ctx.get("nasip", "") + SUBMIT_PATH try: resp = session.post(url, data=payload, headers=HEADERS, timeout=8) except requests.RequestException as e: logging.warning("提交认证请求异常: %s", e) return False body = resp.text if '"result":"ok"' in body or "认证成功" in body: return True logging.info("认证返回: %s", body[:200]) return False这段代码里有几处需要你按实际情况调整的地方,我在注释里标了。SUBMIT_PATH必须换成你抓到的真实路径;密码的哈希方式如果不一样,要改那两行;成功判断的关键字也要按你的网关返回改。这几个地方是唯二硬编码的部分,其余全部来自动态提取。
提交地址我拼成了“接入设备 IP + 路径”的形式,这是常见情况,但也见过提交地址和接入设备 IP 无关、固定指向认证服务器的情况。以你抓到的请求为准,不要照抄。抓包时把那条 POST 请求的完整 URL 复制下来,看看它和你提取到的哪个参数有关联,再决定怎么拼。
3.3 状态检测:怎么判断自己掉线了
检测逻辑的可靠性直接决定了整个方案的体验。做得糙的,要么频繁误判导致反复重连(网关那边可能还会因为短时间多次认证把你锁一会儿),要么长时间掉线却毫无察觉。
最简单的一招是访问一个已知的、返回内容固定的小页面,判断返回内容是否符合预期。如果被重定向了,说明掉线;如果拿到正常内容,说明在线。探测地址要选那种响应快、内容短、不依赖特定网络的,比如某个公共的连通性检测接口。不要选大网站首页,几百 KB 的页面拿来每十秒拉一次,纯属浪费。
def is_online(session, timeout=4): try: resp = session.get(PORTAL_PROBE, headers=HEADERS, allow_redirects=False, timeout=timeout) except requests.RequestException: return None # 网络本身断了,区别于"被拦截" if resp.status_code in (301, 302, 303, 307, 308): loc = resp.headers.get("Location", "") return not ("portal" in loc or "login" in loc or "auth" in loc) return resp.status_code == 200注意这里返回三态而不是布尔值:True表示在线,False表示被拦截,None表示请求本身失败了。这个区分很重要。如果返回None,说明是链路问题,此时去跑认证流程毫无意义——参数都取不到。正确的处理是先等一段时间,看链路恢复没有,恢复了再走认证。我做第一版的时候没区分这两种情况,结果网线被拔的时候脚本拼命尝试认证,日志刷了几百行全是失败,硬盘都写热了。
还有一个细节:探测请求要带上和认证相同的会话。前面提过原因,这里补充另一个理由——有些网关会根据会话里的 Cookie 识别客户端,用新会话探测可能被当作未知设备,判定结果不可靠。
3.4 守护循环与重试节奏控制
把上面三块串起来就是一个完整的守护循环。骨架很朴素,但节奏控制有讲究:
def main_loop(): session = requests.Session() session.trust_env = False # 忽略系统代理设置 backoff = 5 while True: state = is_online(session) if state is True: backoff = 5 time.sleep(15) continue if state is None: time.sleep(20) continue ctx = grab_context(session) if not ctx or not ctx.get("userip"): logging.info("未能获取认证上下文,稍后重试") time.sleep(30) continue ok = do_login(session, ctx, ACCOUNT, PASSWORD) logging.info("认证尝试结果: %s", ok) if not ok: time.sleep(backoff) backoff = min(backoff * 2, 300) else: backoff = 5 time.sleep(8) # 认证后稍等再验证这里有两个设计点值得展开。第一是退避。认证失败时不要固定间隔死磕,网关一般都有失败次数限制,短时间内连续失败可能触发临时锁定,反而让情况更糟。用指数退避,从 5 秒开始翻倍,封顶 5 分钟,既能快速恢复,又不会把网关惹毛。
第二是认证后的等待。提交成功后立刻去检测在线状态,大概率还是判定为掉线,因为网关那边的状态同步有延迟,几百毫秒到两三秒都见过。所以认证成功后先等几秒再进入下一轮检测,避免刚认证完就被自己误判成失败、再认证一次,形成循环。
session.trust_env = False这一行也别忽略。很多开发机上配了环境级的代理变量,requests 会老老实实遵守,结果你的认证请求全走代理出去了,网关根本看不到你的 IP,自然认证失败。这个坑我踩过,排查了大半天,最后发现是代理环境变量在作祟。
4. 让它长期稳定运行的几个关键动作
4.1 开机自启与后台驻留
脚本能跑起来只是开始,能开机自动跑、断了能自己活过来,才算真正省心。不同系统做法不一样,我列一下我实际用过的几种。
Windows 上最省事的是任务计划程序。新建任务,触发器选“计算机启动时”和“用户登录时”两个,操作里填 Python 解释器的完整路径作为程序,脚本路径作为参数,起始于填脚本所在目录(这一项千万别漏,否则相对路径的配置文件读不到)。设置里勾上“如果任务失败,按以下频率重新启动”,间隔设 1 分钟,重试 3 次。还有一个隐藏选项要打开:在“常规”里选“不管用户是否登录都要运行”,这样锁屏状态下也能工作。
Linux 上就用 systemd 服务,比 nohup 挂后台靠谱得多,崩了会自动拉起:
[Unit] Description=Portal Auto Login Daemon After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=/usr/bin/python3 /opt/portal/main.py WorkingDirectory=/opt/portal Restart=always RestartSec=30 Environment=PORTAL_ACCOUNT=your_account Environment=PORTAL_PASSWORD=your_password [Install] WantedBy=multi-user.targetAfter=network-online.target这一行很重要,没有它,服务可能在网络栈还没就绪时就启动,第一次取参数必然失败。虽然靠重试也能恢复,但日志里会多出一堆没用的报错,看着心烦。Restart=always配合RestartSec=30是兜底,进程因为内存、异常之类的原因退出时能自动回来。
如果跑在资源更紧张的设备上,还可以考虑用定时任务代替常驻进程——每 5 分钟跑一次脚本,检查一次、必要就认证一次,跑完退出。这种做法内存占用几乎为零,缺点是掉线后最长要等 5 分钟才恢复。取舍看你更在意资源还是恢复速度。
4.2 凭据管理与日志脱敏
这段单独拎出来讲,因为它是安全问题里最容易出事的环节。三条底线:凭据不进代码、凭据不进日志、凭据不进版本库。
凭据不进代码前面说过,用环境变量或者独立配置文件。配置文件的话,Linux 上记得chmod 600,Windows 上右键属性把继承权限去掉只留自己。凭据不进版本库,靠的是版本控制工具的忽略规则,把配置文件、日志文件都加进去。
凭据不进日志是最容易被忽略的一条。调试阶段为了看请求体,很多人会把整个 payload 打到日志里,跑通之后忘了删。这等于把你的密码明文(或者明文哈希,一样危险)存在了磁盘上,而这个日志文件很可能被你随手打包发给别人求助。我的做法是在打日志的地方统一过一个脱敏函数:
SENSITIVE_KEYS = ("password", "passwd", "pwd", "token", "account") def mask(data): if isinstance(data, dict): return {k: ("***" if k.lower() in SENSITIVE_KEYS else mask(v)) for k, v in data.items()} if isinstance(data, str) and len(data) > 6: return data[:3] + "***" + data[-2:] return data账号脱敏后保留首尾几位,方便确认“是不是用错账号了”,同时不至于把完整账号写进日志。这个函数实现很简单,但能救命。另外日志轮转也要配一下,不然跑几个月日志文件涨到几百 MB,磁盘满了反而搞出一堆新问题。Python 标准库的日志模块自带按大小轮转的处理器,配几行就行,最大 10 MB,保留 5 份,够用了。
提示:如果你的脚本要分享给别人参考,把配置文件里的真实凭据换成占位符,检查一遍日志目录有没有一起打包。这两步花不了一分钟,但泄露一次的成本很高。
5. 出问题时的排查路径与避坑清单
5.1 症状对照表:从现象倒推原因
这类脚本的失败现象高度集中,我把自己遇到过的整理成一张表,出问题时先按症状对号入座,能省掉大半的盲目排查。
| 现象 | 大概率原因 | 验证方法 | 处理动作 |
|---|---|---|---|
| 一直提示取不到上下文 | 已处于认证状态,或探测地址不通 | 手动访问探测地址看是否直连 | 换探测地址,或先判断在线再取参 |
| 请求成功但返回失败 | 密码加密方式变了 / 字段缺失 | 用浏览器抓一条成功的请求逐字段对比 | 比对请求体差异,补齐字段 |
| 认证成功但很快又掉线 | 同账号多设备互踢 / 会话已存在 | 查看是否有其他终端在用同一账号 | 单设备使用,或错开使用时段 |
| 偶尔成功偶尔失败 | 参数带时效,取参与提交间隔太久 | 看失败日志的时间差 | 把取参与提交合并到紧邻窗口 |
| 本机手动能登录脚本不行 | 环境变量里的代理被带上了 | 打印实际使用的代理设置 | 显式关闭代理继承 |
| 日志里全是连接超时 | 链路本身断了,不是认证问题 | ping 内网网关 | 等链路恢复,加三态判断 |
| 换了楼栋就失效 | 接入设备标识硬编码 | 对比新旧参数 | 全部改为动态提取 |
这张表里最想强调第一行和最后一行。第一行是新手的头号杀手:你正处在认证状态,访问任何地址都不会被重定向,当然取不到上下文,然后你开始怀疑网关、怀疑代码、怀疑人生。养成习惯,动手前先确认自己现在是不是已经在线。
最后一行则是“换环境就失效”的根源,解决办法只有一个:把所有位置类参数全部动态提取,一个都别硬编码。这个原则我从第一版就坚持,后面换过三次宿舍、两次实验室,脚本一次都没改过。
5.2 几个只有踩过才知道的细节
局域网有多个认证入口时,探测地址要选对。有些校园网分内外两个入口,访问内网地址不会被拦截,访问外网地址才会。如果你的探测地址选成了内网服务,脚本会一直认为在线,永远不触发认证。选一个明确在校园网外的公网地址做探测,最保险。
时间不同步会导致签名失效。如果表单里有时间戳参与签名,而你的设备时间比标准时间差了几分钟,签名就永远对不上。这个错误极其隐蔽,因为请求格式完全正确,就是返回“签名错误”。我那次是因为一台长期不联网的开发板,时间漂移了十几分钟,排查了一下午。之后我给所有设备都配了时间同步,并且脚本启动时打印一次当前时间,方便对照。
认证成功后不要立刻高频检测。有些网关对短时间内的大量请求有防护,你认证成功后每两秒探测一次,可能被判为异常行为,直接给你断掉。我现在的节奏是:认证成功后等 8 秒做第一次确认,之后正常间隔 15 秒,稳定运行后可以放宽到 30 秒甚至 60 秒。掉线恢复的及时性对绝大多数使用场景来说,30 秒完全够用。
多网卡设备要指定出口。笔记本同时有线和无线、或者装了虚拟网卡时,请求可能从错误的网卡出去,导致网关看到的源 IP 和你提取的userip不一致,认证直接失败。Windows 上可以临时禁用不用的网卡来验证;Linux 上可以绑定源地址。这个问题在多虚拟机的开发机上特别常见,我建议调试阶段就先把多余网卡关掉,排除干扰。
接口改版是必然会发生的,做好心理准备。我维护这个脚本的几年里,遇到过两次接口变更:一次是字段名改了,一次是加密方式从一次哈希变成了“哈希加盐”。两次都是通过“拿浏览器抓一条成功请求,和脚本发出去的请求做 diff”定位的。所以我在代码里留了个调试开关,打开后会把完整请求体和响应体(脱敏后)写到单独的文件里,出问题时打开开关跑一次,对比一下,几分钟就能找到差异点。这个开关比什么文档都好使。
最后说一句个人体会。这类脚本的价值不在于技术多复杂,而在于它把一件每天要打断你好几次的小事彻底抹平了。真正花时间的从来不是写那几十行代码,而是搞懂“为什么这个字段必须带”“为什么这里要等三秒”。把这几处理解透了,以后碰到任何类似的网页表单自动化——无论是内网系统、老旧管理后台还是各种需要重复填表的场景——你都能用同一套方法快速拿下。