ArubaOS8 Custom Internal Captive Portal快速入門

ArubaOS8 Custom Internal Captive Portal快速入門

客制化Captive Portal可以做到Logo、版型、編碼等自由搭配。對於中文用戶,這樣可以使中文正常顯示。

但是,一旦客製化,就會面臨有些關鍵html tag不知如何搭配才好。

然後Internal Captive Portal的Type又分為「帳密」、「email」、「不用帳密與email」,然後Custom Captive Portal又需要與這三種Type搭配,就需要Know-how了。

帳密對應Captive Portal Profile的User Login。
Email對應Captive Portal Profile的Guest Login。
無帳密與Email對應Captive Portal Profile的User Login&Guest Login兩者取消勾選。

以下內容將讓讀者可以在最快時間內,明白如何客製化出自己的Internal Captive Portal。

流程

  1. 決定Captive Portal的Type:帳密、Email、無帳密與Email
  2. 依據Type,從Controller上取得Template。
  3. 依據Template,調整成自己想要的版面。
  4. 上傳HTML,測試功能與微調。

概念

帳密、Email、無帳密與Email,在Html中,分別需要具備:

帳密:input「user」、input「password」。
Email:input「email」。
無帳密與Email:input「user」、input「password」。
※「」為name屬性,不是id屬性。

因此,帳密與無帳密與Email在驗證概念是相同的,只是前者的帳密需要使用者輸入,而後者的帳密則由html直接帶入,對使用者來說就無需輸入帳密。※無帳密與Email的使用的預設帳密需要先新增到Auth Server中。

input則是包含在form之下,當登入時,網頁會將所有input的資訊傳送給form的action,因此,action即為Controller的位置。而Internal Captive Portal的URL就是Controller的位置,所以一般來說Action只需要填入相對路徑(省略IP)即可。

action可以填入以下兩個其中一個,視情況調整:
「/cgi-bin/login」
「/auth/index.html/u」

一個最簡單的Captive Portal,如果是帳密,只需要做一個Form,設定action,然後新增input包含user、password,並且新增一個按鈕,讓Form能submit。

如果是email,只需要做一個Form,設定action,然後新增input包含email,並且新增一個按鈕,讓Form能submit。

其餘的功能可以視自己的需求或是別人的審美觀添加,只要確保核心功能能正常,自製Captive Portal將不會花太多時間除錯!

依據Level,從Controller上取得Template

ArubaOS中,針對三種Level的Captive Portal在內建都有提供Template,並且功能正常。

筆者在使用8.10.0.16以上時,發現內建的Captive Portal有功能異常,Preview時頁面空白且無html內容,因此改用8.6去提取出正常功能的Template。

要最快速提取Template的方法,就是新增一個Primary usage為Guest的WLAN。並且在Security選擇

透過Preview,可以看到內建的Internal Captive Portal Template,然後將網頁保存,基本上只需要html內容即可以。

內建的Internal Captive Portal Template其實具備不同Type的驗證能力,要用哪種?

在對應的style=”display: none”拿掉,input顯示在網頁上,就能使用。
如果type為帳密:將registered的style中的display: none拿掉。
如果type為帳密:將guest的style中的display: none拿掉。

以下為ArubaOS Internal Captive Portal Template,使用者需要依據需求調整。

<html xmlns="http://www.w3.org/1999/xhtml"><head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Portal Login</title>
    
    <link href="/auth/default1/styles.css" rel="stylesheet" media="screen" type="text/css">
<script language="javascript" type="text/javascript">

function cp_ua() {
    this.div_ids = ['user-agreement', 'logins'];
    this.cp_a_type = null;
    this.init = function (cp_a_type) {

        if(this.cp_a_type == null)
            this.cp_a_type = cp_a_type;
    };
    this.show_forms = function(isUa, isDisabled) {
        // noop
    };

    // When there is no credential or email input fields
    this.only_aup = function(requested_url) {

        var xform = document.getElementById("only_aup_form");
        var url = document.getElementsByName("requested_url")[0];
        url.value = requested_url;
        xform.submit();
    };
}
var CpUa = new cp_ua();

