前端安全 F12 破解 Cloudflare 後端驗證 踩坑紀錄

網站密碼保護踩坑:從 F12 一秒破解到真正安全的後端驗證

Thomas 花 2 小時做的密碼保護,被一行代碼在 1 秒內破解。這篇記錄了從「以為很安全」到「真的安全」的完整過程。不需要寫代碼,放一個文件就搞定。

目錄(踩坑時間線)
01 故事起點:用密碼保護網站(看起來沒問題) 02 F12 一秒破解(發現真相) 03 為什麼會這樣?(前端保護的根本問題) 04 關鍵詞彙中英文對照 05 真正的解決方案:後端驗證 06 前端 vs 後端完整對比 07 小白操作步驟(5 分鐘搞定) 08 教訓總結 FAQ 常見問題

第一章:故事起點 -- 用密碼保護網站

Thomas 有 5 個私人網站,部署在 Cloudflare Pages 上:

問題:這些網址任何人打開就能看到全部內容。Thomas 決定加密碼保護。

嘗試一:Google OAuth(失敗)

踩坑:花了 2 小時搞 Google OAuth 登入

Thomas 最初想用 Google 帳號登入來保護網站。結果:GCP Console 設定複雜、OAuth Client 建不出來、回調地址搞不定...... 折騰 2 小時後放棄,改用密碼方案。

嘗試二:HTML + JavaScript 密碼保護(成功了......嗎?)

最終做出來的方案長這樣:

  1. 一個全屏密碼輸入框覆蓋在網頁內容上面
  2. 用戶輸入密碼 → JavaScript 用 SHA-256(一種加密算法)計算 Hash
  3. 如果 Hash 匹配 → 隱藏密碼框,顯示內容
  4. 密碼不對 → 顯示錯誤提示
SHA-256 是什麼?

一種單向加密算法(Hashing Algorithm)。把任何文字丟進去,都會變成一串 64 個字符的亂碼。重點是:不能反向解密。所以代碼裡存的是 Hash,不是明文密碼。聽起來很安全對吧?

所有人都說「SHA-256 很安全」,AI 也給了 8/10 分的安全評分。Thomas 以為搞定了。

比喻:看起來安全的門

就像在房間門口裝了一個指紋鎖 -- 鎖本身很厲害(SHA-256 確實是好算法)。但問題是:房間的牆壁是透明玻璃做的。鎖再好,窗戶一砸就進去了。

第二章:F12 一秒破解(發現真相)

某天 Thomas 問了一個關鍵問題:

Thomas 的提問

「這個密碼保護到底有多不安全?教我怎麼破解它。」

AI 教了一行代碼

在 Chrome 按 F12 → Console 裡貼入這行
document.getElementById('pw-screen').style.display='none';
document.getElementById('main-content').style.display='block';

操作流程:

F12 破解全過程(10 秒)
打開被密碼保護的網站,看到密碼輸入框
按鍵盤 F12(或右鍵 → 檢查)→ 打開 DevTools(開發者工具)
點擊上方的 Console(控制台)標籤
貼入那兩行代碼 → 按 Enter
密碼框消失,所有網頁內容直接顯示
殘酷的事實

完全不需要知道密碼。

SHA-256 也沒有用 -- 因為破解者根本不需要去猜密碼或破解 Hash。

密碼框只是一層 CSS 遮罩(display:none),把它移開就行了。

第三章:為什麼會這樣?

前端保護的根本問題

理解這個問題只需要記住一件事:

核心概念

當你打開一個網頁,瀏覽器會下載這個網頁的所有內容
「密碼框」只是用 CSS 把內容藏起來,但內容已經全部在你的電腦上了

前端密碼保護的工作原理
用戶打開網頁
瀏覽器下載完整 HTML(包含所有私密內容)
JavaScript 顯示密碼輸入框
用 CSS display:none 隱藏內容
密碼只是一層「窗簾」
F12 → 把窗簾拉開 → 內容全都在
比喻:透明玻璃櫃

