跳至主要内容

自定义 Captive Portal:HTML 与 CSS 指南

本权威技术参考指南概述了设计和编写自定义 Captive Portal 登录页面所需的开发标准、CSS 架构和网络级限制。它为前端开发人员和网络架构师提供了切实可行的策略,以应对 Apple CNA 和 Android webview 环境,确保提供像素级完美、合规且高性能的访客 WiFi 体验。

📖 11 分钟阅读📝 1,348 🔧 2 应用实例3 练习题📚 8 关键定义

收听本指南

查看播客转录
自定义 Captive Portal:HTML 与 CSS 指南 — Purple 技术简报 [INTRODUCTION] 欢迎阅读 Purple 技术简报系列。今天,我们将深入探讨影响每一个访客 WiFi 部署的细节 — Captive Portal。具体来说,我们将讨论如何为自定义 Captive Portal 落地页编写整洁、可靠的 HTML 和 CSS。 如果您曾经连接过酒店 WiFi,并看到一个损坏的登录页面 — 丢失图像、无样式的文本、对触摸没有响应的登录按钮 — 您就体验到了开发人员在不了解其运行环境限制的情况下构建门户网站时会发生什么。今天,我们将确保这种情况不会发生在您身上。 本简报面向前端开发人员、创意设计师和网页开发人员,他们要么正在从头开始构建 Captive Portal,要么正在自定义现有模板。我们将介绍 HTML 结构、重要的 CSS 规则、即使是经验丰富的开发人员也会遇到的 Apple CNA 微型浏览器限制,以及像 Purple 的门户构建器这样的平台如何能够完全消除大部分这些复杂性。 让我们开始吧。 [TECHNICAL DEEP-DIVE] 首先,让我们从网络层面确定什么是 Captive Portal。当设备连接到需要身份验证的 WiFi 网络时,网络会拦截 HTTP 流量并将用户重定向到落地页。这就是 Captive Portal。用户看到一个登录页面,完成一个操作 — 输入电子邮件、接受条款、通过社交媒体登录 — 然后网络就会授予完整的互联网访问权限。 需要理解的关键是该页面在何处渲染。在 iOS 设备上,它在 Apple 的 Captive Network Assistant (CNA) 中打开 — 这是一个精简的 WebKit webview。它不是 Safari。它没有持久的 cookie。它无法加载外部资源。它的 JavaScript 支持有限。而且一旦用户切换到另一个应用程序,它就会关闭。在 macOS 上,CNA 以固定的 900 x 572 像素进行渲染。在 Android 上,现代设备使用 Chrome Custom Tabs,其功能要强大得多。Windows 10 会打开用户的默认浏览器。三星设备使用 Samsung Internet。 这种平台碎片化是生产环境中 Captive Portal 损坏的最大单一原因。开发人员在他们的 Android 手机上进行测试,一切看起来都很棒,然后酒店持 iPhone 的客人们就会看到一个带有无样式文本的白屏。因此,让我们来谈谈如何进行防御性编码。 Captive Portal HTML 和 CSS 的黄金法则是:将页面视为没有互联网连接。因为在身份验证阶段,它确实没有连接。网络是受限的。您的页面尝试从外部 URL 加载的任何资源 — Google Font、CDN 托管的样式表、JavaScript 库、Logo 图像 — 都会静默失败,或者导致加载微标永远无法解析。 首先是 HTML 结构。您的文档应该是一个干净的 HTML5 页面。在 head 部分,您需要一个 viewport 视口元标签,其 content 属性设置为 width 等于 device-width,且 initial-scale 等于 1。这对于移动端渲染是不可妥协的要求。如果没有它,iOS 将会以 980 像素的宽度渲染页面并将其缩小,导致所有内容都变得极其微小。 您的 CSS 必须是内联的 - 可以放在 head 元素内的 style 标签块中,也可以作为单个元素上的 inline style 样式属性。切勿使用通过 link 标签链接的外部样式表。因为该样式表存在于您的服务器上,在身份验证期间,Captive Portal 无法访问它,导致页面渲染时完全没有样式。 对于字体,请使用系统字体栈。例如:font-family - apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif。这会告诉浏览器使用任何可用的系统字体。切勿使用 Google Fonts。导入调用会失败,您的备用字体将变成 Times New Roman,这可不是您的客户付费想要获得的品牌体验。 对于图像 - 您的 Logo、背景图形、装饰元素 - 您有两种选择。要么从同一个 Captive Portal 服务器提供服务,这意味着它们位于同一个本地网络中,并且在身份验证完成之前即可访问。或者,更好的方法是,直接在 HTML 或 CSS 中将它们编码为 Base64 数据 URI。这可以完全消除外部依赖。 现在我们来谈谈页面布局。由于超过百分之九十的 Captive Portal 登录发生在移动设备上,您的设计应该采用移动优先。这意味着采用单列布局,最大宽度在 480 像素左右,居中显示。在 body 元素上使用 flexbox - display flex, flex-direction column, align-items centre, justify-content centre, min-height 100 viewport height。这可以在任何屏幕尺寸上将您的内容卡片在水平和垂直方向上居中。 您的主要行动呼吁按钮需要便于触摸点击。Apple 的《人机界面指南》指定了最小 44 乘 44 像素的点击目标。在实际操作中,对于主要 CTA,您可能需要更接近 48 像素的高度、容器内全宽、以及 8 到 12 像素左右的 border-radius 圆角。 对于表单字段 - 电子邮箱输入、姓名输入 - 请将 font-size 字体大小设置为至少 16 像素。这至关重要。iOS Safari 和 CNA 会自动放大任何 font-size 低于 16 像素的输入框,这会破坏您精心设计的布局。将 font-size 设置为 16 像素或以上可以防止这种缩放行为。 法律合规同意部分值得特别关注。根据 GDPR,如果您正在收集个人数据 - 哪怕只是一个电子邮件地址 - 您也需要获得明确且知情的同意。这意味着需要一个默认不勾选的复选框,并配有清晰说明用户同意内容的可见标签。切勿预先勾选该复选框。同意复选框本身必须清晰可见,无需滚动页面即可看到。 现在来看一个专门针对 iOS CNA 的关键实现细节。当用户完成身份验证时,CNA 会监测受控域名是否已变得可访问。该检测是由完整的页面导航触发的,而不是由 JavaScript AJAX 调用触发的。这意味着如果您构建了一个单页应用,通过 fetch 或 XMLHttpRequest 提交表单并在不进行完整页面重定向的情况下更新 DOM,CNA 将永远无法检测到身份验证已完成。您必须在身份验证后重定向到新的 URL - 这是一个完整的 HTTP 重定向,而不是 JavaScript DOM 操作。这是 captive portal 开发中最常见的错误之一。 对于 JavaScript,请保持极简。CNA 的 JS 支持有限,并且无法访问 localStorage 或 sessionStorage。当 CNA 关闭时,Cookie 会被销毁。任何依赖这些浏览器 API 的状态管理都将失败。原生 JavaScript 事件监听器是没有问题的。jQuery 是一个 30 KB 的外部依赖项,将会加载失败。 [实现建议与常见陷阱] 让我为您提供一份实用的实现清单。第一:始终设置 viewport 元标签。第二:所有 CSS 采用内联样式,不要使用外部样式表。第三:所有图片要么从 captive portal 服务器提供,要么进行 Base64 编码。第四:使用系统字体栈,不使用网络字体。第五:所有输入字段的最小字体大小为 16 像素。第六:便于触摸的点击目标,最小为 44 x 44 像素。第七:单栏布局,最大宽度 480 像素。第八:身份验证后进行整页重定向,而不是 JavaScript 状态更新。第九:符合 GDPR 的同意复选框,默认不勾选。第十:在真实的 iOS 设备上使用实际的 captive 网络进行测试,而不是浏览器预览。 我在生产环境中经常看到的一些陷阱。第一:Google Fonts - 请删除此导入,它会加载失败。第二:外部 JavaScript 库 - Bootstrap、jQuery 等任何 CDN 托管的脚本都会失败。第三:在外部样式表中声明的 CSS 变量 - 它们必须写在您的内联样式块中。第四:通过 URL 引用的背景图片 - 请对其进行 Base64 编码。第五:没有进行身份验证后重定向的 AJAX 表单提交 - CNA 将无法检测到身份验证的完成。 现在,我们来坦诚地聊聊自建还是购买。从头开始构建自定义 captive portal 意味着您还要负责后端基础设施 - RADIUS 服务器、数据库、SSL 证书、DNS 配置、与接入点的网络集成以及持续的安全补丁。这是一项重大的工程投入。 Purple 的门户构建器为需要像素级精准控制的开发人员提供了一个拖拽式界面以及自定义 HTML 和 CSS 编辑器,同时处理了所有后端基础设施 - 身份验证、数据捕获、分析、GDPR 合规工具以及与 200 多家接入点厂商的网络集成。您可以在没有基础设施开销的情况下获得创意控制权。 [快速问答] 我可以在 captive portal 中使用 CSS Grid 吗?可以,但需特别在 iOS CNA 上进行测试。Flexbox 在较旧的 WebKit 版本中具有更广泛的支持。我可以使用 SVG 标志吗?可以,内联 SVG 得到完全支持,且比 Base64 编码的 PNG 标志更理想,因为它们在视网膜屏幕上可以完美缩放。macOS CNA 是否支持与 iOS CNA 相同的限制?大致相同,但有一个区别:macOS CNA 会在一个固定的 900 x 572 像素窗口中渲染。我可以使用像 Tailwind 这样的 CSS 框架吗?只有在您生成了一个经过净化、自包含的 CSS 文件并将其内联到 style 块中时才可以。那么 HTTPS 呢?您的 captive portal 必须通过 HTTP 进行提供,以便初始重定向能够正常工作 - HTTPS 连接无法被 captive network 拦截。 [总结与后续步骤] 总结一下今天的简报。自定义 captive portal 是一个受限的网页环境,而不是标准的浏览器上下文。Apple CNA 和 Android webviews 对外部资源、cookie、JavaScript 和会话状态施加了严格的限制。解决方案是构建包含内联 CSS、系统字体、Base64 编码图像以及在认证时进行全页重定向的自包含 HTML 页面。 对于评估其选择的场馆运营商和 IT 团队:如果您的需求是一个完全品牌化、量身定制且带有自定义 HTML 和 CSS 的门户,那么选择就在于自己构建和维护整个技术栈(这是一项重大的工程投入),或者是使用像 Purple 这样在生产级后端基础设施之上提供自定义 HTML 和 CSS 编辑功能的平台。 接下来的步骤:审阅 Purple 的门户编辑器文档,对照我们今天介绍的移动优先清单审计您现有的门户,如果您是从头开始,请使用我们概述的 HTML 模板结构作为您的基准。感谢您的收听,我们下期简报再见。

