EP.04網路與協定

認證與授權:JWT、OAuth2、Session
Cookie vs Token、Refresh Token、第三方登入

Authentication(認證)和 Authorization(授權)是兩個不同的問題,卻常被混淆。 選錯機制不只讓系統不安全,還讓使用者體驗變差。這篇帶你理解每種方案的設計邏輯與適用場景。

Joseph Chen 2026 14 min read JWT · OAuth2 · Session · Cookie · Auth

「Authentication:你是誰?Authorization:你能做什麼? 很多安全漏洞來自於混淆這兩個問題,或者實作其中一個時忘記另一個。」

從傳統的 Session-Cookie,到 JWT Token,到現在普遍的 OAuth2 第三方登入—— 每種方案都有它的設計取捨。理解原理,才能在不同場景做出正確選擇。

認證 vs 授權:先搞清楚概念

Authentication(認證)

你是誰?驗證身份的過程。

就像進辦公室時門禁刷卡——系統確認「這是員工 Joseph」。

• 帳號密碼登入

• Google / GitHub 第三方登入

• 手機簡訊 OTP

• 生物辨識(Face ID)

Authorization(授權)

你能做什麼?在確認身份後,確認你有沒有權限執行特定操作。

就像進了辦公室後,只有 HR 能開薪資系統。

• 角色控制(RBAC):admin / user / guest

• 資源所有者:只能刪自己的文章

• API Scope:只能讀取,不能寫入

Session-Cookie:傳統有狀態認證

Session-Cookie 是最傳統的 Web 認證方式。伺服器「記住」使用者的登入狀態,存在記憶體或 Redis 中。

Session-Cookie 流程

① 登入使用者 POST /login,帶帳號密碼
② 驗證伺服器驗證成功,建立 Session(session_id + 使用者資料)存在 Redis
③ Set-Cookie伺服器回傳 Set-Cookie: session_id=abc123; HttpOnly; Secure
④ 後續請求瀏覽器自動帶上 Cookie: session_id=abc123
⑤ 驗證 Session伺服器用 session_id 到 Redis 查詢使用者資料,確認登入狀態
⑥ 登出伺服器刪除 Redis 中的 Session,使 Cookie 立即失效

Set-Cookie 的三個安全屬性

HttpOnly禁止 JavaScript 讀取 Cookie(document.cookie)。防止 XSS 攻擊竊取 Session ID。
Secure只在 HTTPS 連線時傳送 Cookie。防止明文傳輸被中間人竊聽。
SameSite=Strict跨站請求(CSRF)不帶 Cookie。Strict:完全禁止跨站帶;Lax(推薦):允許從外部連結導入,但阻止 POST 跨站請求。

以上三者應同時設定。省略任何一個都可能留下安全漏洞。

✅ Session 的優點

  • • 可以立即撤銷(刪除 Redis 中的 Session)
  • • 敏感資料存在伺服器端,不暴露給客戶端
  • • 瀏覽器自動管理 Cookie,不需要前端處理

❌ Session 的缺點

  • • 有狀態(Stateful),水平擴展需要共享 Session 存儲
  • • CSRF(跨站請求偽造)攻擊的目標
  • • 不適合 API / 行動 App(Cookie 是瀏覽器限定)

JWT:無狀態的 Token 認證

JWT(JSON Web Token)把使用者資訊編碼在 Token 本身,伺服器不需要儲存 Session—— 只需要驗證 Token 的簽名是否有效。

JWT 結構:三段 Base64 用「.」分隔

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJqb3NlcGhAZXhhbXBsZS5jb20iLCJleHAiOjE3MDAwMDAwMDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header(紅色)

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload(藍色)

{
  "id": 1,
  "email": "...",
  "exp": 1700000000
}

Signature(綠色)

HMACSHA256(
base64(header) + "." + base64(payload),
SECRET_KEY)