把東西鎖在透明玻璃櫃裡 -- 鎖再好(SHA-256),玻璃一敲就碎(F12)。因為東西(HTML 內容)已經送到你面前了。不管用什麼加密算法,只要內容在瀏覽器裡,就能被看到。

為什麼 SHA-256 沒用?

SHA-256 本身是好的加密算法,問題不在它。問題在於:

教訓

安全不是看你用了什麼加密算法,而是看內容在哪裡。如果內容已經在瀏覽器裡,怎麼加密都沒用。

第四章:關鍵詞彙中英文對照

在往下看之前,先搞懂這些術語。每一個都有中英文名稱小白比喻

術語 解釋 + 比喻
F12 / DevTools
開發者工具
瀏覽器自帶的「後台」,能看到網頁的所有代碼。
比喻:餐廳的監控攝像頭,你能看到廚房裡的一切。
Console
控制台
DevTools 裡的命令行,能執行 JavaScript 代碼。
比喻:餐廳的對講機,你可以直接對廚房下指令。
CSS display:none
CSS 隱藏元素
用 CSS 讓某個元素在畫面上消失。元素還在 HTML 裡,只是看不見。
比喻:用布蓋住東西 -- 東西還在,只是你看不到。
Frontend
前端
在你瀏覽器裡跑的代碼(HTML、CSS、JavaScript)。所有前端代碼你都能看到和修改。
比喻:你面前這桌菜 -- 想怎麼動就怎麼動。
Backend
後端
在伺服器上跑的代碼。你看不到也改不了,只能接收伺服器給你的回應。
比喻:餐廳的廚房 -- 你只能點菜,不能進去炒。
Middleware
中間件
「守門員」,每個請求必須先經過它。密碼不對就把你擋在門外。
比喻:酒吧的門口保全 -- 沒有手環就不讓進。
Cookie
瀏覽器 Cookie
瀏覽器存的一小段文字,用來記住你的登入狀態。30 天內不用再輸密碼。
比喻:演唱會入場後蓋在手上的章 -- 中途出去再回來不用重新驗票。
HttpOnly Cookie
安全 Cookie
加了保護的 Cookie,JavaScript(F12)看不到也改不了。只有伺服器能讀寫。
比喻:蓋在皮膚裡面的隱形章 -- 只有專用掃描器能讀。
401 Unauthorized
未授權錯誤
伺服器的回應:「你沒有權限」。連內容都不給你。
比喻:保全說「你不在名單上,請回」。
Cloudflare Pages Functions
Cloudflare 頁面函式
Cloudflare 提供的免費後端功能。在你的網站資料夾裡放一個 functions/ 目錄,Cloudflare 就會自動執行裡面的代碼。不需要額外設置。
比喻:大樓自帶的保全系統 -- 不用另外請人,放張保全卡就啟動。
Cloudflare Pages Secret
環境變數密鑰
存在 Cloudflare 伺服器上的秘密值。不在代碼裡、不在 HTML 裡、不在任何文件裡。只有 Worker/Functions 能讀取。
比喻:保險箱裡的密碼紙 -- 只有保全有鑰匙。

第五章:真正的解決方案 -- 後端驗證

核心差異:內容什麼時候發送?

前端保護(被破解)
瀏覽器請求網頁
伺服器發送完整 HTML + 密碼框
F12 → 掀開密碼框 → 看到所有內容
後端保護(無法破解)
瀏覽器請求網頁
Cloudflare 中間件檢查 Cookie
沒有 Cookie → 只返回登入頁
內容根本不發送
比喻:快遞 vs 銀行保險箱

前端保護 = 快遞員把包裹送到你家門口,貼了一張「請先報密碼」的紙條。你撕掉紙條就能打開包裹。
後端保護 = 你要親自去銀行,跟櫃員說密碼,他才從保險箱拿東西出來。你不到銀行,東西永遠在保險箱裡。

