<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-CN"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://www.thatgfsj.xyz/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.thatgfsj.xyz/" rel="alternate" type="text/html" hreflang="zh-CN" /><updated>2026-06-06T20:59:12+00:00</updated><id>https://www.thatgfsj.xyz/feed.xml</id><title type="html">Thatgfsj</title><subtitle>全栈开发者，熟练vibe coding。 折腾工具链、自动化流程和 AI 工程化。</subtitle><author><name>Thatgfsj</name></author><entry><title type="html">校园网 Web 认证（Portal Authentication）机制详解</title><link href="https://www.thatgfsj.xyz/blog/2026/06/campus-network-portal-authentication/" rel="alternate" type="text/html" title="校园网 Web 认证（Portal Authentication）机制详解" /><published>2026-06-02T10:53:00+00:00</published><updated>2026-06-02T10:53:00+00:00</updated><id>https://www.thatgfsj.xyz/blog/2026/06/campus-network-portal-authentication</id><content type="html" xml:base="https://www.thatgfsj.xyz/blog/2026/06/campus-network-portal-authentication/"><![CDATA[<blockquote>
  <p>本文档讲解校园网 Portal 认证协议，以及项目中 PowerShell 脚本的自动化实现原理。</p>
</blockquote>

<hr />

<h2 id="目录">目录</h2>

<ol>
  <li><a href="#1-什么是-portal-认证">什么是 Portal 认证</a></li>
  <li><a href="#2-认证流程全景">认证流程全景</a></li>
  <li><a href="#3-协议分析请求参数详解">协议分析：请求参数详解</a></li>
  <li><a href="#4-动态参数获取策略">动态参数获取策略</a></li>
  <li><a href="#5-脚本核心实现">脚本核心实现</a></li>
  <li><a href="#6-安全设计凭证管理">安全设计：凭证管理</a></li>
  <li><a href="#7-认证响应码">认证响应码</a></li>
  <li><a href="#8-连通性验证为什么用-204">连通性验证：为什么用 204？</a></li>
  <li><a href="#9-附录关键代码位置">附录：关键代码位置</a></li>
</ol>

<hr />

<h2 id="1-什么是-portal-认证">1. 什么是 Portal 认证</h2>

<p><strong>Portal Authentication（Web 认证）</strong> 是目前国内高校普遍采用的网络准入控制方案。它的工作方式如下：</p>

<p>用户连接校园网 WiFi 后，设备虽然拿到了 IP 地址，但<strong>默认情况下无法访问外网</strong>。此时如果用户打开浏览器访问任意一个 HTTP 网站，<strong>AC（无线接入控制器）会拦截这个请求，并以 302 重定向的方式将浏览器引导到认证服务器的 Portal 登录页面</strong>。用户在页面中输入账号密码提交，认证服务器验证通过后，将该设备的 MAC 地址和 IP 加入放行列表，设备随即获得外网访问权限。</p>

<p>这一整套流程的底层由 <strong>AC + RADIUS 服务器</strong> 协同完成，但对客户端来说，核心任务只有一件：<strong>向认证服务器发送一个携带正确参数的 HTTP GET 请求</strong>。浏览器渲染的登录页面本质上只是一层皮——表单提交的最终结果就是一次 GET 请求。抓住了这一点，就有条件用纯脚本替代浏览器来完成自动登录。</p>

<hr />

<h2 id="2-认证流程全景">2. 认证流程全景</h2>

<p>下面这张时序图展示了从设备连接 WiFi 到最终上网的完整交互过程：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>客户端                    AC                       Portal Server           RADIUS
  │                        │                         │                      │
  │  ① 关联 WiFi           │                         │                      │
  │───────────────────────&gt;│                         │                      │
  │                        │                         │                      │
  │  ② DHCP 分配 IP        │                         │                      │
  │&lt;───────────────────────│                         │                      │
  │                        │                         │                      │
  │  ③ 访问任意 HTTP 网站   │                         │                      │
  │───────────────────────&gt;│                         │                      │
  │                        │  ④ AC 劫持 + 302 重定向  │                      │
  │                        │────────────────────────&gt;│                      │
  │                        │                         │                      │
  │  ⑤ 返回 Portal 页面（URL 中携带 wlanuserip/mac/vlan 等参数）             │
  │&lt;─────────────────────────────────────────────────│                      │
  │                        │                         │                      │
  │  ⑥ 提交认证 GET 请求（携带用户凭证 + 设备信息）                           │
  │─────────────────────────────────────────────────&gt;│                      │
  │                        │                         │  ⑦ RADIUS 验证       │
  │                        │                         │─────────────────────&gt;│
  │                        │                         │  ⑧ 返回验证结果       │
  │                        │                         │&lt;─────────────────────│
  │                        │                         │                      │
  │  ⑨ 返回认证结果         │                         │                      │
  │&lt;─────────────────────────────────────────────────│                      │
  │                        │                         │                      │
  │  ⑩ 可以上网了          │                         │                      │