⚠️ Payload 只是 Base64 編碼(不是加密),任何人都能解碼看到內容!不要放敏感資料(密碼、卡號)。

JWT 的正確使用方式(Node.js)
import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET!;  // 至少 32 字元的隨機字串

// ── 登入時:簽發 JWT ──────────────────────────────
async function login(email: string, password: string) {
    const user = await db.user.findUnique({ where: { email } });
    if (!user || !await bcrypt.compare(password, user.hashedPassword)) {
        throw new Error('Invalid credentials');
    }

    const accessToken = jwt.sign(
        { userId: user.id, email: user.email },
        SECRET,
        { expiresIn: '15m' }   // Access Token 短存活期(15 分鐘)
    );

    const refreshToken = jwt.sign(
        { userId: user.id },
        SECRET,
        { expiresIn: '7d' }    // Refresh Token 長存活期(7 天)
    );

    // Refresh Token 存在 HttpOnly Cookie(更安全)
    // Access Token 存在記憶體或 localStorage
    return { accessToken, refreshToken };
}

// ── 每次請求:驗證 JWT ────────────────────────────
function authenticate(req: Request) {
    const authHeader = req.headers.get('Authorization');
    const token = authHeader?.replace('Bearer ', '');
    if (!token) throw new Error('No token');

    try {
        const payload = jwt.verify(token, SECRET) as { userId: number };
        return payload.userId;
    } catch (err) {
        throw new Error('Invalid or expired token');
    }
}

// ── Access Token 過期時:用 Refresh Token 換新的 ──
async function refreshAccessToken(refreshToken: string) {
    const payload = jwt.verify(refreshToken, SECRET) as { userId: number };
    const newAccessToken = jwt.sign({ userId: payload.userId }, SECRET, { expiresIn: '15m' });
    return newAccessToken;
}

Refresh Token 撤銷:Redis 黑名單

JWT 一旦簽發就無法撤銷(直到過期)。若使用者登出或帳號被停用, Access Token 15 分鐘後才會失效。需要撤銷 Refresh Token 來防止換新 Token。

Refresh Token 撤銷(Redis 黑名單)
// 登出時:把 Refresh Token 加進黑名單
async function logout(refreshToken: string) {
  const payload = jwt.verify(refreshToken, SECRET) as { userId: number; exp: number };
  const ttl = payload.exp - Math.floor(Date.now() / 1000);
  // 只需黑名單到原本的過期時間(7天),TTL到期後自動清除
  await redis.setex(`blacklist:${refreshToken}`, ttl, '1');
}

// 換新 Access Token 時:先檢查黑名單
async function refreshAccessToken(refreshToken: string) {
  const isRevoked = await redis.exists(`blacklist:${refreshToken}`);
  if (isRevoked) throw new Error('Refresh token has been revoked');

  const payload = jwt.verify(refreshToken, SECRET) as { userId: number };
  return jwt.sign({ userId: payload.userId }, SECRET, { expiresIn: '15m' });
}

JWT 最常見的錯誤

  • 存在 localStorage:容易被 XSS 竊取,改用記憶體(React state)+ Refresh Token in HttpOnly Cookie
  • 存活期太長:Access Token 應該短(15min),搭配 Refresh Token 延續 session
  • Payload 放密碼:Payload 只是 Base64,任何人都能解碼
  • 忘記處理過期:前端要捕捉 401,自動用 Refresh Token 換新 Access Token

OAuth2:第三方登入的標準流程

OAuth2 是「授權委派」協定——讓使用者授權你的應用存取他在 Google / GitHub / Facebook 的資料, 而不需要把密碼給你。「使用 Google 登入」就是 OAuth2 的 Authorization Code Flow。

OAuth2 Authorization Code Flow(最安全的流程)