Cloudflare Pages Functions Middleware

Thomas 最終用的方案是 Cloudflare Pages Functions Middleware(Cloudflare 頁面函式中間件)。重點是:

不需要額外設置任何 Cloudflare 後台

不需要改 Cloudflare Dashboard 的任何設定。不需要開啟什麼功能。不需要綁信用卡。只需要在你的網站資料夾裡放一個文件,Cloudflare 就會自動執行它。

核心文件結構

你的網站資料夾/
├── index.html         ← 你的網頁內容(不需要任何密碼代碼)
├── other-page.html    ← 其他頁面(也不需要密碼代碼)
└── functions/
    └── _middleware.js  ← 後端守門員(Cloudflare 自動執行)

Middleware 做了什麼?

_middleware.js 的工作流程
任何請求進來(不管是首頁還是子頁面)
先經過 _middleware.js(守門員)
檢查瀏覽器有沒有帶 cf_auth Cookie?
沒有 Cookie → 返回登入頁面
(HTML 內容不發送)
有 Cookie 且正確 → 放行
(返回真正的 HTML 內容)

當用戶輸入密碼時:

  1. Middleware 用 SHA-256 計算輸入密碼的 Hash
  2. 和存在 Cloudflare Secret 裡的 Hash 比對
  3. 正確 → 設一個 HttpOnly Cookie(有效期 30 天)
  4. 之後 30 天內再訪問,自動帶 Cookie,不用再輸密碼

密碼 Hash 存在哪?

存在 Cloudflare Pages Secret(環境變數密鑰)裡:

設定密碼的命令
npx wrangler pages secret put PW_HASH --project-name=你的項目名
為什麼這樣更安全?

密碼 Hash 不在代碼裡、不在 HTML 裡、不在任何文件裡。它存在 Cloudflare 的伺服器上,只有 Worker/Functions 的代碼能讀取。就算有人下載了你的整個 GitHub repo,也找不到密碼。

第六章:前端 vs 後端完整對比

前端密碼保護 後端密碼保護
內容在哪 已全部下載到瀏覽器 密碼正確才從伺服器發送
F12 能破解嗎 一秒破解 不能
密碼存在哪 HTML / JS 裡(能被看到) Cloudflare Secret(看不到)
F12 看到什麼 完整的 HTML 內容 + 密碼框 只有登入表單,沒有任何內容
安全評分 3/10(擋小白而已) 10/10
需要改 HTML 嗎 需要(加密碼框代碼) 不需要(HTML 保持原樣)
額外設置 無(放個文件就行)
費用 免費 免費
Cookie 類型 localStorage(JS 可讀寫) HttpOnly Cookie(JS 不可讀)
一句話總結

前端保護:東西已經送到你手上了,用布蓋著而已。
後端保護:東西鎖在伺服器裡,你不證明身份就拿不到。

第七章:小白操作步驟(5 分鐘搞定)

以下是給一個全新網站加後端密碼保護的完整步驟。假設你已經有一個部署在 Cloudflare Pages 的網站。

1 建立 functions 資料夾

在你的網站根目錄裡,建一個名叫 functions 的資料夾:

你的網站資料夾/
├── index.html
└── functions/   ← 新建這個資料夾

2 建立 _middleware.js

functions/ 裡建立一個文件叫 _middleware.js,內容如下。你不需要理解每一行,直接複製就好:

// Cloudflare Pages Function -- backend password protection
// Content is NEVER sent to the browser until password is verified

async function sha256(str) {
  const buf = await crypto.subtle.digest(
    'SHA-256', new TextEncoder().encode(str)
  );
  return Array.from(new Uint8Array(buf))
    .map(b => b.toString(16).padStart(2, '0')).join('');
}