</code></pre></div></div>

<p>本项目做的事情很直接：<strong>跳过步骤 ③~⑤ 的浏览器交互环节</strong>，直接从步骤 ⑥ 开始——构造 HTTP GET 请求发送到认证服务器。</p>

<hr />

<h2 id="3-协议分析请求参数详解">3. 协议分析：请求参数详解</h2>

<h3 id="31-请求概要">3.1 请求概要</h3>

<table>
  <thead>
    <tr>
      <th>属性</th>
      <th>值</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>目标端点</td>
      <td><code class="language-plaintext highlighter-rouge">http://&lt;认证服务器&gt;:6060/quickauth.do</code></td>
    </tr>
    <tr>
      <td>请求方法</td>
      <td><code class="language-plaintext highlighter-rouge">GET</code></td>
    </tr>
    <tr>
      <td>参数传递</td>
      <td>Query String（查询字符串）</td>
    </tr>
  </tbody>
</table>

<p>不需要 POST body，不需要 Cookie，不需要 Session。所有认证信息全部编码在 URL 的查询字符串中。</p>

<h3 id="32-参数分类说明">3.2 参数分类说明</h3>

<p>认证请求的 Query String 包含十余个参数，按性质可以分成四类。</p>

<h4 id="第一类用户凭证">第一类：用户凭证</h4>

<p>这两个参数直接决定认证是否通过：</p>

<table>
  <thead>
    <tr>
      <th>参数</th>
      <th>格式</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">userid</code></td>
      <td><code class="language-plaintext highlighter-rouge">[学号]@[运营商后缀]</code></td>
      <td>用户身份标识。后缀决定了认证请求被路由到哪个运营商的 RADIUS 服务器</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">passwd</code></td>
      <td>明文，需 URL 编码</td>
      <td>校园网密码</td>
    </tr>
  </tbody>
</table>

<p><strong>运营商后缀对照：</strong></p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">编号</th>
      <th>运营商</th>
      <th>后缀</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">1</td>
      <td>移动</td>
      <td><code class="language-plaintext highlighter-rouge">@xxgcyd</code></td>
    </tr>
    <tr>
      <td style="text-align: center">2</td>
      <td>联通</td>
      <td><code class="language-plaintext highlighter-rouge">@xxgclt</code></td>
    </tr>
    <tr>
      <td style="text-align: center">3</td>
      <td>电信</td>
      <td><code class="language-plaintext highlighter-rouge">@xxgcdx</code></td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>选择不同的运营商意味着认证请求会被转发到不同的 RADIUS 服务器进行身份校验，因此后缀必须和设备办理宽带时选择的运营商一致。</p>
</blockquote>

<h4 id="第二类设备与网络环境参数">第二类：设备与网络环境参数</h4>

<p>这些参数随设备、位置和网络环境变化，每次登录都可能不同：</p>

<table>
  <thead>
    <tr>
      <th>参数</th>
      <th>说明</th>
      <th>获取方式</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">wlanuserip</code></td>
      <td>客户端当前分配的 IPv4 地址</td>
      <td>查询无线网卡配置 / 从重定向 URL 提取</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">mac</code></td>
      <td>无线网卡物理地址</td>
      <td>查询网卡属性 / 从重定向 URL 提取</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">vlan</code></td>
      <td>客户端所属虚拟局域网 ID</td>
      <td><strong>只能从重定向 URL 提取</strong>（系统无法直接查询）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hostname</code></td>
      <td>计算机名</td>
      <td><code class="language-plaintext highlighter-rouge">$env:COMPUTERNAME</code></td>
    </tr>
  </tbody>
</table>

<h4 id="第三类接入点固定参数">第三类：接入点固定参数</h4>

