跳至主要內容
Skip to content

JWT 深入剖析:結構、簽名與實戰

JWT(JSON Web Token)是現代 API 認證的主流方案。本篇將深入解析 JWT 的每個細節。


一、 JWT 結構

1.1 三部分組成

JWT 由三個部分組成,用 . 分隔:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
└────────────────Header────────────────┘.└──────────────────────Payload──────────────────────┘.└───────────Signature───────────┘

1.2 Header(標頭)

json
{
  "alg": "HS256", // 簽名算法
  "typ": "JWT" // Token 類型
}

Base64Url 編碼後:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

1.3 Payload(載荷)

json
{
  "sub": "1234567890", // Subject(用戶 ID)
  "name": "John Doe", // 自定義資料
  "iat": 1516239022, // Issued At(發行時間)
  "exp": 1516242622 // Expiration(過期時間)
}

標準聲明(Registered Claims)

Claim全名說明
issIssuer發行者
subSubject主題(通常是用戶 ID)
audAudience接收者
expExpiration過期時間(Unix 時間戳)
nbfNot Before生效時間
iatIssued At發行時間
jtiJWT IDToken 唯一識別碼

1.4 Signature(簽名)

javascript
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret);

二、 簽名算法

2.1 對稱加密(HMAC)

javascript
// HS256、HS384、HS512
// 發行和驗證使用相同的密鑰

const jwt = require("jsonwebtoken");
const SECRET = "my-secret-key";

// 簽發
const token = jwt.sign({ userId: 1 }, SECRET);

// 驗證
const decoded = jwt.verify(token, SECRET);

優點:簡單、快速 缺點:密鑰需要共享給所有驗證方

2.2 非對稱加密(RSA/ECDSA)

javascript
// RS256、RS384、RS512
// 私鑰簽發,公鑰驗證

const fs = require("fs");
const privateKey = fs.readFileSync("private.pem");
const publicKey = fs.readFileSync("public.pem");

// 簽發(使用私鑰)
const token = jwt.sign({ userId: 1 }, privateKey, { algorithm: "RS256" });

// 驗證(使用公鑰)
const decoded = jwt.verify(token, publicKey);

優點:公鑰可以公開,適合微服務 缺點:較慢

2.3 算法選擇

場景推薦算法
單一服務HS256
微服務RS256
行動端RS256
高安全需求RS512 或 ES256

三、 完整實作

3.1 Token 服務

javascript
const jwt = require("jsonwebtoken");

const ACCESS_SECRET = process.env.ACCESS_SECRET;
const REFRESH_SECRET = process.env.REFRESH_SECRET;

class TokenService {
  // 生成 Access Token
  generateAccessToken(user) {
    return jwt.sign(
      {
        userId: user.id,
        email: user.email,
        role: user.role,
      },
      ACCESS_SECRET,
      {
        expiresIn: "15m",
        issuer: "myapp.com",
        audience: "myapp.com",
      }
    );
  }

  // 生成 Refresh Token
  generateRefreshToken(user) {
    return jwt.sign(
      {
        userId: user.id,
        tokenVersion: user.tokenVersion,
      },
      REFRESH_SECRET,
      {
        expiresIn: "7d",
        issuer: "myapp.com",
      }
    );
  }

  // 驗證 Access Token
  verifyAccessToken(token) {
    try {
      return jwt.verify(token, ACCESS_SECRET, {
        issuer: "myapp.com",
        audience: "myapp.com",
      });
    } catch (err) {
      return null;
    }
  }

  // 驗證 Refresh Token
  verifyRefreshToken(token) {
    try {
      return jwt.verify(token, REFRESH_SECRET, {
        issuer: "myapp.com",
      });
    } catch (err) {
      return null;
    }
  }

  // 解碼(不驗證)
  decode(token) {
    return jwt.decode(token);
  }
}

module.exports = new TokenService();

3.2 認證中介軟體

javascript
const tokenService = require("./token.service");

function authenticate(req, res, next) {
  // 從 Header 或 Cookie 取得 Token
  let token = req.cookies?.accessToken;

  if (!token) {
    const authHeader = req.headers.authorization;
    if (authHeader?.startsWith("Bearer ")) {
      token = authHeader.substring(7);
    }
  }

  if (!token) {
    return res.status(401).json({ error: "No token provided" });
  }

  const decoded = tokenService.verifyAccessToken(token);

  if (!decoded) {
    return res.status(401).json({ error: "Invalid or expired token" });
  }

  req.user = decoded;
  next();
}

// 可選認證(有 Token 就驗證,沒有也通過)
function optionalAuth(req, res, next) {
  const token = req.cookies?.accessToken;

  if (token) {
    const decoded = tokenService.verifyAccessToken(token);
    if (decoded) {
      req.user = decoded;
    }
  }

  next();
}

// 角色檢查
function authorize(...roles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: "Not authenticated" });
    }

    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: "Not authorized" });
    }

    next();
  };
}

3.3 登入流程

javascript
const tokenService = require("./token.service");

app.post("/api/login", async (req, res) => {
  const { email, password } = req.body;

  // 驗證使用者
  const user = await User.findOne({ email });
  if (!user || !(await user.comparePassword(password))) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  // 生成 Tokens
  const accessToken = tokenService.generateAccessToken(user);
  const refreshToken = tokenService.generateRefreshToken(user);

  // Refresh Token 存入 HttpOnly Cookie
  res.cookie("refreshToken", refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "strict",
    maxAge: 7 * 24 * 60 * 60 * 1000,
    path: "/api/auth/refresh",
  });

  // Access Token 也可以存 Cookie(或返回給前端)
  res.cookie("accessToken", accessToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "strict",
    maxAge: 15 * 60 * 1000,
  });

  res.json({
    user: { id: user.id, email: user.email, role: user.role },
    expiresIn: 15 * 60,
  });
});

