跳至正文

🎫 JWT 令牌完全理解

📖 定義

JWT(JSON Web Token)是用於在客戶端和伺服器之間安全傳輸資訊的基於JSON的令牌。由三部分(Header、Payload、Signature)組成,由伺服器頒發,客戶端在請求時一起發送進行認證處理。與會話不同,不在伺服器儲存狀態(stateless),因此擴展性優秀。

🎯 用類比理解

遊樂園門票

將JWT比作遊樂園門票:

普通門票(會話方式)
├─ 入場時獲得手環
├─ 每次乘坐時工作人員檢查手環
└─ 工作人員需要記住所有訪客(伺服器負擔)

VIP門票(JWT方式)
├─ 入場時獲得防偽印章的門票
├─ 門票上記錄姓名、有效期、等級
├─ 每次乘坐只需出示門票
└─ 工作人員只檢查印章(伺服器負擔小)

JWT = 帶有防偽印章的資訊卡

⚙️ 工作原理

1. JWT結構

JWT = Header.Payload.Signature

例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywidXNlcm5hbWUiOiJraW0ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
│ │ │
Header (標頭) Payload (有效載荷) Signature (簽章)

Header (標頭)

{
"alg": "HS256", // 簽章演算法
"typ": "JWT" // 令牌類型
}
// Base64編碼

Payload (有效載荷) - 實際資料

{
"userId": 123,
"username": "kim",
"email": "kim@example.com",
"role": "admin",
"iat": 1640000000, // Issued At (頒發時間)
"exp": 1640086400 // Expiration (過期時間)
}
// Base64編碼

Signature (簽章) - 防止偽造

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret // 金鑰(只有伺服器知道)
)

2. JWT認證流程

1. 登入
客戶端 → 伺服器: username, password
伺服器: 認證成功 → 產生JWT並返回

2. 請求時包含JWT
客戶端 → 伺服器: Authorization: Bearer <JWT>

3. 伺服器驗證JWT
- 驗證簽章(是否偽造)
- 驗證過期時間
- 從Payload提取使用者資訊

4. 回應
伺服器 → 客戶端: 請求的資料

💡 實際範例

JWT產生和驗證 (Node.js)

const jwt = require('jsonwebtoken');

// 金鑰(建議用環境變數管理)
const SECRET_KEY = 'your-secret-key-keep-it-safe';

// ========== 產生JWT ==========
function generateToken(user) {
// 定義Payload
const payload = {
userId: user.id,
username: user.username,
email: user.email,
role: user.role
};

// 選項
const options = {
expiresIn: '1h', // 1小時後過期
// expiresIn: '7d', // 7天
// expiresIn: '30m', // 30分鐘
issuer: 'my-app', // 頒發者
subject: 'user-auth' // 用途
};

// 產生JWT
const token = jwt.sign(payload, SECRET_KEY, options);
return token;
}

// ========== 驗證JWT ==========
function verifyToken(token) {
try {
const decoded = jwt.verify(token, SECRET_KEY);
console.log('驗證成功:', decoded);
/*
{
userId: 123,
username: 'kim',
email: 'kim@example.com',
role: 'admin',
iat: 1640000000,
exp: 1640003600
}
*/
return decoded;
} catch (error) {
if (error.name === 'TokenExpiredError') {
console.error('令牌已過期');
} else if (error.name === 'JsonWebTokenError') {
console.error('無效的令牌');
}
return null;
}
}

// ========== 使用範例 ==========
const user = {
id: 123,
username: 'kim',
email: 'kim@example.com',
role: 'admin'
};

const token = generateToken(user);
console.log('產生的JWT:', token);

const decoded = verifyToken(token);
console.log('解碼:', decoded);

登入API實作

const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');

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

const SECRET_KEY = process.env.JWT_SECRET || 'your-secret-key';
const users = []; // 實際應使用資料庫

// ========== 註冊 ==========
app.post('/api/register', async (req, res) => {
const { username, password, email } = req.body;

// 密碼雜湊化
const hashedPassword = await bcrypt.hash(password, 10);

const user = {
id: users.length + 1,
username,
email,
password: hashedPassword
};

users.push(user);

res.json({ message: '註冊成功', userId: user.id });
});

// ========== 登入 ==========
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;

// 查詢使用者
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).json({ error: '使用者不存在' });
}

// 驗證密碼
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: '密碼錯誤' });
}

// 產生JWT
const token = jwt.sign(
{
userId: user.id,
username: user.username,
email: user.email
},
SECRET_KEY,
{ expiresIn: '1h' }
);

// 同時頒發Refresh Token(可選)
const refreshToken = jwt.sign(
{ userId: user.id },
SECRET_KEY,
{ expiresIn: '7d' }
);

res.json({
message: '登入成功',
accessToken: token,
refreshToken: refreshToken
});
});