function validateLogin() {
    var aupBox = document.getElementById("user-agreement");
    var aupCheck = document.getElementById("agreement_agree");

    /* Allow login only if the user checked the AUP box,
       or if the AUP box is hidden (thus nothing to accept)
     */
    if (aupCheck.checked || aupBox.style.display === "none") {
       document.getElementById('regform').submit();
    }
}

function showPolicy(doShow) {
    var elem = document.getElementById('policyOverlay');
    elem.style.display = doShow ? '' : 'none';

    elem = document.getElementById('mask');
    elem.style.display = doShow ? '' : 'none';
}

function focus() {
    var focusElem;
    var boxElem = document.getElementById("registered");

    if (boxElem.style.display !== "none") {
        focusElem = document.getElementById("user");
    } else {
        boxElem = document.getElementById("guest");

        if (boxElem.style.display !== "none") {
            focusElem = document.getElementById("email");
        } else {
            boxElem = document.getElementById("user-agreement");
            if (boxElem.style.display !== "none") {
                focusElem = document.getElementById("agreement_agree");
            }
        }
    }

    if (focusElem) {
        focusElem.focus();
    }
}

window.onload = function () {
    try {
        focus();
    } catch (e) {
        window.console.error(e);
    }
};
</script>


</head>

<body style=" background-image:url();">
    <div class="wrapper">
        <div class="content">
            <img class="banner-image" style="" src="/auth/default1/hpe_aruba_w.png">

            <div id="instructions" style="display:none">
                <div id="instructionstext" style=""></div>
            </div>
            <form action="/cgi-bin/login" id="regform" method="post" autocomplete="off">
        		<div id="registered" style="display: none">

                    <div class="login-field">
                        <input type="text" id="user" name="user" placeholder="Username" size="25" onkeypress="if(event &amp;&amp; event.keyCode == 13){validateLogin();return false;}">
                    </div>
                    <div class="login-field">
                        <input type="password" id="password" name="password" placeholder="Password" size="25" onkeypress="if(event &amp;&amp; event.keyCode == 13){validateLogin();return false;}">
                    </div>
                    
                </div>

                <div id="guest" style="display: none">
                    <div id="emailbox" class="login-field">
                        <label for="email" accesskey="e">Email: </label>
                        <input type="text" id="email" name="email" placeholder="example@host.com" size="25" onkeypress="if(event &amp;&amp; event.keyCode == 13){validateLogin();return false;}">
                    </div>
                </div>

                <input type="hidden" name="cmd" value="authenticate">

                <div id="user-agreement" style="display">
                    <input type="checkbox" id="agreement_agree" name="agreementAck" value="Accept" onclick="">
                    <label for="agreement_agree" class="agreement-policy-label" style="">
                        I accept the <a class="agreement-policy-link" style="" href="javascript:void(0)" onclick="showPolicy(true); return false;">terms and conditions</a>
                    </label>
                </div>
                <div id="loginBox" style="display: none">
                    <a class="button-box login" href="javascript:void(0)" onclick="validateLogin();return false;">
                        <div name="login" class="button" style="">Sign in</div>
                    </a>
                </div>
            </form>
            <div style="display: none">
                <form action="login" id="only_aup_form" method="post" title="">
                <input type="hidden" name="accept_aup" value="accept_aup">
                <input type="hidden" name="requested_url" value="">
                </form>
            </div>
            <div id="errorbox" style="display: none">
                <img src="/images/alert-login-page.png">
                <span id="failreason">
                    
                </span>
            </div>
        </div>
    </div>
    <div id="policyOverlay" style="display:none;">
        <div class="wrapper">
            <div class="policy-content">
                <div class="policy-title">
                    Terms and Conditions
                </div>
                <div class="policy-text">
                    
                </div>
                <div class="button-row">
                    <a class="button-box" href="javascript:void(0)" onclick="showPolicy(false);return false;">
                        <div name="okPolicy" class="button" style="">Close</div>
                    </a>
                </div>
            </div>
        </div>
    </div>
    <div id="mask" style="display:none;"></div>