<p>这些参数在同一校区内是固定不变的，由 AC 设备决定：</p>

<table>
  <thead>
    <tr>
      <th>参数</th>
      <th>典型值</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">wlanacname</code></td>
      <td><code class="language-plaintext highlighter-rouge">XXGC-AC-01</code></td>
      <td>无线接入控制器名称</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">wlanacIp</code></td>
      <td><code class="language-plaintext highlighter-rouge">172.18.xxx.xxx</code></td>
      <td>无线接入控制器 IP 地址</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">portalpageid</code></td>
      <td><code class="language-plaintext highlighter-rouge">"3"</code></td>
      <td>Portal 页面模板 ID</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">portaltype</code></td>
      <td><code class="language-plaintext highlighter-rouge">"0"</code></td>
      <td>Portal 认证类型</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">version</code></td>
      <td><code class="language-plaintext highlighter-rouge">"0"</code></td>
      <td>协议接口版本号</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bindCtrlId</code></td>
      <td>空字符串</td>
      <td>绑定控制 ID</td>
    </tr>
  </tbody>
</table>

<h4 id="第四类请求唯一性参数">第四类：请求唯一性参数</h4>

<p>这两个参数由客户端在每次请求时动态生成，用于标识请求的唯一性，防止重放：</p>

<table>
  <thead>
    <tr>
      <th>参数</th>
      <th>生成方式</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">uuid</code></td>
      <td><code class="language-plaintext highlighter-rouge">[guid]::NewGuid()</code> — 标准 UUID v4</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">timestamp</code></td>
      <td>Unix 时间戳 × 1000（毫秒级）</td>
    </tr>
  </tbody>
</table>

<h3 id="33-完整请求示例">3.3 完整请求示例</h3>

<p>将所有参数拼接到一起，最终发出的 GET 请求长这样（为便于阅读做了换行）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET http://172.18.xxx.xxx:6060/quickauth.do
  ?userid=20210101001%40xxgcyd
  &amp;passwd=mypassword123
  &amp;wlanuserip=10.10.50.100
  &amp;wlanacname=XXGC-AC-01
  &amp;wlanacIp=172.18.xxx.xxx
  &amp;vlan=1050
  &amp;mac=aa%3Abb%3Acc%3Add%3Aee%3Aff
  &amp;version=0
  &amp;portalpageid=3
  &amp;timestamp=1680000000000
  &amp;uuid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  &amp;portaltype=0
  &amp;hostname=MYPC
  &amp;bindCtrlId=
</code></pre></div></div>

<hr />

<h2 id="4-动态参数获取策略">4. 动态参数获取策略</h2>

<h3 id="41-两条路线的取舍">4.1 两条路线的取舍</h3>

<p>要实现自动登录，必须解决一个问题：如何获取那些随设备和网络环境变化的参数（IP、MAC、VLAN）？我们探索了两条路线：</p>

<p><strong>路线 A：系统查询。</strong> 使用 <code class="language-plaintext highlighter-rouge">Get-NetAdapter</code>、<code class="language-plaintext highlighter-rouge">Get-NetIPAddress</code> 等 PowerShell cmdlet 直接从系统读取网络配置。</p>

<p><strong>路线 B：URL 解析。</strong> 利用 AC 重定向到 Portal 页面时在 URL 中附带全部参数的这一行为，解析 URL 的 Query String 来提取参数。</p>

<p>路线 A 有两个致命缺陷：</p>

<ol>
  <li><strong>VLAN ID 无法通过任何系统命令获取。</strong> VLAN 是网络设备层面的概念，客户端操作系统不掌握这个信息。</li>
  <li><strong>虚拟机网卡干扰。</strong> 如果电脑安装了 VirtualBox、VMware 或 Hyper-V，系统中会存在多个虚拟网卡，它们的 IP 地址段有时和校园网汇聚层格式相似，导致脚本误将虚拟 IP 当作 <code class="language-plaintext highlighter-rouge">wlanuserip</code> 发送。</li>
</ol>

<p><strong>最终方案：以 URL 解析为主，系统查询兜底。</strong> URL 解析能一次性拿到 BaseURL、VLAN、MAC、AC 名称等全套参数；系统查询则用于在 URL 中的 IP 为 <code class="language-plaintext highlighter-rouge">0.0.0.0</code> 等异常情况下补全正确的本机 IP 和 MAC。</p>

