iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
Modern Web

不只是登入畫面!一起打造現代化登入系統系列 第 19

屋內安全[ 8 / 8 ]:正式環境 Token 儲存:用 HttpOnly Cookie 才是真防禦力

  • 分享至 

  • xImage
  •  

在正式環境,我們不再用 localStorage,改由後端回傳 Cookie

Token 由 瀏覽器自動附帶,JS 完全碰不到,安全性提升一個等級。


Cookie 版的好處

  • 前端 JS 無法偷 Token(HttpOnly)
  • Request 時自動附上(credentials: 'include')
  • 後端可控過期時間、安全性、清除方式
  • 適合上線環境/會員系統/敏感資料

A. 後端:Express + Firebase Admin 寫 HttpOnly Cookie

import express from "express";
import admin from "firebase-admin";
import dotenv from "dotenv";

dotenv.config();
const router = express.Router();

const COOKIE_NAME = "token"; // 你也可以換成 idToken
const COOKIE_MAX_AGE = 60 * 60 * 1000; // 1 小時

// 驗證 + 設置 Cookie
router.post("/verify", async (req, res) => {
  try {
    const { idToken } = req.body;
    if (!idToken) return res.status(400).json({ message: "缺少 idToken" });

    const decoded = await admin.auth().verifyIdToken(idToken);
    // 寫入 HttpOnly Cookie
    res.cookie(COOKIE_NAME, idToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production", // 正式環境會用 HTTPS
      sameSite: "strict",
      maxAge: COOKIE_MAX_AGE,
    });

    res.json({
      uid: decoded.uid,
      email: decoded.email,
      message: "驗證成功,Token 已寫入 Cookie",
    });
  } catch (err) {
    res.status(401).json({ message: "Token 驗證失敗", error: err.message });
  }
});

// 登出:清除 Cookie
router.post("/logout", (req, res) => {
  res.clearCookie(COOKIE_NAME);
  res.json({ message: "已登出,Cookie 已清除" });
});

export default router;

只要寫在 Express 主檔案中即可:

import express from "express";
import authRoutes from "./authRoutes.js";
import cookieParser from "cookie-parser";

const app = express();
app.use(express.json());
app.use(cookieParser());

app.use("/auth", authRoutes);

app.listen(3000, () => console.log("Server on 3000"));

B. 前端登入 + 丟 Token 給後端寫 Cookie

import { getAuth, signInWithEmailAndPassword } from "firebase/auth";

export const loginAndSetCookie = async (email, password) => {
  const auth = getAuth();
  const result = await signInWithEmailAndPassword(auth, email, password);
  const idToken = await result.user.getIdToken();

  // 把 Token 丟去後端,由後端寫入 Cookie
  const res = await fetch("/auth/verify", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    credentials: "include", // 重點!
    body: JSON.stringify({ idToken }),
  });

  if (!res.ok) throw new Error("驗證失敗");

  return await res.json(); // 可取得 uid / email / message
};

C. 後續 API 請求:Cookie 自動帶上

export const getProtectedData = async () => {
  const res = await fetch("/api/protected", {
    method: "GET",
    credentials: "include",
  });

  if (!res.ok) throw new Error("未授權");
  return res.json();
};

後端就能從 Cookie 拿 Token:

router.get("/protected", async (req, res) => {
  const token = req.cookies?.token;
  if (!token) return res.status(401).json({ message: "沒有 Token" });

  try {
    const decoded = await admin.auth().verifyIdToken(token);
    res.json({ message: "你進來了!", user: decoded });
  } catch (err) {
    res.status(401).json({ message: "Token 無效" });
  }
});

D. 登出:清除 Cookie

export const logout = async () => {
  await fetch("/auth/logout", {
    method: "POST",
    credentials: "include",
  });
};

上一篇
屋內安全[ 7 / 7 ]:Token 要放哪裡?LocalStorage、Cookie、Memory 的優缺點與實作方式
下一篇
屋內安全[ 9 / 9 ]:權限配置與 Redux 管理登入狀態|角色控制 / 動態功能鎖定
系列文
不只是登入畫面!一起打造現代化登入系統23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言