</body></html>

依據Template,調整成自己想要的版面

在過往,調整html成自己喜歡的樣子,需要學習html相關知識,練習,然後刻出。

現在,可以使用ChatGPT等大語言模型幫助。

為了讓大語言模型能順利調整成自己想要的樣子,我建議遵照以下的步驟

  • 收集調整方向的Sample,以圖片形式或是Html形式,讓大語言模型能知道怎麼改。
  • 版面、功能分離,即只需要讓大語言模型異動版面,勿讓大語言模型調整功能部分。
  • 自備Logo與說明文字,並且指示大語言模型擺入指定位置。
  • 若發現大語言模型調整的html不是最新的版本,則建立新的聊天。

使用範例

上傳Sample的圖片

<Aruba Internal Captive Portal Template 內容>

###
根據圖片,將以上HTML版面調整成相似風格,並且服務條款調整成指定內容。
避免異動任何tag與功能,只針對Style與文字進行調整。
調整後的HTML,不使用Canva,直接完整輸出。

###服務條款
xxxxxxxx

上傳HTML,測試功能與微調。

如果有使用背景圖片,需要在image中的src導入相對位置,相對位置又與Captive Portal Profile相關,所有相關上傳都會放在「upload/custom/<Captive Portal Profile Name>/」。

如果Captive Portal Profile叫「test_cppm_prof」,要導入logo.png上傳之後,html內容中關於logo的image src則設為「upload/custom/test_cppm_prof/logo.png」。

對於新手來說,這不友善。

因此!建議!直接讓大語言模型將Logo等圖片轉成base64並將輸出轉換成文字檔,然後使用image src直接導入base64。

<img src="data:image/png;base64,<image base64>" />

if image base64 = aaabbb
=> <img src="data:image/png;base64,aaabbb" />

這會讓html變得長且大,但可避免不懂如何使用多重上傳的技巧。

將html準備完成後,就可以準備上傳了,而在WLAN建立完成後,能調整Captive Portal的地方,在「Configuration > Roles & Policies > (Your Role) > Advanced View > Captive Portal」,或是「Configuration > WLANs > (Your WLAN) > Security」

「Configuration > Roles & Policies > (Your Role) > Advanced View > Captive Portal」

「Configuration > WLANs > (Your WLAN) > Security」

切換成Custom HTML,點擊File for Login Page旁的Browse上傳HTML與相關檔案。

我的HTML,搭配type「email」。

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>無線訪客網路登入</title>
  <style>
    /* 全域重置與排版 */
    * { margin:0; padding:0; box-sizing:border-box; }
    body {
      display:flex;
      align-items:center;
      justify-content:center;
      height:100vh;
      background:#f0f2f5;
      font-family:"Microsoft JhengHei",sans-serif;
      color:#333;
    }
    .card {
      background:#fff;
      border-radius:12px;
      box-shadow:0 4px 12px rgba(0,0,0,0.1);
      padding:2rem;
      max-width:360px;
      width:90%;
      text-align:center;
    }
    .card h1 {
      font-size:1.5rem;
      margin-bottom:1rem;
    }
    /* 表格對齊 */
    .info-table {
      width:100%;
      border-collapse:collapse;
      margin-bottom:1.5rem;
    }
    .info-table td {
      padding:0.5rem 0.25rem;
      font-size:0.95rem;
      vertical-align:middle;
    }
    .info-table .label {
      width:4em;
      text-align:left;
      white-space:nowrap; /* 避免換行 */
    }
    .info-table .value {
      text-align:left;
      word-break:break-all;
    }

    /* 按鈕及脈衝動畫 */
    @keyframes pulse {
      0%,100% { transform: scale(1); }
      50% { transform: scale(1.05); }
    }
    .btn-connect {
      display:block;
      width:100%;
      padding:0.75rem 0;
      font-size:1rem;
      border:none;
      border-radius:8px;
      background:#2a4099;
      color:#fff;
      cursor:pointer;
      transition:background 0.3s;
      animation:pulse 2s ease-in-out infinite;
    }
    .btn-connect:hover {
      background:#1f3077;
      animation-play-state:paused;
    }
  </style>