① 使用者點「用 Google 登入」你的 App 把使用者導向 Google 的授權頁
② Google 詢問使用者「要允許 App 存取你的 email 和 profile 嗎?」
③ 使用者同意Google 重定向回 App,帶上一次性的 Authorization Code(短存活期)
④ 後端用 Code 換 TokenApp 後端(不是前端!)向 Google 換取 Access Token 和 ID Token
⑤ 取得使用者資料用 Access Token 向 Google 的 API 取得使用者的 email、姓名等
⑥ 建立 SessionApp 查詢或建立本地使用者帳號,簽發自己的 JWT / Session
Next.js 用 NextAuth.js 實作 Google 登入(最簡方式)
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';

const handler = NextAuth({
    providers: [
        GoogleProvider({
            clientId: process.env.GOOGLE_CLIENT_ID!,
            clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
        }),
    ],
    callbacks: {
        async jwt({ token, account, profile }) {
            // 第一次登入時,account 有 Google 回傳的資料
            if (account && profile) {
                token.userId = await findOrCreateUser(profile.email, profile.name);
            }
            return token;
        },
        async session({ session, token }) {
            session.userId = token.userId;  // 把 userId 帶進 session
            return session;
        },
    },
});

export { handler as GET, handler as POST };

// ── 在組件中使用 ──────────────────────────────────
// 'use client'
import { useSession, signIn, signOut } from 'next-auth/react';

export function AuthButton() {
    const { data: session } = useSession();
    if (session) {
        return <button onClick={() => signOut()}>登出 {session.user?.name}</button>;
    }
    return <button onClick={() => signIn('google')}>用 Google 登入</button>;
}

⚠️ OAuth2 和 Session / JWT 的關係

OAuth2 解決的是「使用者授權第三方存取其帳號」的問題——它本身不規定你要用 Session 還是 JWT。 第⑥步 NextAuth 拿到 Google 的使用者資料後,你的 App 自己決定要簽發 Session(存 Redis)還是 JWT。 NextAuth 預設使用加密的 Session Cookie(本質是 JWT),你也可以換成 Database Adapter 改用 Redis Session。

Session vs JWT:怎麼選?

Session-CookieJWT
有無狀態Stateful(需要 Redis)Stateless(伺服器不存資料)
水平擴展需要共享 Session Store✅ 任一台伺服器都能驗證
立即撤銷✅ 刪 Redis 即時生效❌ 需等 Token 自然過期(或黑名單)
適合場景Web 應用(傳統 MVC)API、行動 App、微服務
CSRF 風險有(Cookie 自動帶上)無(需手動帶 Header)
XSS 風險低(HttpOnly Cookie)高(若存 localStorage)

實務建議

傳統 Web App(Next.js SSR):用 NextAuth.js,它幫你管理 Session-Cookie,簡單安全。
Pure API / 行動 App:用 JWT(短存活 Access Token + HttpOnly Cookie 的 Refresh Token)。
第三方登入:不要自己實作 OAuth2,用 NextAuth.js / Auth.js / Clerk 等成熟方案。

本篇重點回顧

🎭Authentication(認證)= 你是誰;Authorization(授權)= 你能做什麼。這是兩個不同的問題,要分開處理。
🍪Session-Cookie:有狀態,能立即撤銷,適合 Web;擴展需要共享 Redis,有 CSRF 風險。
🎟️JWT 三段結構:Header(演算法)+ Payload(資料,僅 Base64)+ Signature(驗證簽名)。Payload 不是加密!
⏱️Access Token 短存活(15min)+ Refresh Token 長存活(7 天,存 HttpOnly Cookie),這是目前最佳實踐。
🌐OAuth2 Authorization Code Flow:使用者授權 → Code → 後端換 Token → 取得使用者資料。Code 交換必須在後端做。
🛡️直接用 NextAuth.js / Clerk,不要自己實作 OAuth2——Auth 的細節太多,自己實作很容易出安全漏洞。
JWT
OAuth2
Session
Cookie
Authentication
Authorization
NextAuth
EP.04