<h3 id="42-重定向-url-的结构">4.2 重定向 URL 的结构</h3>

<p>当 AC 劫持 HTTP 请求并重定向时，浏览器的地址栏会出现类似这样的 URL：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://172.18.xxx.xxx:6060/portal.do
  ?wlanuserip=10.10.50.100
  &amp;wlanacname=XXGC-AC-01
  &amp;wlanacIp=172.18.xxx.xxx
  &amp;mac=AA:BB:CC:DD:EE:FF
  &amp;vlan=1050
  &amp;hostname=MYPC
  &amp;rand=123456
</code></pre></div></div>

<p>脚本中的 <code class="language-plaintext highlighter-rouge">RedirectUrlParser</code> 类负责解析这个 URL，解析步骤为：</p>

<ol>
  <li><strong>提取 BaseURL。</strong> 用正则 <code class="language-plaintext highlighter-rouge">http://&lt;host&gt;/xxx.do</code> 匹配出认证服务器的地址和路径，然后将 <code class="language-plaintext highlighter-rouge">xxx.do</code> 替换为 <code class="language-plaintext highlighter-rouge">quickauth.do</code>，得到实际的认证请求端点。</li>
  <li><strong>拆解 Query String。</strong> 按 <code class="language-plaintext highlighter-rouge">&amp;</code> 分割得到参数对，再按 <code class="language-plaintext highlighter-rouge">=</code> 分割得到键和值。</li>
  <li><strong>字段映射。</strong> 将 URL 参数名（如 <code class="language-plaintext highlighter-rouge">wlanuserip</code>）映射到脚本内部的配置字段。</li>
  <li><strong>MAC 标准化。</strong> 将 MAC 地址统一转换为小写、冒号分隔的格式（<code class="language-plaintext highlighter-rouge">aa:bb:cc:dd:ee:ff</code>），避免因格式差异导致认证失败。</li>
</ol>

<h3 id="43-自动检测两级回退机制">4.3 自动检测：两级回退机制</h3>

<p>脚本启动后会依次尝试三种方式获取参数：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>方法 ①：GET http://www.qq.com（设置 MaximumRedirection=0）
         └─ AC 返回 302 重定向 → 从 Location 响应头提取 Portal URL → 解析参数
         └─ 超时或失败 → 尝试方法 ②

方法 ②：GET http://172.18.xxx.xxx:6060（设置 MaximumRedirection=0）
         └─ AC 返回 302 重定向 → 解析 URL → 用本地获取的 IP/MAC 补全缺失字段
         └─ 超时或失败 → 进入方法 ③

方法 ③：弹出提示，让用户手动从浏览器地址栏复制 Portal URL 并粘贴到终端
</code></pre></div></div>

<blockquote>
  <p>方法 ① 和 ② 的核心技巧是设置 <code class="language-plaintext highlighter-rouge">-MaximumRedirection 0</code>，阻止 PowerShell 自动跟随重定向。这样我们才能在响应头中拿到 AC 返回的 Portal 地址，而不是直接被带到登录页面。</p>
</blockquote>

<h3 id="44-虚拟机网卡过滤">4.4 虚拟机网卡过滤</h3>

<p>如前所述，虚拟网卡会干扰 IP 和 MAC 的获取。<code class="language-plaintext highlighter-rouge">NetworkInterfaceHelper</code> 在查询网卡时应用了严格的过滤条件：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">InterfaceDescription</code> 必须匹配 <code class="language-plaintext highlighter-rouge">Wi-Fi|Wireless|WLAN</code></li>
  <li><code class="language-plaintext highlighter-rouge">Status</code> 必须为 <code class="language-plaintext highlighter-rouge">Up</code></li>
  <li><code class="language-plaintext highlighter-rouge">Name</code> 不能包含 <code class="language-plaintext highlighter-rouge">Virtual</code>、<code class="language-plaintext highlighter-rouge">VMware</code>、<code class="language-plaintext highlighter-rouge">Hyper-V</code>、<code class="language-plaintext highlighter-rouge">VirtualBox</code> 等关键词</li>
</ul>

<p>只保留同时满足以上三个条件的第一个结果，确保拿到的是真实的物理无线网卡。</p>

<hr />

<h2 id="5-脚本核心实现">5. 脚本核心实现</h2>