📚 核心系列的一部分:Captive Portal 终极指南

header_image.png

核心摘要

对于企业级场所 - 从奢华酒店 酒店业 和零售连锁 零售业 到交通枢纽 交通运输 以及现代医疗园区 医疗保健 - 访客 WiFi 登录页面是其数字化门户。然而,超过 90% 的访客 WiFi 登录发生在移动设备上,这些设备上的页面渲染并非由 Safari 或 Chrome 等标准浏览器控制,而是受制于严格受限的 Captive Network Assistant (CNA) Webview [1]。这些“微型浏览器”实施了严格的安全沙盒限制:它们会阻止外部 CDN、禁用持久性 Cookie、忽略外部 Web 字体,并严格限制 JavaScript 的执行,以降低安全风险并防止会话劫持 [2]。

当开发人员使用传统的 Web 标准设计登录页面时,这些限制会导致布局错乱、品牌资产丢失以及登录按钮失效,直接影响客户满意度和数字化参与度。本指南提供了解决这些挑战的方案,介绍了防御性编码实践 - 例如内联 CSS、Base64 资源编码、系统字体栈以及显式的基于导航的身份验证握手 - 以确保无缝的跨平台渲染。此外,我们还将探讨如何利用像 Purple 的门户构建器这样的托管解决方案,使开发人员能够保持完整的 HTML/CSS 创意控制,同时将 RADIUS 身份验证、数据库扩展、GDPR/PCI 合规性以及多厂商 AP 集成工作卸载给平台处理 [3]。