</head>
<body>
  <div class="card">
    <h1>無線訪客網路</h1>
    <table class="info-table">
      <tr>
        <td class="label">SSID:</td>
        <td class="value" id="ssidDisplay">載入中...</td>
      </tr>
      <tr>
        <td class="label">IP 位址:</td>
        <td class="value" id="ipDisplay">載入中...</td>
      </tr>
      <tr>
        <td class="label">MAC 位址:</td>
        <td class="value" id="macDisplay">載入中...</td>
      </tr>   
    </table>
    <button class="btn-connect" id="connectBtn">開始使用訪客網路</button>   
  </div>

  <form id="regform"
        action="https://securelogin.arubanetworks.com/cgi-bin/login"
        method="post"
        style="display:none;">
    <input type="text" id="email" name="email" placeholder="example@host.com">     
    <input type="hidden" name="cmd" value="authenticate"/>
    <input type="hidden" name="requested_url" value=""/>
  </form>

  <script>
    // 解析 URL 參數
    const params = new URLSearchParams(window.location.search);
    const essid = params.get("essid") || "無法取得";
    const ip    = params.get("ip")    || "無法取得";
    const mac   = params.get("mac")   || "無法取得";
    
    // 顯示連線資訊
    document.getElementById("ssidDisplay").textContent = essid;
    document.getElementById("ipDisplay").textContent   = ip;
    document.getElementById("macDisplay").textContent  = mac;



    // 一鍵連線動作
    document.getElementById("connectBtn").addEventListener("click", () => {
      // 組出 yyyymmddhhMM 格式
      const now   = new Date();
      const YYYY  = now.getFullYear();
      const MM    = String(now.getMonth()+1).padStart(2,'0');
      const DD    = String(now.getDate()).padStart(2,'0');
      const hh    = String(now.getHours()).padStart(2,'0');
      const mm    = String(now.getMinutes()).padStart(2,'0');
      const ts    = `${YYYY}${MM}${DD}${hh}${mm}`;

      //預設email
      const emailInput = document.getElementById('email');
      emailInput.value = ts + '@wifi.guest'; 

      document.querySelector('[name="requested_url"]').value = window.location.href;
      document.getElementById("regform").submit();
    });
  </script>
</body>
</html>

選取檔案之後,按下Submit上傳。

上傳順序:「圖片&相關檔案 >>>>> HTML」。總之,HTML是最後一個上傳。

上傳的檔案不會自動刪除,名稱相同的話則會覆蓋過去。一定要按下Submit之後,等上傳完成,才能上傳下一個檔案。

之後測試WLAN,連上後跳出需要登入的通知,順利看到Captive Portal。

常見的Captive Portal規劃

使用訪客帳密:使用特定一組帳密或是用腳本每天變動。

僅需同意條款:公共提供網路服務,不需使用者額外輸入身份資訊。

我認為提供訪客用無線網路,應該要簡單方便使用,僅需同意條款的類型也在個資意識抬頭上越來越多。

以下提供的範本是部署上最簡易的,僅需建立訪客WLAN,Type「EMAIL」,然後上傳HTML即可。