<h3 id="51-通信方式绕过浏览器">5.1 通信方式：绕过浏览器</h3>

<p>模拟浏览器的完整登录流程（打开页面 → 等待渲染 → 填写表单 → 管理 Cookie → 提交）会引入大量不必要的复杂度和故障点。</p>

<p>本项目的 <code class="language-plaintext highlighter-rouge">xywdl.ps1</code> 直接使用 PowerShell 原生的 <code class="language-plaintext highlighter-rouge">Invoke-WebRequest</code> cmdlet 构造 HTTP GET 请求，全程不涉及任何浏览器环节：</p>

<ul>
  <li>不加载页面，不执行 JavaScript</li>
  <li>不维护 Cookie 或 Session</li>
  <li>不解析 HTML DOM</li>
  <li>不需要 WebDriver 或 headless 浏览器</li>
</ul>

<p>整个认证过程被精简为：<strong>构造 URL → 发送 GET → 解析响应</strong>。</p>

<h3 id="52-配置结构化管理">5.2 配置结构化管理</h3>

<p>所有认证参数封装在 <code class="language-plaintext highlighter-rouge">NetworkConfig</code> 类中，与业务逻辑完全解耦。固定参数（如 BaseURL、AC 信息）通过 JSON 文件持久化存储，避免了散落在代码各处的硬编码字符串。用户首次配置后，后续运行无需重复输入。</p>

<h3 id="53-请求参数标准化">5.3 请求参数标准化</h3>

<p>在拼接 Query String 之前，脚本对每个参数做了严格的标准化处理：</p>

<ul>
  <li><strong>URL 编码。</strong> 用户名（含 <code class="language-plaintext highlighter-rouge">@</code>）、密码、主机名、AC 名称等可能包含特殊字符的值，统一调用 <code class="language-plaintext highlighter-rouge">[Uri]::EscapeDataString()</code> 进行百分号编码，防止参数被截断或解析错误。</li>
  <li><strong>UUID 生成。</strong> 每次认证请求生成一个新的 GUID，满足服务器端对请求唯一性的校验要求。</li>
  <li><strong>毫秒级时间戳。</strong> 使用高精度时间戳，避免同一秒内发出的多次请求被服务器去重机制误判为重复请求。</li>
</ul>

<h3 id="54-全链路异常处理">5.4 全链路异常处理</h3>

<p>认证请求的每个环节都做了异常捕获，确保即使失败也能给出有意义的信息：</p>

<ul>
  <li><strong>网络层异常</strong>：DNS 解析失败、连接超时、SSL/TLS 错误</li>
  <li><strong>HTTP 层异常</strong>：服务器返回 4xx 或 5xx 状态码</li>
  <li><strong>响应解析异常</strong>：返回内容格式不符合预期</li>
</ul>

<p>所有异常被统一捕获并输出结构化的错误描述，不会因为未处理的异常导致脚本崩溃。</p>

<hr />

<h2 id="6-安全设计凭证管理">6. 安全设计：凭证管理</h2>

<h3 id="61-密码加密存储">6.1 密码加密存储</h3>

<p>用户密码<strong>绝对不以明文形式写入磁盘</strong>。整个加密和存储流程如下：</p>

<p><strong>写入时：</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>用户输入密码（Read-Host -AsSecureString，终端不回显）
    ↓
转换为 SecureString
    ↓
ConvertFrom-SecureString（调用 Windows DPAPI 加密）
    ↓
生成 Base64 格式的密文字符串
    ↓
存入 JSON 配置文件
</code></pre></div></div>

<p><strong>读取时：</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>从 JSON 读取 Base64 密文
    ↓
ConvertTo-SecureString（DPAPI 解密）
    ↓
Marshal.SecureStringToBSTR（还原为明文字符串到内存）
    ↓
使用完毕后立即 Marshal.FreeBSTR 释放内存
</code></pre></div></div>

<p>整个过程中，明文密码仅在内存中短暂存在，且使用后立即释放，不会残留在堆中。</p>

<h3 id="62-windows-dpapi-的保护范围">6.2 Windows DPAPI 的保护范围</h3>

<p>DPAPI（Data Protection API）是 Windows 操作系统内置的加密服务。默认情况下，加密密钥由当前用户的登录凭据和当前机器的硬件信息共同派生。这意味着：</p>