技术深度剖析

为了构建一个具有弹性的自定义 Captive Portal,开发人员必须了解当访客关联到开放的服务集标识符 (SSID) 时发生的网络级拦截和浏览器虚拟化。

Captive Portal 生命周期

当客户端设备关联到 Captive SSID 时,会触发以下流程:

  1. IP 关联:设备完成 3 次握手,并通过 DHCP 请求 IP 地址。
  2. 活动连接探测:操作系统的后台网络管理器会立即向特定的厂商中立探针 URL(例如 Apple 的 http://captive.apple.com/hotspot-detect.html 或 Google 的 http://connectivitycheck.gstatic.com/generate_204)发送 HTTP GET 请求 [1]。
  3. DNS/HTTP 拦截:本地无线局域网控制器 (WLC) 或接入点 (AP) 会拦截此端口 80 的 HTTP 请求。网关不会返回预期的 HTTP 200 或 204 状态码,而是通过 HTTP 302 重定向将客户端的流量重定向到 Captive Portal 的着陆页 URL [2]。
  4. Webview 唤起:检测到重定向后,操作系统会唤起其原生的 Captive Network Assistant (CNA) 微型浏览器以显示重定向的登录页面,从而无需用户手动打开完整浏览器。
  5. 身份验证与状态转换:用户填写登录表单并向门户服务器提交凭据。随后,门户服务器会指示网关(通常通过 RADIUS Access-Accept 或外部 API 调用)对 MAC 地址进行授权。
  6. CNA 退出握手:CNA 微型浏览器向其金丝雀(canary)URL 重新发送 HTTP GET 请求。如果收到预期的 200/204 响应,它会将其右上角的按钮从“取消”更改为“完成”,并将 WiFi 连接确立为主网络接口。

特定平台的微型浏览器限制

每个操作系统都在不同的 webview 环境中处理该生命周期,从而导致了高度碎片化的行为。下表详细列出了这些关键限制:

平台 / Webview 显示方式 持久性 Cookie 外部网络字体 JavaScript 执行 窗口尺寸 退出握手触发方式
Apple iOS CNA (Websheet) 微型浏览器弹窗 被阻止(关闭时销毁) 被阻止(离线) 受限(无 localStorage/sessionStorage) 响应式(设备宽度) 仅限全页面 HTTP 重定向 [1]
Apple macOS CNA (Captive Network Assistant) 微型浏览器弹窗 被阻止 被阻止 受限(无 alert/confirm 对话框) 固定 (900px x 572px) 仅限全页面 HTTP 重定向
Android (Google) (CaptivePortalLogin) 推送通知 -> Chrome Custom Tab 允许(与 Chrome 共享) 允许(如果已列入围墙花园白名单) 完全 响应式 自动 (Captive Portal API / 204 检测) [2]
Samsung Android (Samsung Internet) 推送通知 -> 微型浏览器 允许 允许 完全 响应式 自动
Windows 10/11(默认浏览器) 自动启动默认浏览器 允许(完整浏览器环境) 允许 完全 响应式 手动 / 自动

cna_constraints_comparison.png

规避 Apple CNA “完成”按钮陷阱的编码技巧

在自定义门户开发中,最常见的失败模式之一就是 iOS 设备上的 “完成”按钮陷阱。当用户进行身份验证时,iOS Websheet webview 必须检测到网络已不再受限。它是通过监测其后台金丝雀(canary)请求是否成功来实现这一点的。

至关重要的是,iOS CNA 仅会在整页 HTTP 导航(位置重定向)时触发此检查。如果开发人员构建了一个现代单页面应用 (SPA),该应用通过异步 AJAX 调用(例如 fetch()Axios)提交表单数据,并在不更改 URL 的情况下动态更新 DOM,则 CNA 将 永远不会 重新运行其连接性检查。用户将在网关级别通过身份验证,但右上角的 CNA 按钮将保持为“取消”。如果感到沮丧的用户点击“取消”,iOS 设备将立即断开与 SSID 的连接,从而终止 WiFi 会话 [1]。

为了防止这种情况,身份验证成功处理器 必须 执行指向物理落地页的整页重定向(例如 window.location.href = '/success'),或者通过标准的 HTTP POST 操作原生提交登录表单。

实施指南

为了确保在所有平台上的呈现一致,开发人员必须从现代、资产繁重的 Web 设计过渡到高度自包含、防御性的编码风格。

黄金法则:针对零互联网连接进行设计

在受限状态下,客户端设备无法访问更广泛的互联网。它只能解析和访问无线控制器有围墙的花园Walled Garden)中明确加入白名单的 IP 地址和域名(例如 Captive Portal 服务器本身的 IP)。因此,HTML 中引用的任何外部资产都将加载失败,从而导致布局损坏。

为了进行防御性设计,请实施以下移动优先 Captive Portal 设计清单

mobile_first_checklist.png

1. 视口配置

为了防止移动设备将视口缩小到桌面宽度(通常为 980px),HTML <head> 必须包含一个响应式视口 meta 标签。如果没有这个,文本和输入字段在移动设备上会显得极其微小:

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

2. 内联 CSS 并移除外部依赖项

切勿链接到外部 CSS 文件或 CDN(例如 Bootstrap、Tailwind 或 Google Fonts)。所有 CSS 必须嵌入在 HTML <head><style> 块中。

对于排版,请利用引用预安装的本地操作系统字体的系统字体栈。这完全避免了 HTTP 请求,并确保使用设备的原生、高质量无衬线字体瞬间加载页面:

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    background-color: #F5F1ED; /* 珍珠白品牌色 */
    color: #011638; /* 深蓝品牌色 */
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
}