function loginPage(error) {
  return `<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<style>
  *{margin:0;padding:0;box-sizing:border-box}
  body{background:#0f172a;color:#e2e8f0;font-family:sans-serif;
       display:flex;align-items:center;justify-content:center;
       min-height:100vh}
  .box{text-align:center;padding:40px}
  .title{font-size:1.5rem;font-weight:700;margin-bottom:24px}
  form{display:flex;flex-direction:column;align-items:center;gap:12px}
  input{padding:12px 20px;border-radius:10px;border:1px solid #334155;
        background:#1e293b;color:#e2e8f0;font-size:1rem;width:260px;
        text-align:center;outline:none}
  input:focus{border-color:#60a5fa}
  button{padding:10px 32px;border-radius:10px;border:none;
         background:#60a5fa;color:#fff;font-size:0.95rem;
         font-weight:600;cursor:pointer}
  .error{color:#fb7185;font-size:0.85rem;margin-top:4px}
</style>
</head><body>
<div class="box">
  <div class="title">Please enter password</div>
  <form method="POST" action="/__auth">
    <input type="password" name="password"
           placeholder="Password" autofocus />
    <button type="submit">Enter &rarr;</button>
    ${error ? '<div class="error">Incorrect</div>' : ''}
  </form>
</div>
</body></html>`;
}

export async function onRequest(context) {
  const { request, next, env } = context;
  const url = new URL(request.url);
  const storedHash = env.PW_HASH;
  if (!storedHash) return next(); // No password set

  const expectedToken = await sha256(storedHash + '__cf_auth_salt__');
  const cookies = request.headers.get('Cookie') || '';
  const match = cookies.match(/cf_auth=([a-f0-9]{64})/);
  if (match && match[1] === expectedToken) return next();

  if (request.method === 'POST' && url.pathname === '/__auth') {
    const formData = await request.formData();
    const password = formData.get('password') || '';
    const inputHash = await sha256(password);
    if (inputHash === storedHash) {
      return new Response(null, {
        status: 302,
        headers: {
          'Location': '/',
          'Set-Cookie': `cf_auth=${expectedToken}; Path=/; ` +
            `Max-Age=${30*24*60*60}; HttpOnly; Secure; SameSite=Strict`
        }
      });
    }
    return new Response(loginPage(true), {
      status: 401,
      headers: { 'Content-Type': 'text/html; charset=utf-8' }
    });
  }

  return new Response(loginPage(false), {
    status: 401,
    headers: { 'Content-Type': 'text/html; charset=utf-8' }
  });
}

3 設定密碼 Hash

在終端(Terminal / 命令列)裡跑兩行命令:

第一步:算出你密碼的 SHA-256 Hash
echo -n "你的密碼" | sha256sum

這會輸出一串 64 個字符的 Hash,例如:
5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8

第二步:把 Hash 存到 Cloudflare Secret
npx wrangler pages secret put PW_HASH --project-name=你的項目名

系統會提示你輸入值,把剛才的 Hash 貼進去按 Enter。

注意

Secret 存的是 Hash(64 字符亂碼),不是你的原始密碼。即使有人能讀到這個 Secret,也無法反推出你的密碼。

4 部署

用你平時的部署方式即可。確保 functions/ 目錄包含在部署文件裡:

npx wrangler pages deploy . --project-name=你的項目名 --commit-dirty=true

5 測試(重要!)

部署完成後,做以下測試來確認安全性:

  1. 打開你的網站 → 應該看到登入頁面(不是你的內容)
  2. F12 → 切到 Elements 標籤 → 確認 HTML 裡只有登入表單,沒有任何網頁內容
  3. 在 Console 裡試試之前的破解代碼 → 不會有任何效果(因為內容根本不在 HTML 裡)
  4. 輸入正確密碼 → 應該跳轉到你的網頁內容
  5. 關閉瀏覽器再打開 → 不用重新輸密碼(Cookie 30 天有效)

第八章:教訓總結