<ul>
  <li><strong>同一用户在同一台机器上</strong>可以正常解密配置文件</li>
  <li><strong>配置文件被复制到另一台机器</strong>后无法解密</li>
  <li><strong>另一个用户在同一台机器上登录</strong>也无法解密</li>
</ul>

<p>这种绑定机制为本地凭证存储提供了操作系统级别的安全保障，无需应用程序自行管理加密密钥。</p>

<h3 id="63-文件层面的额外保护">6.3 文件层面的额外保护</h3>

<p>除了内容加密，配置文件本身也做了防护：</p>

<ul>
  <li><strong>文件属性设为隐藏</strong>（<code class="language-plaintext highlighter-rouge">[System.IO.FileAttributes]::Hidden</code>），普通用户浏览目录时不可见</li>
  <li><strong>存储路径放在 <code class="language-plaintext highlighter-rouge">$env:APPDATA</code> 目录下</strong>，使用不起眼的文件名 <code class="language-plaintext highlighter-rouge">xxgc_campus_net_config.txt</code></li>
</ul>

<hr />

<h2 id="7-认证响应码">7. 认证响应码</h2>

<p>认证服务器返回的 JSON 响应中，<code class="language-plaintext highlighter-rouge">code</code> 字段指示认证结果：</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">code</th>
      <th>含义</th>
      <th>用户应检查</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">0</code></td>
      <td>认证成功，设备已获得外网访问权限</td>
      <td>—</td>
    </tr>
    <tr>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">1</code></td>
      <td>账号不存在</td>
      <td>学号是否正确、运营商是否选对</td>
    </tr>
    <tr>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">44</code></td>
      <td>非法接入</td>
      <td>VLAN ID 和 MAC 地址是否与当前网络环境匹配</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>脚本同时兼容非 JSON 格式的旧版响应。如果返回的不是标准 JSON，则通过关键字匹配来判断结果——<code class="language-plaintext highlighter-rouge">success</code> 和 <code class="language-plaintext highlighter-rouge">认证成功</code> 视为通过，<code class="language-plaintext highlighter-rouge">账号不存在</code> 和 <code class="language-plaintext highlighter-rouge">非法接入</code> 视为失败。</p>
</blockquote>

<hr />

<h2 id="8-连通性验证为什么用-204">8. 连通性验证：为什么用 204？</h2>

<h3 id="81-认证成功不等于能上网">8.1 认证成功不等于能上网</h3>

<p>认证服务器返回 <code class="language-plaintext highlighter-rouge">code: 0</code> 只代表<strong>凭证校验在 RADIUS 端通过了</strong>，并不等价于设备已经可以正常访问外网。实际中还可能遇到以下情况：</p>

<ul>
  <li>AC 放行有延迟（RADIUS 计费报文从认证服务器同步到 AC 需要几秒）</li>
  <li>同一账号已在其他设备登录，IP/MAC 绑定冲突</li>
  <li>认证服务器返回成功，但 AC 因未知原因未执行放行动作</li>
</ul>

<p>因此，在收到认证成功响应之后，必须做一步额外的<strong>外网连通性验证</strong>，用事实而非状态码来确认设备确实通了。</p>

<h3 id="82-http-200-的陷阱">8.2 HTTP 200 的陷阱</h3>

<p>直觉上，发一个 HTTP 请求到百度之类的网站，如果返回 200 就说明能上网。但这个逻辑在校园网环境下是<strong>不成立</strong>的。</p>

<p>原因在于：在设备尚未认证的状态下，AC 会劫持所有 HTTP 请求并返回 Portal 登录页面。<strong>这个 Portal 页面本身的 HTTP 状态码就是 200 OK。</strong> 也就是说：</p>

<table>
  <thead>
    <tr>
      <th>场景</th>
      <th>你请求的 URL</th>
      <th>实际响应方</th>
      <th style="text-align: center">HTTP 状态码</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>未认证</td>
      <td><code class="language-plaintext highlighter-rouge">http://www.baidu.com</code></td>
      <td>AC（劫持后返回 Portal 页面）</td>
      <td style="text-align: center">200</td>
    </tr>
    <tr>
      <td>已认证</td>
      <td><code class="language-plaintext highlighter-rouge">http://www.baidu.com</code></td>
      <td>百度服务器</td>
      <td style="text-align: center">200</td>
    </tr>
  </tbody>
</table>

