sell整合實戰
lock_person

前後端認證串接

從免寫程式的登入牆,到手刻的 session cookie —— 看清認證如何在整個技術棧裡流動。

2種認證做法
50 users免費 Access 名額
0 linesAccess 免寫程式
HttpOnlyCookie 強化
insights

我們要串接什麼?

「認證(Authentication)」其實就是:在放使用者進來之前,先確認他是誰。在 Cloudflare 上有兩種乾淨的做法,本篇會帶你走過兩種,讓你挑對適合自己的那一種。

做法 A 是 Cloudflare Access(零信任):身分驗證直接擋在整個應用程式前面,你完全不用寫程式。做法 B 是應用程式層級的 session:你自己的 Worker 檢查密碼、把 session 存進 Workers KV,再發一個 cookie 給瀏覽器。

schema兩條路線一眼看懂

零信任

應用程式登入

瀏覽器

用哪種認證?

Cloudflare Access

你的 Worker

受保護的應用程式

Workers KV session

verified_user

A. Cloudflare Access

在應用程式前面做身分檢查。免寫程式,在 Zero Trust 儀表板設定。很適合內部工具。

cookie

B. KV session

你的 Worker 驗證登入、把 session 存進 KV、再設一個 cookie。完全掌控、但要寫程式。很適合公開的應用程式。

meeting_room

把它想成…

Access 就像幫整棟大樓的大門請一位保全。自建 session 則是你在自家櫃檯給每位訪客一條有編號的手環,之後每進一個房間都核對那條手環。

verified_user

做法 A — Cloudflare Access(免寫程式)

Cloudflare Access 在邊緣(edge)幫你的應用程式擋一道登入牆。在 Access 用你信任的提供者(Google、GitHub、Microsoft Entra ID,或寄到 email 的一次性密碼)確認身分之前,使用者根本碰不到你的應用程式。

這就是單一登入(SSO,Single Sign-On):使用者用既有帳號登入一次,Access 就發一個已簽章的權杖(JWT)當作 cookie。你的應用程式可以完全不用碰密碼。

schemaCloudflare Access SSO 流程
"你的應用程式""身分提供者""Cloudflare Access""瀏覽器""你的應用程式""身分提供者""Cloudflare Access""瀏覽器"打開應用程式網址轉址到登入頁用 Google 登入身分確認完成檢查 Access 政策設定已簽章的 JWT cookie請求帶著 JWT受保護的頁面
code_off

零程式碼

在儀表板新增應用程式和一條政策即可。不用寫、不用建置、也不用部署。

groups

沿用公司帳號

接上 Google Workspace、GitHub 或 Entra ID,大家直接用既有帳號。

policy

以身分為基礎的政策

只允許 @yourcompany.com 的 email、要求多因素驗證,或依群組限制。

history

內建稽核紀錄

每次登入嘗試都會被記錄(誰、做了什麼、何時),不用額外做任何事。

verified

你的應用程式仍可信任這個權杖

Access 會轉發一個已簽章的 Cf-Access-Jwt-Assertion 標頭。需要的話,你的 Worker 可以拿 Cloudflare 的公鑰驗證這個 JWT 來讀出使用者 email —— 但你完全不必自己處理登入。

cookie

做法 B — 應用程式層級的 KV session

當你要自己做註冊和登入(公開的應用程式、自訂介面、你自己的使用者資料表)時,認證就由程式碼處理。最經典、最穩健的模式就是「伺服器端 session + cookie」。

Worker 檢查密碼、產生一個隨機的 session id、把真正的 session 資料連同到期時間存進 Workers KV,然後發給瀏覽器一個「只裝這個 id」的 cookie。之後每次請求,瀏覽器會自動帶上 cookie,Worker 再用它去 KV 查出 session。

schema應用程式登入流程
"Workers KV""Worker""瀏覽器""Workers KV""Worker""瀏覽器"POST /login 帶 email 與密碼驗證密碼雜湊put 寫入 session id 與使用者 json已存入並設定 TTLSet-Cookie 回傳 session idGET /me 帶著 cookieget 查 session id使用者 json個人資料 JSON
key

不透明的 session id

cookie 只裝一個隨機 id。使用者資料放在伺服器端的 KV,絕不放在瀏覽器裡。

schedule

自動過期

expirationTtl 會讓 KV 自動刪掉 session —— 等於免費的「逾時自動登出」。

logout

即時撤銷

刪掉那個 KV key,該 session 立刻失效,就算還沒到期也一樣。

tune

完全掌控

你的路由、你的 cookie 旗標、你的規則。可以加角色、加更新機制,什麼都行。

lan

為什麼 KV 適合存 session

幾乎每個請求都要讀 session,而 KV 正是為「讀多、全球同步、內建 TTL」的查詢而設計的,所以拿來放 Workers 上的 session 資料剛剛好。

call_split

我該用哪一種?