Type 「EMAIL」。使用email並且透過script讓登入時間去組成email並作為username。

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>無線訪客網路登入</title>
  <style>
    /* 全域重置與排版 */
    * { margin:0; padding:0; box-sizing:border-box; }
    body {
      display:flex;
      align-items:center;
      justify-content:center;
      height:100vh;
      background:#f0f2f5;
      font-family:"Microsoft JhengHei",sans-serif;
      color:#333;
    }
    .card {
      background:#fff;
      border-radius:12px;
      box-shadow:0 4px 12px rgba(0,0,0,0.1);
      padding:2rem;
      max-width:360px;
      width:90%;
      text-align:center;
    }
    .card h1 {
      font-size:1.5rem;
      margin-bottom:1rem;
    }
    /* 表格對齊 */
    .info-table {
      width:100%;
      border-collapse:collapse;
      margin-bottom:1.5rem;
    }
    .info-table td {
      padding:0.5rem 0.25rem;
      font-size:0.95rem;
      vertical-align:middle;
    }
    .info-table .label {
      width:4em;
      text-align:left;
      white-space:nowrap; /* 避免換行 */
    }
    .info-table .value {
      text-align:left;
      word-break:break-all;
    }

    /* 按鈕及脈衝動畫 */
    @keyframes pulse {
      0%,100% { transform: scale(1); }
      50% { transform: scale(1.05); }
    }
    .btn-connect {
      display:block;
      width:100%;
      padding:0.75rem 0;
      font-size:1rem;
      border:none;
      border-radius:8px;
      background:#2a4099;
      color:#fff;
      cursor:pointer;
      transition:background 0.3s;
      animation:pulse 2s ease-in-out infinite;
    }
    .btn-connect:hover {
      background:#1f3077;
      animation-play-state:paused;
    }
  </style>
</head>
<body>
  <div class="card">
    <h1>無線訪客網路</h1>
    <table class="info-table">
      <tr>
        <td class="label">SSID:</td>
        <td class="value" id="ssidDisplay">載入中...</td>
      </tr>
      <tr>
        <td class="label">IP 位址:</td>
        <td class="value" id="ipDisplay">載入中...</td>
      </tr>
      <tr>
        <td class="label">MAC 位址:</td>
        <td class="value" id="macDisplay">載入中...</td>
      </tr>   
    </table>
    <button class="btn-connect" id="connectBtn">開始使用訪客網路</button>   
  </div>

  <form id="regform"
        action="https://securelogin.arubanetworks.com/cgi-bin/login"
        method="post"
        style="display:none;">
    <input type="text" id="email" name="email" placeholder="example@host.com">     
    <input type="hidden" name="cmd" value="authenticate"/>
    <input type="hidden" name="requested_url" value=""/>
  </form>

  <script>
    // 解析 URL 參數
    const params = new URLSearchParams(window.location.search);
    const essid = params.get("essid") || "無法取得";
    const ip    = params.get("ip")    || "無法取得";
    const mac   = params.get("mac")   || "無法取得";
    
    // 顯示連線資訊
    document.getElementById("ssidDisplay").textContent = essid;
    document.getElementById("ipDisplay").textContent   = ip;
    document.getElementById("macDisplay").textContent  = mac;



    // 一鍵連線動作
    document.getElementById("connectBtn").addEventListener("click", () => {
      // 組出 yyyymmddhhMM 格式
      const now   = new Date();
      const YYYY  = now.getFullYear();
      const MM    = String(now.getMonth()+1).padStart(2,'0');
      const DD    = String(now.getDate()).padStart(2,'0');
      const hh    = String(now.getHours()).padStart(2,'0');
      const mm    = String(now.getMinutes()).padStart(2,'0');
      const ts    = `${YYYY}${MM}${DD}${hh}${mm}`;

      //預設email
      const emailInput = document.getElementById('email');
      emailInput.value = ts + '@wifi.guest'; 

      document.querySelector('[name="requested_url"]').value = window.location.href;
      document.getElementById("regform").submit();
    });
  </script>
</body>
</html>