// ========== 認證中介軟體 ==========
function authenticateToken(req, res, next) {
// 從Authorization標頭提取令牌
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"

if (!token) {
return res.status(401).json({ error: '無令牌' });
}

jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: '令牌已過期' });
}
return res.status(403).json({ error: '無效的令牌' });
}

// 驗證成功 - 將使用者資訊儲存到req
req.user = user;
next();
});
}

// ========== 受保護的路由 ==========
app.get('/api/profile', authenticateToken, (req, res) => {
// req.user是從JWT提取的資訊
res.json({
message: '個人資料資訊',
user: req.user
});
});

app.get('/api/admin', authenticateToken, (req, res) => {
// 權限檢查
if (req.user.role !== 'admin') {
return res.status(403).json({ error: '需要管理員權限' });
}

res.json({ message: '管理員頁面' });
});

app.listen(3000, () => {
console.log('伺服器執行中: http://localhost:3000');
});

前端使用JWT

// ========== 登入 ==========
async function login(username, password) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});

const data = await response.json();

if (response.ok) {
// 將JWT儲存到LocalStorage
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);

console.log('登入成功!');
}
}

// ========== API請求時包含JWT ==========
async function fetchProfile() {
const token = localStorage.getItem('accessToken');

const response = await fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${token}`
}
});

if (response.status === 401) {
// 令牌過期 → 跳轉到登入頁
window.location.href = '/login';
return;
}

const data = await response.json();
console.log('個人資料:', data);
}

// ========== 使用Axios攔截器自動化 ==========
import axios from 'axios';

// 請求攔截器: 自動為所有請求加入JWT
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);

// 回應攔截器: 401錯誤時自動登出
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('accessToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);

// 現在所有請求都會自動包含JWT!
axios.get('/api/profile').then(response => {
console.log(response.data);
});

Refresh Token實作

// ========== Access Token + Refresh Token模式 ==========

// Access Token: 短期(15分鐘)
// Refresh Token: 長期(7天)

app.post('/api/login', async (req, res) => {
// ... 登入驗證 ...

const accessToken = jwt.sign(
{ userId: user.id, username: user.username },
SECRET_KEY,
{ expiresIn: '15m' } // 15分鐘
);

const refreshToken = jwt.sign(
{ userId: user.id },
REFRESH_SECRET_KEY,
{ expiresIn: '7d' } // 7天
);

// Refresh Token儲存到DB(可選)
// await saveRefreshToken(user.id, refreshToken);

res.json({ accessToken, refreshToken });
});

// ========== 刷新Access Token ==========
app.post('/api/refresh', (req, res) => {
const { refreshToken } = req.body;

if (!refreshToken) {
return res.status(401).json({ error: '無Refresh Token' });
}

jwt.verify(refreshToken, REFRESH_SECRET_KEY, (err, user) => {
if (err) {
return res.status(403).json({ error: '無效的Refresh Token' });
}

// 頒發新的Access Token
const accessToken = jwt.sign(
{ userId: user.userId },
SECRET_KEY,
{ expiresIn: '15m' }
);

res.json({ accessToken });
});
});

// ========== 前端: 自動刷新令牌 ==========
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;

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

try {
// 用Refresh Token獲取新的Access Token
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/api/refresh', { refreshToken });

const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);

// 重試原始請求
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh Token也過期 → 登出
localStorage.clear();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}

return Promise.reject(error);
}
);

🤔 常見問題

Q1. JWT vs 會話有什麼區別?

A:

// ========== 會話方式 ==========
// 伺服器儲存使用者資訊

登入 → 伺服器儲存會話
{
sessionId: 'abc123',
userId: 123,
username: 'kim'
}

客戶端只儲存sessionId
Cookie: sessionId=abc123

請求時用sessionId查詢伺服器會話

優點: 伺服器可立即無效化
缺點: 需要伺服器記憶體/儲存,擴展性低

// ========== JWT方式 ==========
// 客戶端儲存令牌

登入 → 產生JWT並返回
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

客戶端儲存整個JWT
localStorage.setItem('token', jwt)

請求時包含JWT,伺服器只驗證簽章

優點: 伺服器無負擔,擴展性好
缺點: 難以無效化令牌,體積大

Q2. JWT安全嗎?

A: 正確使用時安全:

// ✅ 安全的使用方法
1. 必須使用HTTPS
- HTTP會被攔截JWT

2. 金鑰安全
- 用環境變數管理
- 絕對禁止硬編碼到程式碼中
const SECRET = process.env.JWT_SECRET;

3. 短過期時間
- Access Token: 15分鐘~1小時
- 用Refresh Token刷新

4. 禁止儲存敏感資訊
{ password: '1234', ssn: '123-45-6789' }
{ userId: 123, role: 'user' }

5. 防止XSS
- 推薦使用HttpOnly Cookie而非LocalStorage
res.cookie('token', jwt, {
httpOnly: true, // JavaScript無法存取
secure: true, // 僅HTTPS
sameSite: 'strict'
});

// ❌ 危險的使用方法
1. 無過期時間
2.HTTP傳輸
3. 儲存在LocalStorage(XSS漏洞)
4. 包含密碼等敏感資訊

Q3. 如何無效化JWT?

A: 有幾種方法:

// 1. 黑名單(Blacklist)
// - 登出時將令牌儲存到DB/Redis
// - 請求時檢查黑名單

const blacklist = new Set();

app.post('/api/logout', authenticateToken, (req, res) => {
const token = req.headers.authorization.split(' ')[1];
blacklist.add(token);
res.json({ message: '登出成功' });
});

function authenticateToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];

if (blacklist.has(token)) {
return res.status(401).json({ error: '已無效化的令牌' });
}

// ... JWT驗證 ...
}

// 2. 儲存到Redis(設定過期時間)
const redis = require('redis');
const client = redis.createClient();

app.post('/api/logout', authenticateToken, async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token);

// 黑名單保持到令牌過期時間
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
await client.setEx(`blacklist:${token}`, ttl, 'true');

res.json({ message: '登出成功' });
});

// 3. 短期 + Refresh Token
// - Access Token: 15分鐘(無需無效化)
// - 只在DB中儲存和無效化Refresh Token

// 4. 版本管理
// - 在DB中儲存使用者令牌版本
// - 登出時增加版本號

const payload = {
userId: user.id,
tokenVersion: user.tokenVersion // 從DB獲取
};

// 登出
await db.users.update(
{ id: userId },
{ $inc: { tokenVersion: 1 } } // 增加版本號
);

Q4. JWT應該儲存在哪裡?

A:

// 選項1: LocalStorage(簡單但有XSS漏洞)
localStorage.setItem('token', jwt);
// ❌ XSS攻擊可竊取令牌

// 選項2: SessionStorage(關閉分頁即刪除)
sessionStorage.setItem('token', jwt);
// ❌ XSS漏洞

// 選項3: HttpOnly Cookie(最安全,推薦)
// 伺服器設定
res.cookie('token', jwt, {
httpOnly: true, // JavaScript無法存取(防XSS)
secure: true, // 僅HTTPS
sameSite: 'strict',// 防CSRF
maxAge: 3600000 // 1小時
});

// 客戶端自動發送
fetch('/api/profile', {
credentials: 'include' // 包含Cookie
});

// 選項4: 記憶體(最安全但重新整理後登出)
let token = null;

function login() {
// ... 登入 ...
token = response.accessToken; // 僅儲存在變數
}

// 優缺點比較
LocalStorage: 簡單但有XSS漏洞
Cookie: 安全但設定複雜
Memory: 最安全但UX

Q5. Access Token vs Refresh Token?

A:

// Access Token
// - 短期(15分鐘~1小時)
// - 每次API請求都發送
// - 被竊取也會快速過期

// Refresh Token
// - 長期(7天~30天)
// - 僅在刷新Access Token時使用
// - 儲存在DB中管理

// 流程
1. 登入
→ 頒發Access Token(15分鐘) + Refresh Token(7)

2. API請求
→ 使用Access Token

3. Access Token過期(15分鐘後)
→ 用Refresh Token獲取新的Access Token

4. Refresh Token也過期(7天後)
→ 需要重新登入

// 程式碼範例
// 頒發令牌
const accessToken = jwt.sign(payload, SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: '7d' });

// 自動刷新
if (accessTokenExpired) {
const newAccessToken = await refreshAccessToken(refreshToken);
}

🎓 下一步

理解JWT後,繼續學習:

  1. 什麼是HTTPS? (文件準備中) - 安全的令牌傳輸
  2. 什麼是CORS? (文件準備中) - API安全
  3. 什麼是Cookie? - 會話 vs JWT

有用的工具

// JWT偵錯器
// https://jwt.io
// - JWT解碼和驗證

// 函式庫
// Node.js: jsonwebtoken
// Python: PyJWT
// Java: jjwt
// Go: jwt-go

// 測試
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const decoded = jwt.decode(token, { complete: true });
console.log(decoded);
/*
{
header: { alg: 'HS256', typ: 'JWT' },
payload: { userId: 123, ... },
signature: '...'
}
*/

🎬 總結

JWT是現代Web的標準認證方式:

  • 結構: Header.Payload.Signature
  • 優點: 無狀態、可擴展、支援多平台
  • 安全: HTTPS、HttpOnly Cookie、短期有效
  • 模式: Access Token + Refresh Token

正確使用可建構安全高效的認證系統! 🎫✨