<p>两边都是 200，仅凭状态码根本无法区分。你需要一个<strong>不会被 AC 伪造的差异化信号</strong>。</p>

<h3 id="83-业界的做法captive-portal-detection">8.3 业界的做法：Captive Portal Detection</h3>

<p>各大操作系统在检测强制门户（Captive Portal）时都采用了相同的思路——请求一个<strong>响应特征已知且独特</strong>的外部 URL，通过比较实际响应和预期特征来判断流量是否被劫持：</p>

<table>
  <thead>
    <tr>
      <th>系统</th>
      <th>探测 URL</th>
      <th>预期特征</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Android</td>
      <td><code class="language-plaintext highlighter-rouge">http://connectivitycheck.gstatic.com/generate_204</code></td>
      <td>HTTP 204 No Content</td>
    </tr>
    <tr>
      <td>Apple</td>
      <td><code class="language-plaintext highlighter-rouge">http://captive.apple.com/hotspot-detect.html</code></td>
      <td>HTTP 200 + 正文为 <code class="language-plaintext highlighter-rouge">Success</code></td>
    </tr>
    <tr>
      <td>Windows</td>
      <td><code class="language-plaintext highlighter-rouge">http://www.msftconnecttest.com/connecttest.txt</code></td>
      <td>HTTP 200 + 正文为 <code class="language-plaintext highlighter-rouge">Microsoft Connect Test</code></td>
    </tr>
  </tbody>
</table>

<p>这些端点的共同特点是：它们的预期响应非常独特，AC 在劫持请求时返回的 Portal 页面在状态码或正文内容上与预期响应存在显著差异，从而可以被可靠地区分。</p>

<h3 id="84-本项目的实现">8.4 本项目的实现</h3>

<p>项目在 Rust 后端（<code class="language-plaintext highlighter-rouge">src-tauri/src/lib.rs</code> 中的 <code class="language-plaintext highlighter-rouge">check_url</code> 函数）实现了三层次的连通性判断：</p>

<p><strong>层次一：检查 302 重定向</strong></p>

<p>请求发出后如果收到了 3xx 重定向，检查 <code class="language-plaintext highlighter-rouge">Location</code> 头的内容。若其中包含 <code class="language-plaintext highlighter-rouge">portal</code>、<code class="language-plaintext highlighter-rouge">drcom</code>、<code class="language-plaintext highlighter-rouge">inode</code>、<code class="language-plaintext highlighter-rouge">eportal</code>、<code class="language-plaintext highlighter-rouge">srun</code>、<code class="language-plaintext highlighter-rouge">authserv</code> 等 Portal 系统关键词，则判定为<strong>未认证</strong>——请求仍然被 AC 劫持到了 Portal 页面。如果重定向目标不匹配这些关键词（比如正常的 CDN 跳转），则视为<strong>已连通</strong>。</p>

<p><strong>层次二：检查 204 状态码</strong></p>

<p>如果收到了 <strong>HTTP 204 No Content</strong>，直接判定为<strong>已连通</strong>。</p>

<p>这是整个检测体系中最可靠的信号。原因很简单：校园网 AC 在劫持 HTTP 请求时，只会返回两种响应——HTTP 200（Portal 登录页面）或 HTTP 302（重定向到 Portal）。AC 绝对不会返回 204，因为 204 No Content 是一个应用层约定的语义（”请求成功，但没有响应体”），只有真正的目标服务器才会生成它。</p>

<p>因此，<strong>收到 204 = 请求确实到达了外网的真实服务器 = 设备已通过认证</strong>。这一步不需要检查任何正文内容，不需要匹配任何关键词，204 本身就是铁证。</p>

<p><strong>层次三：检查正文内容</strong></p>

<p>如果收到了 HTTP 200（或其他 2xx），则需要进一步分析响应正文：</p>

<ul>
  <li>正文包含 <code class="language-plaintext highlighter-rouge">drcom</code> / <code class="language-plaintext highlighter-rouge">inode</code> / <code class="language-plaintext highlighter-rouge">eportal</code> / <code class="language-plaintext highlighter-rouge">srun</code> / <code class="language-plaintext highlighter-rouge">portal认证</code> / <code class="language-plaintext highlighter-rouge">校园网认证</code> → <strong>未认证</strong>，当前看到的是 AC 返回的 Portal 页面</li>
  <li>正文包含 <code class="language-plaintext highlighter-rouge">百度一下</code> / <code class="language-plaintext highlighter-rouge">baidu</code> → <strong>已连通</strong>，请求确实到达了百度</li>
  <li>其他内容 → <strong>已连通</strong>（保守策略：不包含 Portal 特征的内容视为正常响应）</li>