四個核心教訓

  1. 前端保護 = 假安全 -- 不管用什麼加密算法(SHA-256、bcrypt、AES),只要內容在瀏覽器裡,F12 都能繞過。加密算法保護的是密碼,不是內容。
  2. 後端保護 = 真安全 -- 內容不在瀏覽器裡,怎麼 F12 都看不到。因為伺服器根本沒把內容發送過來。
  3. Cloudflare Pages Functions 是免費的 -- 不需要額外設置 Cloudflare Dashboard,不需要綁信用卡。放一個 functions/_middleware.js 文件就行。
  4. 不要相信「安全評分」 -- AI 給了前端方案 8/10 分。但實際上 F12 一秒破解。永遠要自己動手測試驗證。
Thomas 的安全升級時間線
嘗試 Google OAuth 登入 → 失敗(花了 2 小時)
改用 HTML + JS 密碼保護 + SHA-256 Hash → 成功部署
F12 + 一行代碼 → 一秒破解(崩潰)
理解前端 vs 後端的根本區別
改用 Cloudflare Pages Functions Middleware → 真正安全
F12 只能看到登入頁面,沒有任何內容可偷看
寫給未來的自己

下次做任何「安全功能」時,先問自己一個問題:「攻擊者能在 F12 裡看到什麼?」如果答案是「所有內容」,那你的保護就是假的。

FAQ 常見問題

Q1: 後端驗證會讓網站變慢嗎?

不會。Cloudflare Functions 跑在 Cloudflare 的全球邊緣網路上,延遲只有幾毫秒。用戶輸入密碼後,Cookie 會記住 30 天,之後的訪問完全不受影響。

Q2: 我有多個頁面,每個都要加密碼嗎?

不用。_middleware.js 放在 functions/ 根目錄下,會自動保護所有頁面。不管訪問 /index.html 還是 /secret-page.html,都要先通過密碼驗證。

Q3: 可以讓某些頁面不需要密碼嗎?

可以。在 _middleware.js 裡加一個判斷:如果 URL 是某些路徑(例如 /public/),就直接放行(return next())。

Q4: 忘記密碼怎麼辦?

重新跑一次 npx wrangler pages secret put PW_HASH,輸入新密碼的 Hash 就行。舊的 Cookie 會自動失效(因為 Token 是根據 Hash 計算的)。

Q5: 這和 Cloudflare Access 有什麼區別?

Cloudflare Access(存取控制)是 Cloudflare 的企業級產品,需要在 Dashboard 裡設定,免費版有人數限制。Pages Functions Middleware 是自己寫的後端代碼,完全免費,無人數限制,但功能比較基礎(只有密碼,沒有 SSO 或多用戶管理)。對個人網站來說,Middleware 完全夠用。

Q6: HttpOnly Cookie 是什麼意思?

普通 Cookie 可以被 JavaScript 讀取(document.cookie),也就是 F12 Console 裡能看到。HttpOnly Cookie 加了一個標記,告訴瀏覽器:「這個 Cookie 只能在 HTTP 請求裡帶,JavaScript 不能碰。」這樣即使有 XSS 攻擊(跨站腳本注入),攻擊者也拿不到你的登入 Cookie。

Q7: 為什麼前端方案 AI 還給 8/10 分?

因為 AI 評分看的是「密碼本身的安全性」(SHA-256 Hash、不存明文)。這些確實是好的做法。但 AI 沒考慮到「內容已經在瀏覽器裡」這個根本問題。這也是為什麼教訓 #4 說:不要只看評分,要自己動手測試。

Q8: 這篇和「網站密碼保護全攻略」有什麼區別?

那篇是「概念教學」 -- 從明文到 Hash 到後端驗證的安全等級金字塔。這篇是「踩坑紀錄」 -- 記錄 Thomas 真的被 F12 破解後,從發現問題到修復的完整過程,附帶可以直接複製的代碼。

故事起點 F12 破解 為什麼能破 詞彙表 後端方案 對比表 操作步驟 教訓總結 FAQ ← 返回首頁 ↑ 回到頂部