依「對象」來選。如果你要把關的是內部工具或員工後台,Access 勝出 —— 它比你自己手刻的任何東西都更快也更安全。如果你在做一個對外、讓陌生人自行註冊的產品,那你就需要自己的 session。

schema決策:Access 還是自建 session

需要保護某個東西

內部工具或員工系統?

用 Cloudflare Access

公開且有使用者帳號?

自建 KV session

只是想擋表單機器人?

加上 Turnstile

登入牆完成

badge

選 Access 的時機…

內部應用程式、管理後台、測試站、SSH/遠端桌面,或任何給「已知一群人」用的東西。

public

自建 session 的時機…

對外開放註冊、自訂登入介面、社群帳號、每位使用者各自的資料,或行動/SPA 後端。

shield

或只要 Turnstile…

如果你只是要擋住聯絡或註冊表單上的機器人,加個 Turnstile 就好,完全不用做認證。

merge

兩者可以混用

同一個專案裡,用 Access 守後台、用自己的 KV session 處理一般使用者帳號,是很常見的組合。兩者並不衝突。

construction

動手做:一個完整的 session Worker

以下是做法 B 的完整版:建立 KV 命名空間、綁定、塞一個示範使用者,接著是 Worker(登入 + 中介層 + 登出)和一個小小的前端。做法 A 不用寫程式,照前面 Access 那段的儀表板步驟做即可。

  1. 建立兩個 KV 命名空間

    一個存 session,一個存示範使用者。Wrangler 會各印出一個 id。

    bash
    # Two namespaces: one for sessions, one for the demo user store
    npx wrangler kv namespace create SESSIONS
    npx wrangler kv namespace create USERS
  2. 在 wrangler.jsonc 綁定

    把 id 貼進來,Worker 就能用 env.SESSIONS 和 env.USERS 取用。

    json
    {
      "name": "auth-worker",
      "main": "src/index.js",
      "compatibility_date": "2025-01-01",
      "kv_namespaces": [
        { "binding": "SESSIONS", "id": "<paste-sessions-id>" },
        { "binding": "USERS", "id": "<paste-users-id>" }
      ]
    }
  3. 塞一個示範使用者

    存一位使用者,他的 passwordHash 就是密碼「hunter2」的 SHA-256。

    bash
    # Seed one demo user. passwordHash below is SHA-256 of "hunter2".
    npx wrangler kv key put --binding USERS \
      "user:alice@example.com" \
      '{"email":"alice@example.com","passwordHash":"f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7"}' \
      --remote

Worker:登入、中介層、登出

注意:這裡密碼檢查只用 SHA-256 是為了讓示範簡短。正式環境請改用真正的 KDF(金鑰衍生函式)來雜湊密碼,例如 Argon2id、scrypt、bcrypt,或用 Web Crypto 的 PBKDF2 搭配高迭代次數與每位使用者各自的鹽(salt)。

jssrc/index.js
// src/index.js -- app-level sessions stored in Workers KV
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // Public: log in and receive a session cookie
    if (url.pathname === "/login" && request.method === "POST") {
      return handleLogin(request, env);
    }
    // Public: drop the session
    if (url.pathname === "/logout" && request.method === "POST") {
      return handleLogout(request, env);
    }
    // Protected: only reachable with a valid session cookie
    if (url.pathname === "/me") {
      return requireSession(request, env, (session) =>
        Response.json({ email: session.email })
      );
    }
    return new Response("Not found", { status: 404 });
  },
};

// --- Login: verify password, create a session in KV, set a cookie ---
async function handleLogin(request, env) {
  const { email, password } = await request.json();

  const record = await env.USERS.get(`user:${email}`);
  if (!record) return new Response("Invalid login", { status: 401 });
  const user = JSON.parse(record);

  // DEMO ONLY: comparing SHA-256 hashes.
  // In production hash passwords with a real KDF (Argon2id / scrypt / bcrypt,
  // or PBKDF2 via Web Crypto with many iterations and a per-user salt).
  if ((await sha256(password)) !== user.passwordHash) {
    return new Response("Invalid login", { status: 401 });
  }

  // Opaque random id -- the cookie holds only this id, never user data.
  const sessionId = crypto.randomUUID();
  const session = { email: user.email, createdAt: Date.now() };
  const ttl = 60 * 60 * 24 * 7; // 7 days, in seconds

  await env.SESSIONS.put(`session:${sessionId}`, JSON.stringify(session), {
    expirationTtl: ttl,
  });

  return new Response(JSON.stringify({ ok: true }), {
    headers: {
      "Content-Type": "application/json",
      "Set-Cookie": cookie("session", sessionId, ttl),
    },
  });
}

// --- Middleware: read the cookie, look the session up in KV ---
async function requireSession(request, env, handler) {
  const id = getCookie(request, "session");
  if (!id) return new Response("Unauthorized", { status: 401 });

  const record = await env.SESSIONS.get(`session:${id}`);
  if (!record) return new Response("Unauthorized", { status: 401 });

  return handler(JSON.parse(record));
}