四、 安全最佳實踐

4.1 不要在 Payload 放敏感資訊

javascript
// ❌ 錯誤
jwt.sign({ password: "secret", creditCard: "1234..." }, SECRET);

// ✅ 正確
jwt.sign({ userId: 123, role: "user" }, SECRET);

Payload 只是 Base64 編碼,任何人都可以解碼:

javascript
// 解碼 Payload(不需要密鑰)
const [, payload] = token.split(".");
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString());

4.2 設置合理的過期時間

javascript
// Access Token:短期
jwt.sign(payload, SECRET, { expiresIn: "15m" });

// Refresh Token:較長
jwt.sign(payload, SECRET, { expiresIn: "7d" });

4.3 使用強密鑰

javascript
// ❌ 弱密鑰
const SECRET = "secret";

// ✅ 強密鑰(至少 256 bits)
const SECRET = crypto.randomBytes(32).toString("hex");
// 或使用環境變數
const SECRET = process.env.JWT_SECRET;

4.4 驗證所有 Claims

javascript
jwt.verify(token, SECRET, {
  issuer: "myapp.com", // 驗證發行者
  audience: "myapp.com", // 驗證接收者
  algorithms: ["HS256"], // 限制算法
});

4.5 防止算法混淆攻擊

javascript
// ❌ 危險:不指定算法
jwt.verify(token, SECRET);

// ✅ 安全:明確指定算法
jwt.verify(token, SECRET, { algorithms: ["HS256"] });

五、 Token 撤銷策略

5.1 短期 Token + Refresh

javascript
// Access Token 15 分鐘過期
// 即使被盜,窗口期只有 15 分鐘

5.2 拒絕清單

javascript
const redis = require("redis");
const client = redis.createClient();

// 登出時加入拒絕清單
async function revokeToken(token) {
  const decoded = jwt.decode(token);
  const ttl = decoded.exp - Math.floor(Date.now() / 1000);

  if (ttl > 0) {
    await client.setEx(`revoked:${token}`, ttl, "1");
  }
}

// 驗證時檢查
async function isTokenRevoked(token) {
  const result = await client.get(`revoked:${token}`);
  return result !== null;
}

5.3 Token 版本號

javascript
// User Schema
{
  tokenVersion: { type: Number, default: 0 }
}

// 登出:增加版本號
await User.updateOne({ _id: userId }, { $inc: { tokenVersion: 1 } })

// 驗證時檢查版本
const decoded = jwt.verify(refreshToken, SECRET)
const user = await User.findById(decoded.userId)

if (decoded.tokenVersion !== user.tokenVersion) {
  throw new Error('Token revoked')
}

六、 常見問題

6.1 Token Not Yet Valid

Error: jwt not active

// 原因:nbf(Not Before)還沒到
// 檢查伺服器時間是否正確

6.2 Token Expired

javascript
jwt.verify(token, SECRET);
// Error: jwt expired

// 處理方式
try {
  const decoded = jwt.verify(token, SECRET);
} catch (err) {
  if (err.name === "TokenExpiredError") {
    // 嘗試使用 Refresh Token
  }
}

6.3 Invalid Signature

javascript
// 原因:密鑰不匹配或 Token 被篡改
jwt.verify(token, WRONG_SECRET);
// Error: invalid signature

七、 前端處理

7.1 自動刷新

javascript
class AuthService {
  constructor() {
    this.accessToken = null;
    this.refreshPromise = null;
  }

  async request(url, options = {}) {
    const response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${this.accessToken}`,
      },
    });

    if (response.status === 401) {
      await this.refresh();
      return this.request(url, options);
    }

    return response;
  }

  async refresh() {
    // 避免並發刷新
    if (this.refreshPromise) {
      return this.refreshPromise;
    }

    this.refreshPromise = fetch("/api/auth/refresh", {
      method: "POST",
      credentials: "include",
    })
      .then(async (res) => {
        if (res.ok) {
          const data = await res.json();
          this.accessToken = data.accessToken;
        } else {
          throw new Error("Refresh failed");
        }
      })
      .finally(() => {
        this.refreshPromise = null;
      });

    return this.refreshPromise;
  }
}

7.2 Axios 攔截器

javascript
axios.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        await refreshToken();
        return axios(originalRequest);
      } catch (refreshError) {
        window.location.href = "/login";
      }
    }

    return Promise.reject(error);
  }
);

總結

概念說明
Header算法和類型
Payload用戶資料和聲明
Signature防篡改簽名
Access Token短期,用於 API
Refresh Token長期,用於刷新
密鑰強度至少 256 bits
過期處理自動刷新機制

> **安全公式**:

  • 短期 Access Token + 長期 Refresh Token
  • HttpOnly Cookie 儲存
  • 強密鑰 + 明確算法

進階挑戰

  1. 實作 JWK(JSON Web Key)端點,支援公鑰輪換。
  2. 研究並實作 JWE(JSON Web Encryption),加密 Payload。
  3. 設計一個支援多設備管理的 Token 系統,可以單獨登出某個設備。

延伸閱讀與資源