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 | 全名 | 說明 |
|---|---|---|
iss | Issuer | 發行者 |
sub | Subject | 主題(通常是用戶 ID) |
aud | Audience | 接收者 |
exp | Expiration | 過期時間(Unix 時間戳) |
nbf | Not Before | 生效時間 |
iat | Issued At | 發行時間 |
jti | JWT ID | Token 唯一識別碼 |
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 儲存
- 強密鑰 + 明確算法
進階挑戰
- 實作 JWK(JSON Web Key)端點,支援公鑰輪換。
- 研究並實作 JWE(JSON Web Encryption),加密 Payload。
- 設計一個支援多設備管理的 Token 系統,可以單獨登出某個設備。