// --- Logout: delete the session and clear the cookie ---
async function handleLogout(request, env) {
  const id = getCookie(request, "session");
  if (id) await env.SESSIONS.delete(`session:${id}`);
  return new Response(JSON.stringify({ ok: true }), {
    headers: {
      "Content-Type": "application/json",
      "Set-Cookie": cookie("session", "", 0),
    },
  });
}

// --- Helpers ---
function cookie(name, value, maxAge) {
  return [
    `${name}=${value}`,
    "HttpOnly",
    "Secure",
    "SameSite=Lax",
    "Path=/",
    `Max-Age=${maxAge}`,
  ].join("; ");
}

function getCookie(request, name) {
  const header = request.headers.get("Cookie") || "";
  for (const part of header.split(";")) {
    const [key, ...rest] = part.trim().split("=");
    if (key === name) return rest.join("=");
  }
  return null;
}

async function sha256(text) {
  const data = new TextEncoder().encode(text);
  const digest = await crypto.subtle.digest("SHA-256", data);
  return [...new Uint8Array(digest)]
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

前端:登入表單

credentials: "include" 是關鍵 —— 它叫瀏覽器存下回應的 Set-Cookie,並在之後的請求自動帶上 cookie,這樣 /me 才會直接通。

htmllogin.html
<!-- A plain login form posting JSON to the Worker -->
<form id="login">
  <input name="email" type="email" placeholder="Email" required />
  <input name="password" type="password" placeholder="Password" required />
  <button>Log in</button>
</form>

<script>
  const form = document.getElementById("login");
  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const res = await fetch("/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      credentials: "include", // let the browser store & resend the cookie
      body: JSON.stringify({
        email: form.email.value,
        password: form.password.value,
      }),
    });
    if (res.ok) {
      // The session cookie is set; protected fetches now work automatically.
      const me = await fetch("/me", { credentials: "include" }).then((r) => r.json());
      alert(`Welcome, ${me.email}`);
    } else {
      alert("Login failed");
    }
  });
</script>
gpp_maybe

上線前必看

一定要走 HTTPS(Secure 旗標需要它)、cookie 保持 HttpOnly 讓 JavaScript 讀不到、使用真正的密碼 KDF,並在登入路由加上速率限制和 Turnstile,拖慢撞庫攻擊(credential stuffing)。

school

重點術語,白話版

verified_user

零信任

預設誰都不信任;每個請求都驗證身分,不管在網路內外。Access 就建立在這個概念上。

badge

Session(連線階段)

伺服器端「這位使用者已經登入過」的憑證。這裡存在 KV,用一個隨機 id 來辨識。

cookie

Cookie

瀏覽器儲存、並會自動回送到你網域的一小段值。我們只在裡面放 session id。

token

JWT

一個已簽章、自帶內容的權杖,裡面裝著聲明(claims,例如使用者 email)。Access 在 SSO 後會發一個。

shield_lock

HttpOnly/Secure

Cookie 旗標:HttpOnly 讓 JavaScript 讀不到它(防 XSS);Secure 只在 HTTPS 下傳送。

swap_horiz

SameSite

控制 cookie 是否跟著跨站請求一起送出。Lax 是對抗 CSRF 的合理預設值。

password

KDF/密碼雜湊

一種慢、加鹽的單向函式(Argon2/bcrypt/scrypt),用來安全地保存密碼。不是單純的 SHA-256。

login

單一登入(SSO)

用一個身分提供者登入一次,就能進入多個應用程式。這就是 Access 的體驗。

tips_and_updates

常見陷阱與計費

report

千萬別把密碼或使用者資料放進 cookie

cookie 只應該帶一個不透明的 session id。任何放進一般 cookie 的東西,使用者都看得到也能竄改;真正的資料要放在伺服器端的 KV。

  • KV 是最終一致性(eventually consistent):剛建立的 session 幾乎能立刻讀到,但極端情況下要留一點全球同步的時間。
  • 每個 session 都設 expirationTtl,讓過期的 session 自己清掉;cookie 的 Max-Age 也設成一樣。
  • Cloudflare Zero Trust(Access)最多 50 位使用者免費 —— 內部工具超適合。
  • KV 以讀/寫/刪次數計費,且有大方的免費額度;每個請求查一次 session 很便宜。
  • 在登入表單加上 Turnstile,削弱暴力破解和撞庫攻擊。
  • 登出時,要刪掉 KV key「並且」回送一個 Max-Age=0 的 cookie,讓兩邊都忘掉這個 session。
auto_awesome

一句話原則

內部用?先想到 Access。對外註冊?自己用 KV 掌管 session。不論哪種,都讓 Cloudflare 的邊緣幫你扛重活。