Thomas 花 2 小時做的密碼保護,被一行代碼在 1 秒內破解。這篇記錄了從「以為很安全」到「真的安全」的完整過程。不需要寫代碼,放一個文件就搞定。
Thomas 有 5 個私人網站,部署在 Cloudflare Pages 上:
問題:這些網址任何人打開就能看到全部內容。Thomas 決定加密碼保護。
Thomas 最初想用 Google 帳號登入來保護網站。結果:GCP Console 設定複雜、OAuth Client 建不出來、回調地址搞不定...... 折騰 2 小時後放棄,改用密碼方案。
最終做出來的方案長這樣:
SHA-256(一種加密算法)計算 Hash一種單向加密算法(Hashing Algorithm)。把任何文字丟進去,都會變成一串 64 個字符的亂碼。重點是:不能反向解密。所以代碼裡存的是 Hash,不是明文密碼。聽起來很安全對吧?
所有人都說「SHA-256 很安全」,AI 也給了 8/10 分的安全評分。Thomas 以為搞定了。
就像在房間門口裝了一個指紋鎖 -- 鎖本身很厲害(SHA-256 確實是好算法)。但問題是:房間的牆壁是透明玻璃做的。鎖再好,窗戶一砸就進去了。
某天 Thomas 問了一個關鍵問題:
「這個密碼保護到底有多不安全?教我怎麼破解它。」
AI 教了一行代碼:
document.getElementById('pw-screen').style.display='none';
document.getElementById('main-content').style.display='block';
操作流程:
完全不需要知道密碼。
SHA-256 也沒有用 -- 因為破解者根本不需要去猜密碼或破解 Hash。
密碼框只是一層 CSS 遮罩(display:none),把它移開就行了。
理解這個問題只需要記住一件事:
當你打開一個網頁,瀏覽器會下載這個網頁的所有內容。
「密碼框」只是用 CSS 把內容藏起來,但內容已經全部在你的電腦上了。
display:none 隱藏內容把東西鎖在透明玻璃櫃裡 -- 鎖再好(SHA-256),玻璃一敲就碎(F12)。因為東西(HTML 內容)已經送到你面前了。不管用什麼加密算法,只要內容在瀏覽器裡,就能被看到。
SHA-256 本身是好的加密算法,問題不在它。問題在於:
display:none → display:block安全不是看你用了什麼加密算法,而是看內容在哪裡。如果內容已經在瀏覽器裡,怎麼加密都沒用。
在往下看之前,先搞懂這些術語。每一個都有中英文名稱和小白比喻:
| 術語 | 解釋 + 比喻 |
|---|---|
| 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 能讀取。 比喻:保險箱裡的密碼紙 -- 只有保全有鑰匙。 |
前端保護 = 快遞員把包裹送到你家門口,貼了一張「請先報密碼」的紙條。你撕掉紙條就能打開包裹。
後端保護 = 你要親自去銀行,跟櫃員說密碼,他才從保險箱拿東西出來。你不到銀行,東西永遠在保險箱裡。
Thomas 最終用的方案是 Cloudflare Pages Functions Middleware(Cloudflare 頁面函式中間件)。重點是:
不需要改 Cloudflare Dashboard 的任何設定。不需要開啟什麼功能。不需要綁信用卡。只需要在你的網站資料夾裡放一個文件,Cloudflare 就會自動執行它。
_middleware.js(守門員)cf_auth Cookie?當用戶輸入密碼時:
HttpOnly Cookie(有效期 30 天)存在 Cloudflare Pages Secret(環境變數密鑰)裡:
npx wrangler pages secret put PW_HASH --project-name=你的項目名
密碼 Hash 不在代碼裡、不在 HTML 裡、不在任何文件裡。它存在 Cloudflare 的伺服器上,只有 Worker/Functions 的代碼能讀取。就算有人下載了你的整個 GitHub repo,也找不到密碼。
| 前端密碼保護 | 後端密碼保護 | |
|---|---|---|
| 內容在哪 | 已全部下載到瀏覽器 | 密碼正確才從伺服器發送 |
| F12 能破解嗎 | 一秒破解 | 不能 |
| 密碼存在哪 | HTML / JS 裡(能被看到) | Cloudflare Secret(看不到) |
| F12 看到什麼 | 完整的 HTML 內容 + 密碼框 | 只有登入表單,沒有任何內容 |
| 安全評分 | 3/10(擋小白而已) | 10/10 |
| 需要改 HTML 嗎 | 需要(加密碼框代碼) | 不需要(HTML 保持原樣) |
| 額外設置 | 無 | 無(放個文件就行) |
| 費用 | 免費 | 免費 |
| Cookie 類型 | localStorage(JS 可讀寫) | HttpOnly Cookie(JS 不可讀) |
前端保護:東西已經送到你手上了,用布蓋著而已。
後端保護:東西鎖在伺服器裡,你不證明身份就拿不到。
以下是給一個全新網站加後端密碼保護的完整步驟。假設你已經有一個部署在 Cloudflare Pages 的網站。
在你的網站根目錄裡,建一個名叫 functions 的資料夾:
在 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 →</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' }
});
}
在終端(Terminal / 命令列)裡跑兩行命令:
echo -n "你的密碼" | sha256sum
這會輸出一串 64 個字符的 Hash,例如:5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
npx wrangler pages secret put PW_HASH --project-name=你的項目名
系統會提示你輸入值,把剛才的 Hash 貼進去按 Enter。
Secret 存的是 Hash(64 字符亂碼),不是你的原始密碼。即使有人能讀到這個 Secret,也無法反推出你的密碼。
用你平時的部署方式即可。確保 functions/ 目錄包含在部署文件裡:
npx wrangler pages deploy . --project-name=你的項目名 --commit-dirty=true
部署完成後,做以下測試來確認安全性:
functions/_middleware.js 文件就行。下次做任何「安全功能」時,先問自己一個問題:「攻擊者能在 F12 裡看到什麼?」如果答案是「所有內容」,那你的保護就是假的。
不會。Cloudflare Functions 跑在 Cloudflare 的全球邊緣網路上,延遲只有幾毫秒。用戶輸入密碼後,Cookie 會記住 30 天,之後的訪問完全不受影響。
不用。_middleware.js 放在 functions/ 根目錄下,會自動保護所有頁面。不管訪問 /index.html 還是 /secret-page.html,都要先通過密碼驗證。
可以。在 _middleware.js 裡加一個判斷:如果 URL 是某些路徑(例如 /public/),就直接放行(return next())。
重新跑一次 npx wrangler pages secret put PW_HASH,輸入新密碼的 Hash 就行。舊的 Cookie 會自動失效(因為 Token 是根據 Hash 計算的)。
Cloudflare Access(存取控制)是 Cloudflare 的企業級產品,需要在 Dashboard 裡設定,免費版有人數限制。Pages Functions Middleware 是自己寫的後端代碼,完全免費,無人數限制,但功能比較基礎(只有密碼,沒有 SSO 或多用戶管理)。對個人網站來說,Middleware 完全夠用。
普通 Cookie 可以被 JavaScript 讀取(document.cookie),也就是 F12 Console 裡能看到。HttpOnly Cookie 加了一個標記,告訴瀏覽器:「這個 Cookie 只能在 HTTP 請求裡帶,JavaScript 不能碰。」這樣即使有 XSS 攻擊(跨站腳本注入),攻擊者也拿不到你的登入 Cookie。
因為 AI 評分看的是「密碼本身的安全性」(SHA-256 Hash、不存明文)。這些確實是好的做法。但 AI 沒考慮到「內容已經在瀏覽器裡」這個根本問題。這也是為什麼教訓 #4 說:不要只看評分,要自己動手測試。
那篇是「概念教學」 -- 從明文到 Hash 到後端驗證的安全等級金字塔。這篇是「踩坑紀錄」 -- 記錄 Thomas 真的被 F12 破解後,從發現問題到修復的完整過程,附帶可以直接複製的代碼。