3. Base64 资源编码

除非托管域名已列入网关围墙花园(Walled Garden)的白名单,否则不应通过外部 HTTP URL 引用图像、徽标和图标。最可靠的方法是将小型视觉资源(例如 PNG 或 SVG 徽标)作为 Base64 数据 URI 直接编码到 HTML 或 CSS 中:

<!-- 内联 Base64 徽标示例 -->
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgZmlsbD0iIzc0NThGRCIvPjwvc3ZnPg==" alt="Purple Logo" class="brand-logo">

4. 输入框与点击目标优化

为了符合移动设备易用性标准(例如 Apple 的《人机接口指南》),所有交互式元素都必须针对触摸输入进行优化:

  • 输入框字体大小:输入框文本字体大小必须至少为 16px。如果字体大小较低(例如 14px),iOS Safari 和 CNA 将在聚焦时自动放大输入框,从而导致页面布局变形 [1]。
  • 点击目标:按钮、复选框和链接的最小触摸目标区域必须为 44px x 44px,以防止误触或用户产生挫败感。

5. 法律同意与 GDPR 合规性

在捕获访客数据(例如电子邮件、电话号码或社交资料)时,HTML 结构必须支持明确的法律同意。在 GDPR 规定下,预先勾选的复选框或默示同意声明(例如“登录即表示您同意我们的条款”)均属于违规行为 [3]。

  • 提供一个默认未勾选的复选框以用于服务条款和隐私政策。
  • 将详细的法律文本包裹在一个可滚动且限制高度的容器中(max-height: 120px; overflow-y: auto;),以使主要的登录表单保持在折叠线(Above the Fold)上方。
<div class="consent-container">
    <input type="checkbox" id="terms_consent" name="terms_consent" required>
    <label for="terms_consent">
        我明确同意 <a href="#" onclick="showTerms()">服务条款</a> 并同意根据 <a href="#" onclick="showPrivacy()">隐私政策</a> 处理我的数据。
    </label>
</div>

自定义 Captive Portal HTML/CSS 模板