</ul>

<h3 id="85-完整检测流程">8.5 完整检测流程</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>开始检测
  │
  ├─ ① 请求 https://example.com/（禁止跟随重定向）
  │    ├─ 204 或非 Portal 跳转 → ✅ 已连通，结束
  │    └─ Portal 跳转 / 失败 → 继续 ②
  │
  └─ ② 请求 http://connect.rom.miui.com/generate_204（禁止跟随重定向）
       ├─ 204 → ✅ 已连通
       ├─ 302 + Portal 关键词 → ❌ 仍需登录
       ├─ 200 + 正文含 Portal 关键词 → ❌ 仍需登录
       └─ 200 + 正文不含 Portal 关键词 → ✅ 已连通
</code></pre></div></div>

<h3 id="86-为什么用-miui-的端点而非-google-的">8.6 为什么用 MIUI 的端点而非 Google 的</h3>

<p>虽然 Google 的 <code class="language-plaintext highlighter-rouge">gstatic.com/generate_204</code> 是 Android 系统的标准检测端点，但国内校园网环境下访问 Google 服务可能因 DNS 污染或 GFW 干扰导致请求超时（而非返回 204），引入不必要的误判。小米的 <code class="language-plaintext highlighter-rouge">connect.rom.miui.com</code> 部署在国内 CDN 上，延迟低、可用性高，更适合作为检测目标。</p>

<hr />

<h2 id="9-附录关键代码位置">9. 附录：关键代码位置</h2>

<table>
  <thead>
    <tr>
      <th>功能模块</th>
      <th>文件与类/函数</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>认证流程编排</td>
      <td><code class="language-plaintext highlighter-rouge">xywdl.ps1</code> → <code class="language-plaintext highlighter-rouge">AuthenticationClient</code></td>
    </tr>
    <tr>
      <td>请求参数模型</td>
      <td><code class="language-plaintext highlighter-rouge">xywdl.ps1</code> → <code class="language-plaintext highlighter-rouge">NetworkConfig</code></td>
    </tr>
    <tr>
      <td>重定向 URL 解析</td>
      <td><code class="language-plaintext highlighter-rouge">xywdl.ps1</code> → <code class="language-plaintext highlighter-rouge">RedirectUrlParser</code></td>
    </tr>
    <tr>
      <td>网卡信息获取</td>
      <td><code class="language-plaintext highlighter-rouge">xywdl.ps1</code> → <code class="language-plaintext highlighter-rouge">NetworkInterfaceHelper</code></td>
    </tr>
    <tr>
      <td>凭证加密存储与读取</td>
      <td><code class="language-plaintext highlighter-rouge">xywdl.ps1</code> → <code class="language-plaintext highlighter-rouge">ConfigManager</code></td>
    </tr>
    <tr>
      <td>运营商后缀映射</td>
      <td><code class="language-plaintext highlighter-rouge">xywdl.ps1</code> → <code class="language-plaintext highlighter-rouge">DomainConfig</code></td>
    </tr>
    <tr>
      <td>外网连通性检测</td>
      <td><code class="language-plaintext highlighter-rouge">src-tauri/src/lib.rs</code> → <code class="language-plaintext highlighter-rouge">check_url()</code></td>
    </tr>
    <tr>
      <td>桌面应用主逻辑</td>
      <td><code class="language-plaintext highlighter-rouge">src-tauri/src/lib.rs</code></td>
    </tr>
    <tr>
      <td>前端用户界面</td>
      <td><code class="language-plaintext highlighter-rouge">index.html</code>（HTML/CSS/JavaScript）</td>
    </tr>
  </tbody>
</table>]]></content><author><name>Thatgfsj</name></author><category term="网络" /><category term="认证" /><category term="PowerShell" /><category term="逆向" /><summary type="html"><![CDATA[深入解析校园网 Portal 认证协议，以及如何用纯脚本绕过浏览器实现自动登录。涵盖协议分析、参数详解、安全设计、连通性验证等完整技术细节。]]></summary></entry></feed>