以下是一个完整的、可直接用于生产环境的单文件 HTML/CSS 模板,其中融合了所有移动优先、符合 CNA 规范的最佳实践。它具有响应式布局、系统字体栈、支持 Base64 的资源占位符、触摸友好的输入框以及明确且符合 GDPR 要求的同意结构:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>Guest WiFi Login</title>
    <style>
        /* CSS 重置与变量声明 */
        :root {
            --color-primary: #7458FD; /* Purple 品牌颜色 */
            --color-dark: #011638; /* 深蓝 */
            --color-light: #F5F1ED; /* 珍珠白 */
            --color-white: #FFFFFF;
            --color-border: #D1D5DB;
            --radius-container: 12px;
            --radius-element: 6px;
        }
        
        * {
            box-sizing: border-box;
            -webkit-tap-highlight-color: transparent;
        }
        
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            background-color: var(--color-light);
            color: var(--color-dark);
            margin: 0;
            padding: 20px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
        }
        
        /* 响应式卡片布局 */
        .portal-card {
            background-color: var(--color-white);
            width: 100%;
            max-width: 420px;
            padding: 32px 24px;
            border-radius: var(--radius-container);
            box-shadow: 0 4px 20px rgba(1, 22, 56, 0.05);
            text-align: center;
        }
        
        .logo-container {
            margin-bottom: 24px;
        }
        
        /* 嵌入式 Base64 Logo 占位符 */
        .logo {
            width: 120px;
            height: auto;
        }
        
        h1 {
            font-size: 24px;
            font-weight: 700;
            margin: 0 0 8px 0;
            color: var(--color-dark);
        }
        
        p {
            font-size: 14px;
            color: #4B5563;
            margin: 0 0 24px 0;
            line-height: 1.5;
        }
        
        /* 表单样式 */
        .form-group {
            text-align: left;
            margin-bottom: 16px;
        }
        
        label {
            display: block;
            font-size: 14px;
            font-weight: 600;
            margin-bottom: 6px;
            color: var(--color-dark);
        }
        
        /* 关键:font-size 必须至少为 16px,以防止 iOS 自动缩放 */
        input[type="email"],
        input[type="text"] {
            width: 100%;
            height: 48px;
            padding: 12px 16px;
            font-size: 16px;
            border: 1px solid var(--color-border);
            border-radius: var(--radius-element);
            background-color: var(--color-white);
            color: var(--color-dark);
            outline: none;
            transition: border-color 0.2s;
        }
        
        input[type="email"]:focus,
        input[type="text"]:focus {
            border-color: var(--color-primary);
            box-shadow: 0 0 0 3px rgba(116, 88, 253, 0.15);
        }
        
        /* 同意条款样式 */
        .consent-group {
            display: flex;
            align-items: flex-start;
            text-align: left;
            margin: 20px 0;
        }
        
        /* 便于触摸的目标尺寸 */
        input[type="checkbox"] {
            width: 20px;
            height: 20px;
            margin: 2px 12px 0 0;
            flex-shrink: 0;
            cursor: pointer;
            accent-color: var(--color-primary);
        }
        
        .consent-label {
            font-size: 13px;
            line-height: 1.4;
            color: #4B5563;
            user-select: none;
        }
        
        .consent-label a {
            color: var(--color-primary);
            text-decoration: none;
            font-weight: 600;
        }
        
        .consent-label a:hover {
            text-decoration: underline;
        }
        
        /* Touch-Friendly CTA Button (Minimum 44px height) */
        .btn-submit {
            width: 100%;
            height: 48px;
            background-color: var(--color-primary);
            color: var(--color-white);
            border: none;
            border-radius: var(--radius-element);
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: background-color 0.2s;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        .btn-submit:hover {
            background-color: #5B3EE3;
        }
        
        .btn-submit:active {
            background-color: #4A2FD4;
        }
        
        /* Legal Terms Scrollbox */
        .terms-scrollbox {
            display: none; /* Toggled via JS */
            text-align: left;
            font-size: 11px;
            color: #6B7280;
            background-color: var(--color-light);
            padding: 12px;
            border-radius: var(--radius-element);
            max-height: 100px;
            overflow-y: auto;
            margin-bottom: 16px;
            border: 1px solid var(--color-border);
        }
        
        .footer {
            margin-top: 24px;
            font-size: 11px;
            color: #9CA3AF;
        }
    </style>
</head>
<body>

    <div class="portal-card">
        <div class="logo-container">
            <!-- Replace src with your actual Base64-encoded SVG or PNG -->
            <svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 30">
                <rect width="100" height="30" rx="6" fill="#7458FD"/>
                <text x="50" y="20" font-family="sans-serif" font-size="12" fill="#FFFFFF" font-weight="bold" text-anchor="middle">您的品牌</text>
            </svg>
        </div>
        
        <h1>欢迎使用访客 WiFi</h1>
        <p>请在下方输入您的详细信息,以获取安全、高速的互联网接入。</p>
        
        <!-- Form must submit natively (full page navigation) to clear Apple CNA -->
        <form action="/login/submit" method="POST" onsubmit="return validateForm()">
            <div class="form-group">
                <label for="guest_name">姓名</label>
                <input type="text" id="guest_name" name="name" placeholder="张三" required autocomplete="name">
            </div>
            
            <div class="form-group">
                <label for="guest_email">电子邮件地址</label>
                <input type="email" id="guest_email" name="email" placeholder="john@example.com" required autocomplete="email">
            </div>
            
            <div class="consent-group">
                <input type="checkbox" id="terms_consent" name="terms_accepted" required>
                <label for="terms_consent" class="consent-label">
                    我接受 <a href="#" onclick="toggleTerms(event)">服务条款</a> 并同意按照 GDPR 法规处理数据。
                </label>
            </div>
            
            <div id="terms_box" class="terms-scrollbox">
                <strong>WiFi 服务条款:</strong><br>
                1. 本服务按现状提供,不作任何保证。<br>
                2. 用户不得从事非法的、占用大量带宽的活动。<br>
                3. 我们仅根据我们的隐私政策收集个人数据,用于身份验证和营销选择性加入。
            </div>
            
            <button type="submit" class="btn-submit">连接至 WiFi</button>
        </form>
        
        <div class="footer">
            由 Purple 提供支持 | 安全访客 WiFi
        </div>
    </div>

    <script>
        function toggleTerms(e) {
            e.preventDefault();
            var box = document.getElementById('terms_box');
            box.style.display = (box.style.display === 'block') ? 'none' : 'block';
        }
        
        function validateForm() {
            var consent = document.getElementById('terms_consent');
            if (!consent.checked) {
                alert('您必须接受服务条款才能进行连接。');
                return false;
            }
            return true;
        }
    </script>
</body>
</html>

故障排除与风险缓解

在部署自定义代码编写的 HTML/CSS Captive Portal 时,IT 运维团队经常会遇到几种严重的运营风险:

1. SSL/TLS 证书警告循环

由于 Captive Portal 的工作原理是拦截流量,因此它们与现代 HTTPS 网络安全存在根本性的冲突。当用户尝试访问 HTTPS 网站(例如 https://www.google.com),且网关尝试将该流量重定向到 HTTP Captive Portal 时,浏览器会检测到 SSL 证书不匹配,并显示严重的“您的连接不是私密连接”安全警告。

  • 缓解措施:切勿尝试直接拦截 HTTPS 流量。完全依赖操作系统原生的 CNA 助手(它会发送未加密的 HTTP 请求来触发重定向)。确保您的 Captive Portal 域名拥有有效的、公开受信的 SSL 证书(例如 Let's Encrypt 或 DigiCert),并且仅在初始 HTTP 重定向成功将用户路由到您的门户域名之后,才通过 HTTPS 进行服务 [2]。

2. DNS 解析失败(围墙花园陷阱)

如果您的自定义 HTML 页面引用了外部资源(例如社交登录 OAuth 端点(如 Facebook、Google)或支付网关),则针对这些域的 DNS 请求将会失败,除非在无线控制器的 Walled Garden 中将其明确加入白名单。如果白名单中缺少某个域,登录流程将会停滞,并显示空白屏幕。

  • 缓解措施:维护一个严格、精简的 Walled Garden 列表。如果使用社交登录,请将身份提供商推荐的特定通配符域(例如 *.google.com*.gstatic.com)加入白名单。

3. 会话超时与 MAC 地址欺骗漏洞

标准的 Captive Portal 基于设备的 MAC 地址对设备进行身份验证。然而,现代移动操作系统(iOS 14+ 和 Android 10+)默认使用随机 MAC 地址(私有 WiFi 地址)并定期轮换。这可能会导致访客反复被要求重新进行身份验证,从而极大地损害用户体验 [1]。

  • 缓解措施:在 RADIUS 服务器上设置合理的会话超时(例如 24 小时)以防止过期会话,并采用 Passpoint (Hotspot 2.0)WPA3-Enterprise 等现代身份验证标准,以实现无缝、安全的入网,从而完全绕过基于 MAC 的 Captive Portal。

Purple 产品相关性:自建还是购买

虽然编写单个 HTML 页面非常简单,但托管、保护和扩展自定义 Captive Portal 基础设施会面临巨大的技术和合规性障碍。下表对比了自建托管自定义门户与使用 Purple 的托管企业平台在工程和运营方面的实际情况:

功能 / 运营需求 自建托管自定义门户 Purple 企业 WiFi 平台
HTML/CSS 自定义 完全手动编码,将文件上传到单个 AP 或本地 Web 服务器。 像素级完美的开发人员编辑器,允许自定义 HTML/CSS 注入,并结合拖放式可视化构建器。
RADIUS 基础设施 必须部署、配置和维护高可用性的 FreeRADIUS 或 Cloud RADIUS 服务器 [4]。 内置、全球分布且云原生的 RADIUS,具有双活冗余和 99.99% 的可用性 SLA。
多厂商 AP 支持 每个硬件厂商(Cisco、Aruba、Meraki、Ruckus)都需要自定义集成脚本 [5]。 与 200 多种硬件型号原生、开箱即用集成;在混合硬件资产中统一进行门户部署。
数据隐私与合规性 场所承担 GDPR、CCPA 和 PCI-DSS 合规性的 100% 法律责任,包括安全的数据库加密和数据删除工作流。 设计上完全合规。内置同意管理、自动化的数据主体删除请求以及安全的 ISO 27001 认证托管。
身份提供商集成 与 Google、Facebook、Apple 和本地短消息网关的手动 OAuth2 集成。 与主要社交平台、短信网关以及适用于企业访客的 Azure AD / Okta 进行一键集成

Purple 的平台解决了“自建还是购买”的难题。它为开发人员提供了自定义 HTML/CSS 工作区的完整创意自由,同时消除了支持大规模安全 RADIUS 身份验证所需的复杂、高风险后端基础设施工程。

投资回报率与业务影响

投资于专业设计、响应式的自定义 Captive Portal,可在 IT 运营、营销和法律合规性方面带来可衡量的回报。

1. 降低运营成本(IT 服务台工单)

在大规模部署(如体育场或多站点零售连锁店)中,损坏的 Captive Portal 是导致 IT 服务台升级的主要原因。当访客遇到“白屏”或无响应的登录按钮时,他们会使现场工作人员不堪重负或提交支持工单。

$$\text{年度支持节省} = (\text{年度访客总数} \times \text{门户故障率} \times \text{服务台联系率}) \times \text{每张支持工单成本}$$

  • 场景:一个年访客量为 1,000,000 人的会议中心。一个编写拙劣的门户在较旧的 iOS 设备上具有 5% 的故障率,导致 10% 的服务台联系率。按行业标准的每张支持工单 15 美元计算,运营成本为: $$(1,000,000 \times 0.05 \times 0.10) \times $15 = \text{每年 } 75,000 \text{ 美元可避免的支持开销}$$
  • 结果:过渡到针对 CNA 优化的移动优先模板可将门户故障率降低至 <0.1%,几乎消除了这一运营消耗。

2. 营销数据收集与选择性加入优化

对于零售和酒店场所,访客 WiFi 门户是捕获干净的第一方客户数据的主要机制。设计糟糕的用户界面、微小的文字或笨拙的表单布局会导致高跳出率 - 用户完全放弃登录过程,从而导致失去营销机会。

  • 案例研究(零售):一家国家零售连锁店实施了利用 Purple 平台优化的移动优先 Captive Portal。通过将多步骤登录表单替换为单字段电子邮件输入(字体大小:16px)和优化的 48px 点击目标按钮,他们在第一季度内实现了完成注册量增加 42% 以及营销通讯选择性加入量增加 28% [6]。

3. 减轻法律和监管风险

在 GDPR 和 CCPA 的规定下,不合规的数据收集会带来严重的经济处罚(在 GDPR 下高达全球年营业额的 4%)。依赖预先勾选的复选框或未能登入页面(splash page)上提供清晰且易于访问的隐私政策,会使企业面临巨大的法律责任。

  • 降低风险的投资回报率 (ROI):实施明确且未预先勾选的同意复选框,并在优化的滚动框中托管条款,可确保 100% 遵守法规,从而降低数百万美元监管罚款的风险并保护品牌声誉。

核心要点总结

  • CNA 沙箱具有限制性:Apple 的 iOS Websheet 和 macOS CNA 是高度沙盒化的环境,会阻止外部资产、cookie 和网络字体。所有样式和资产必须是自包含的(内联 CSS、Base64 图像、系统字体)[1]。
  • AJAX 会破坏 iOS 退出握手:要成功将 iOS 设备从“受限”过渡到“已连接”状态(将右上角的按钮从“取消”更改为“完成”),您必须触发全页面 HTTP 重定向。异步 DOM 更新将使设备陷入受限循环。
  • 移动优先是强制性的:超过 90% 的登录发生在移动端。设计单栏布局(最大宽度:480px),利用易于触控的点击目标(最小 44px x 44px),并在所有文本输入框上强制执行最小 16px 的字体大小,以防止 iOS 浏览器自动缩放。
  • 围墙花园(Walled Gardens)控制 DNS:登录期间引用的任何外部域名(例如,社交登录 API)都必须在无线控制器的围墙花园中明确列入白名单,否则页面将无法加载。
  • Purple 消除后端复杂性:利用 Purple 的门户生成器,开发人员可以通过自定义编辑器获得完整的 HTML/CSS 控制权,同时减轻 RADIUS、多厂商 AP 集成和符合 GDPR 的数据库管理所带来的巨大安全、扩展和合规负担 [3]。

参考文献


收听技术简报

收听资深解决方案架构师讨论自定义 Captive Portal 的技术限制和实施策略:

关键定义

Captive Portal

在向新连接的 WiFi 网络用户授予更广泛的网络资源访问权限之前,向其展示的网页,通常用于身份验证、付费或显示服务条款。

IT 团队在网关级别部署 Captive Portal,以控制访客访问、捕获用户数据并执行法律合规性。

Captive Network Assistant (CNA)

当操作系统(例如 Apple iOS 和 macOS)检测到 Captive Portal 重定向时自动生成的、高度受限且处于沙盒环境中的微型浏览器,其唯一目的是为了协助门户进行身份验证。

CNA 网页视图(Webview)执行严格的限制,包括阻止外部 CDN、持久性 Cookie 和本地存储,这经常会破坏标准网页设计。

Walled Garden

一个受限的 IP 地址、子网或域名列表,未经验证的访客用户在完成 Captive Portal 登录过程之前,被允许通过网关访问这些内容。

开发人员必须确保任何外部资源(例如社交登录 API 或支付网关)都在 Walled Garden 中列入白名单,以防止登录流程中断。

Base64 编码

一种二进制到文本的编码方案,将二进制数据(例如图像)表示为 ASCII 字符串,允许将资产直接嵌入到 HTML 或 CSS 文档中。

利用 Base64 编码来处理徽标和图标可以消除外部 HTTP 请求,确保资产在离线 CNA 环境中完美渲染。

RADIUS (Remote Authentication Dial-In User Service)

一种网络协议,为连接和使用网络服务的用户提供集中的身份验证、授权和计费(AAA)管理。

一旦满足身份验证标准,Captive Portal 服务器就会与 RADIUS 服务器通信,在网络网关处授权访客的 MAC 地址。

系统字体栈

一种 CSS font-family 声明,它优先使用预安装的操作系统字体(例如 iOS 上的 San Francisco、Windows 上的 Segoe UI 和 Android 上的 Roboto),而不是外部 Web 字体。

实现系统字体栈可确保立即渲染排版,而不会触发对 Google Fonts 等服务的被阻止的外部 HTTP 请求。

Canary URL

由操作系统供应商维护的专用、未加密的 HTTP URL(例如 captive.apple.com),用于测试设备是否具有不受限制的互联网连接。

操作系统后台网络管理器通过检查此 URL 来检测是否存在 Captive Portal 并触发 CNA Webview 弹窗。

Passpoint (Hotspot 2.0)

由 Wi-Fi Alliance(Wi-Fi 联盟)开发的一种行业标准,使移动设备能够自动发现 WiFi 热点并与其进行安全身份验证,从而绕过手动的 Captive Portal 登录。

企业将 Passpoint 与 Purple 等平台结合使用,将访客从繁琐的欢迎页面过渡到无缝、类似于蜂窝网络的安全漫游体验。

应用实例

一家拥有 250 间客房的豪华连锁酒店 [Hospitality](/industries/hospitality) 希望实施一个与其高端品牌指南完美契合的自定义访客 WiFi 登录页面。其创意代理机构设计了一个展示页面,其中使用了自定义品牌字体(托管在 Adobe Fonts 上)、多个高分辨率背景图片(托管在公共 AWS S3 存储桶上)以及多步骤动画 JavaScript 向导。部署后,iOS 访客连接到 SSID,但弹出的门户是一个空白的白色屏幕,用户无法进行身份验证。

要解决白屏和品牌展示失效的问题,我们必须重构门户的前端架构,以符合 Apple CNA 沙盒限制:

  1. 字体修复:由于 Adobe Fonts 需要外部 HTTP 请求,而该请求会被 CNA 拦截,我们将自定义字体调用替换为原生的优质系统字体栈(font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;)。这确保了无需外部网络调用即可立即渲染。
  2. 资源优化:AWS S3 上的背景图片被拦截,因为 S3 不在网关的围墙花园内。我们压缩主品牌标志,将其转换为轻量级的 SVG,并作为 Base64 Data URI 直接编码在 HTML 中。对于背景,我们用使用酒店品牌色彩的干净、响应式 CSS 渐变取代了沉重的图片,从而大幅减轻了页面权重。
  3. JavaScript 简化:多步骤动画向导依赖于外部 jQuery 和 GSAP 库。我们去除了这些外部依赖项,并将表单重构为单页、单列的 HTML 结构。表单验证使用轻量级的原生 JavaScript 重写。
  4. 身份验证握手:表单提交从基于 AJAX 的提交修改为原生 HTML <form action="/submit" method="POST">,以触发全页重定向,从而允许 iOS Websheet 执行其 Canary 测试并显示“完成”按钮。
考官评语: 此场景代表了高端创意设计与 Captive webview 的严格安全限制之间的经典冲突。创意代理机构经常将 Captive Portal 视为标准的桌面网站。然而,由于设备处于未身份验证状态,网络会拦截所有外部流量。通过内联 CSS、使用系统字体栈、Base64 编码资源以及利用原生表单提交,我们在保留高端品牌美感的同时,在 iOS 和 Android 设备上实现了 100% 的运行可靠性。

一家拥有 450 家门店的全国零售连锁店 [Retail](/industries/retail) 希望通过 WiFi 展示页面收集访客电子邮件以填充其 CRM。他们要求访客选择加入营销新闻通讯。初始设计有一个预先勾选的“我同意接收营销电子邮件”复选框。此外,该门户托管在其总部的单个本地服务器上。在高峰时段(周六下午),全国各地的访客都遇到了严重的延迟,许多人无法加载登录页面,导致数据捕获率大幅下降。

我们必须同时解决合规性违规和基础设施瓶颈问题:

  1. 合规整改:根据 GDPR 和 CCPA,预先勾选的同意框是非法的。我们修改 HTML,将营销同意复选框默认设置为未勾选(<input type="checkbox" id="marketing_consent">)。我们还为服务条款添加了一个单独的必填复选框,以将法律协议与营销订阅脱钩。
  2. 基础设施扩展:在单个集中式服务器上托管全国性 Captive Portal 会导致单点故障和巨大的延迟瓶颈。我们将门户前端迁移到具有边缘缓存的高可用、全球分布式的内容分发网络(CDN)。
  3. RADIUS 集成:我们配置本地门店接入点,使其指向具有双活冗余的云原生 RADIUS 集群,确保即使在周六的流量高峰期,身份验证请求也能在边缘本地处理,延迟低于 50 毫秒。
  4. Purple 迁移:为了消除这全部的工程开销,该零售商迁移到了 Purple。Purple 内置的 GDPR 同意工具可自动管理合规的订阅,其全球分布的云基础设施可以处理每日数百万次的身份验证,并保证 99.99% 的在线率,彻底解决了扩展瓶颈。
考官评语: 预先勾选的同意复选框代表了严重的合规风险,可能导致巨额的监管罚款。将营销同意与服务条款脱钩是技术和法律上的最佳实践。在基础设施方面,集中式托管 Captive Portal 是一种反模式。全国性的零售业务布局需要一个去中心化、边缘缓存的前端,并结合云原生的 RADIUS 后端。迁移到像 Purple 这样的托管平台消除了这种架构的复杂性,使零售商能够专注于营销活动,而不是数据库扩展。

练习题

Q1. 一家大型国际机场 [交通](/industries/transport) 的 IT 团队部署了自定义代码的 Captive Portal。他们注意到,虽然 Android 用户连接无缝,但很大一部分 iOS 用户遇到了成功通过身份验证但无法浏览网页的问题。仔细观察后发现,iOS 设备显示已连接到 SSID,但 Captive 弹窗右上角的按钮仍然显示“取消”而不是“完成”。此问题的根本原因是什么,开发人员应如何修复?

提示:分析 Apple CNA 助手如何检测网络已从受限转为已通过身份验证,以及触发此检查需要什么浏览器操作。

查看标准答案

根本原因是 Portal 的成功页面通过 JavaScript(AJAX/SPA 路由)动态更新 UI,而不是执行全页面 HTTP 导航。Apple iOS Captive Network Assistant (CNA) 微型浏览器仅在发生全页面 URL 重定向或导航时,才会重新运行其后台连接性检查(对 captive.apple.com 的 Canary 请求)。如果开发人员通过 AJAX 提交登录表单并仅在 DOM 中显示“成功”消息,CNA 将无法获知网络已解锁。因此,右上角的按钮仍为“取消”。如果用户点击“取消”退出,操作系统会认为登录失败并断开与 WiFi 网络的连接。

解决方案:开发人员必须修改身份验证成功处理程序以强制进行全页面重定向。这可以通过使用标准 HTML <form action="/submit" method="POST"> 原生提交登录表单,或者在 API 返回成功身份验证响应后在 JavaScript 中执行 window.location.href = '/success_landing_page' 来实现。这将触发所需的整页加载,强制 CNA 助手重新评估网络状态,验证 Canary URL 当前是否可达,并将右上角的按钮更改为“完成”。

Q2. 体育场运营团队 [活动] 希望推出一个可以收集营销订阅的访客 WiFi 网络。合规官坚持要求 Portal 必须 100% 符合 GDPR。开发团队展示了一个模型,其中登录表单有一个预先勾选的框,上面写着“我同意服务条款并同意接收营销新闻”。为什么这种设计不合规?在保持高转化率的同时,应如何重构 HTML/CSS 和表单结构以满足 GDPR 要求?

提示:考虑 GDPR 关于明确同意、营销订阅与服务条款解耦以及移动端屏幕上法律文本的物理可见性的严格要求。

查看标准答案

所提议的设计在两个主要方面违反了 GDPR:首先,预先勾选的复选框不构成有效的同意,同意必须是自由给予的、具体的、知情的且明确的。其次,将营销同意与同意服务条款捆绑在一起是不合规的;不能强制用户接受营销电子邮件作为使用 WiFi 服务的条件。

重构策略

  1. 解耦同意:将复选框拆分为两个独立的复选框。复选框 A 是必填的,涵盖服务条款和隐私政策。复选框 B 是可选的,涵盖订阅营销简报。
  2. 设为未勾选:确保在 HTML 中默认未勾选两个复选框(省略 checked 属性)。
  3. CSS 可见性:由于超过 90% 的用户使用移动端,请将复选框直接放置在“连接”按钮上方,以便无需滚动即可在“首屏”看到它们。使用系统字体系列,并将标签字号设置为 14px,行高设置为 1.4,以提高可读性。
  4. 条款滚动框:为了防止法律文本将表单元素挤出屏幕,请将详细的服务条款放置在一个固定高度的滚动容器中(max-height: 100px; overflow-y: auto; background-color: #F5F1ED; border: 1px solid #D1D5DB; border-radius: 6px;),该容器可以通过文本链接切换打开或关闭。这样既能保持干净、高转化率的布局,又能确保绝对的法律合规性。

Q3. 一家零售连锁店 [Retail](/industries/retail) 正在 100 家门店部署自定义编码的引导页面。设计师使用了 Google Fonts (Montserrat) 并连接到 HTML 头部 CDN 托管的 Bootstrap 样式表。在企业网络上进行测试期间,页面渲染得非常漂亮。然而,当部署在具有 Captive Portal 配置的测试门店 AP 上时,页面渲染出的文本却是未设置样式的 Times New Roman、对齐混乱并且缺少图标。为什么会发生这种情况,以及必须如何重构这些资产?

提示:分析用户在通过身份验证之前的网络连接状态,并确定浏览器如何处理指向围墙花园以外域名的外部 HTTP 请求。

查看标准答案

发生此故障是因为加载引导页面时,设备处于未认证的受限状态。在此状态下,无线网关会阻止所有出站互联网流量,仅允许请求网关围墙花园中明确列入白名单的域名。因为 Bootstrap 的 CDN 域名 (cdn.jsdelivr.net) 和 Google Fonts 的域名 (fonts.googleapis.com) 未列入白名单,浏览器获取样式表和字体文件的请求会静默失败。因此,浏览器回退到其默认渲染引擎,导致未设置样式的 HTML(Times New Roman 文本)和布局错乱。

重构策略

  1. 内联 CSS:移除外部 Bootstrap 样式表链接。将必要的 CSS 网格/弹性盒子规则直接复制到 HTML <head><style> 块中。这可以确保所有布局指令都在最初的单页负载中传递。
  2. 实施系统字体系列:移除 Google Fonts 的 @import<link> 调用。将其替换为 CSS 中的原生系统字体系列(font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;)。这会强制设备使用操作系统中已预装的高质量字体,从而完全消除对外部网络的依赖。
  3. Base64 编码图标/Logo:如果布局依赖外部图像或图标库(如 FontAwesome),请将这些图标转换为 SVG 格式,并将它们内联嵌入到 HTML 中,或作为 Base64 数据 URI 嵌入到 CSS 中。这保证了页面 100% 自包含,即使在完全没有互联网连接的情况下也能完美渲染。
自定义 Captive Portal:HTML 与 CSS 指南 | 技术指南 | Purple