實作安全的 API 設計模式,包括身分驗證、授權、輸入驗證、速率限制,以及防範常見的 API 弱點
API 安全最佳實務
概述
引導開發者透過實作身分驗證、授權、輸入驗證、速率限制及防範常見弱點,來建構安全的 API。此技能涵蓋 REST、GraphQL 與 WebSocket API 的安全模式。
使用時機
- 設計新的 API 端點時
- 保護現有 API 時
- 實作身分驗證與授權時
- 防範 API 攻擊(如注入、DDoS 等)時
- 進行 API 安全審查時
- 準備安全稽核時
- 實作速率限制與節流時
- 處理 API 中的敏感資料時
運作方式
步驟 1:身分驗證與授權
我將協助您實作安全的身分驗證:
- 選擇驗證方式(JWT、OAuth 2.0、API 金鑰)
- 實作基於 Token 的身分驗證
- 設定基於角色的存取控制(RBAC)
- 安全的工作階段管理
- 實作多因素驗證(MFA)
步驟 2:輸入驗證與清理
防範注入攻擊:
- 驗證所有輸入資料
- 清理使用者輸入
- 使用參數化查詢
- 實作請求結構驗證
- 防止 SQL 注入、XSS 與命令注入
步驟 3:速率限制與節流
防止濫用與 DDoS 攻擊:
- 對每個使用者/IP 實作速率限制
- 設定 API 節流
- 設定請求配額
- 優雅處理速率限制錯誤
- 監控可疑活動
步驟 4:資料保護
保護敏感資料:
- 傳輸中加密(HTTPS/TLS)
- 靜態敏感資料加密
- 實作適當的錯誤處理(避免資料洩漏)
- 清理錯誤訊息
- 使用安全標頭
步驟 5:API 安全測試
驗證安全實作:
- 測試身分驗證與授權
- 執行滲透測試
- 檢查常見弱點(OWASP API Top 10)
- 驗證輸入處理
- 測試速率限制
範例
範例 1:實作 JWT 身分驗證
## 安全的 JWT 身分驗證實作
### 驗證流程
1. 使用者以憑證登入
2. 伺服器驗證憑證
3. 伺服器產生 JWT Token
4. 用戶端安全儲存 Token
5. 用戶端每次請求攜帶 Token
6. 伺服器驗證 Token
### 實作
#### 1. 產生安全的 JWT Token
\`\`\`javascript
// auth.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
// 登入端點
app.post('/api/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// 驗證輸入
if (!email || !password) {
return res.status(400).json({
error: '需要提供電子郵件與密碼'
});
}
// 尋找使用者
const user = await db.user.findUnique({
where: { email }
});
if (!user) {
// 不要透露使用者是否存在
return res.status(401).json({
error: '憑證無效'
});
}
// 驗證密碼
const validPassword = await bcrypt.compare(
password,
user.passwordHash
);
if (!validPassword) {
return res.status(401).json({
error: '憑證無效'
});
}
// 產生 JWT Token
const token = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{
expiresIn: '1h',
issuer: 'your-app',
audience: 'your-app-users'
}
);
// 產生 Refresh Token
const refreshToken = jwt.sign(
{ userId: user.id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
// 將 Refresh Token 儲存於資料庫
await db.refreshToken.create({
data: {
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
}
});
res.json({
token,
refreshToken,
expiresIn: 3600
});
} catch (error) {
console.error('登入錯誤:', error);
res.status(500).json({
error: '登入時發生錯誤'
});
}
});
\`\`\`
#### 2. 驗證 JWT Token(中介軟體)
\`\`\`javascript
// middleware/auth.js
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
// 從標頭取得 Token
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
error: '需要存取 Token'
});
}
// 驗證 Token
jwt.verify(
token,
process.env.JWT_SECRET,
{
issuer: 'your-app',
audience: 'your-app-users'
},
(err, user) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token 已過期'
});
}
return res.status(403).json({
error: '無效的 Token'
});
}
// 將使用者附加到請求
req.user = user;
next();
}
);
}
module.exports = { authenticateToken };
\`\`\`
#### 3. 保護路由
\`\`\`javascript
const { authenticateToken } = require('./middleware/auth');
// 受保護的路由
app.get('/api/user/profile', authenticateToken, async (req, res) => {
try {
const user = await db.user.findUnique({
where: { id: req.user.userId },
select: {
id: true,
email: true,
name: true,
// 不要回傳 passwordHash
}
});
res.json(user);
} catch (error) {
res.status(500).json({ error: '伺服器錯誤' });
}
});
\`\`\`
#### 4. 實作 Token 刷新
\`\`\`javascript
app.post('/api/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
error: '需要 Refresh Token'
});
}
try {
// 驗證 Refresh Token
const decoded = jwt.verify(
refreshToken,
process.env.JWT_REFRESH_SECRET
);
// 檢查 Refresh Token 是否存在於資料庫
const storedToken = await db.refreshToken.findFirst({
where: {
token: refreshToken,
userId: decoded.userId,
expiresAt: { gt: new Date() }
}
});
if (!storedToken) {
return res.status(403).json({
error: '無效的 Refresh Token'
});
}
// 產生新的存取 Token
const user = await db.user.findUnique({
where: { id: decoded.userId }
});
const newToken = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({
token: newToken,
expiresIn: 3600
});
} catch (error) {
res.status(403).json({
error: '無效的 Refresh Token'
});
}
});
\`\`\`
### 安全最佳實務
- ✅ 使用強大的 JWT 密鑰(至少 256 位元)
- ✅ 設定較短的過期時間(存取 Token 1 小時)
- ✅ 實作 Refresh Token 以支援長時間工作階段
- ✅ 將 Refresh Token 儲存於資料庫(可撤銷)
- ✅ 僅使用 HTTPS
- ✅ 不要在 JWT 酬載中儲存敏感資料
- ✅ 驗證 Token 的發行者與受眾
- ✅ 實作 Token 黑名單以支援登出
範例 2:輸入驗證與 SQL 注入防範
## 防止 SQL 注入與輸入驗證
### 問題
**❌ 有弱點的程式碼:**
\`\`\`javascript
// 絕對不要這樣做 - SQL 注入弱點
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;
// 危險:使用者輸入直接放入查詢
const query = \`SELECT * FROM users WHERE id = '\${userId}'\`;
const user = await db.query(query);
res.json(user);
});
// 攻擊範例:
// GET /api/users/1' OR '1'='1
// 回傳所有使用者!
\`\`\`
### 解決方案
#### 1. 使用參數化查詢
\`\`\`javascript
// ✅ 安全:參數化查詢
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;
// 先驗證輸入
if (!userId || !/^\d+$/.test(userId)) {
return res.status(400).json({
error: '無效的使用者 ID'
});
}
// 使用參數化查詢
const user = await db.query(
'SELECT id, email, name FROM users WHERE id = $1',
[userId]
);
if (!user) {
return res.status(404).json({
error: '找不到使用者'
});
}
res.json(user);
});
\`\`\`
#### 2. 使用 ORM 並正確跳脫
\`\`\`javascript
// ✅ 安全:使用 Prisma ORM
app.get('/api/users/:id', async (req, res) => {
const userId = parseInt(req.params.id);
if (isNaN(userId)) {
return res.status(400).json({
error: '無效的使用者 ID'
});
}
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
name: true,
// 不要選取敏感欄位
}
});
if (!user) {
return res.status(404).json({
error: '找不到使用者'
});
}
res.json(user);
});
\`\`\`
#### 3. 使用 Zod 實作請求驗證
\`\`\`javascript
const { z } = require('zod');
// 定義驗證結構
const createUserSchema = z.object({
email: z.string().email('電子郵件格式無效'),
password: z.string()
.min(8, '密碼長度至少 8 個字元')
.regex(/[A-Z]/, '密碼必須包含大寫字母')
.regex(/[a-z]/, '密碼必須包含小寫字母')
.regex(/[0-9]/, '密碼必須包含數字'),
name: z.string()
.min(2, '名稱長度至少 2 個字元')
.max(100, '名稱太長'),
age: z.number()
.int('年齡必須為整數')
.min(18, '必須年滿 18 歲')
.max(120, '年齡無效')
.optional()
});
// 驗證中介軟體
function validateRequest(schema) {
return (req, res, next) => {
try {
schema.parse(req.body);
next();
} catch (error) {
res.status(400).json({
error: '驗證失敗',
details: error.errors
});
}
};
}
// 使用驗證
app.post('/api/users',
validateRequest(createUserSchema),
async (req, res) => {
// 此時輸入已驗證
const { email, password, name, age } = req.body;
// 雜湊密碼
const passwordHash = await bcrypt.hash(password, 10);
// 建立使用者
const user = await prisma.user.create({
data: {
email,
passwordHash,
name,
age
}
});
// 不要回傳密碼雜湊
const { passwordHash: _, ...userWithoutPassword } = user;
res.status(201).json(userWithoutPassword);
}
);
\`\`\`
#### 4. 清理輸出以防止 XSS
\`\`\`javascript
const DOMPurify = require('isomorphic-dompurify');
app.post('/api/comments', authenticateToken, async (req, res) => {
const { content } = req.body;
// 驗證
if (!content || content.length > 1000) {
return res.status(400).json({
error: '留言內容無效'
});
}
// 清理 HTML 以防止 XSS
const sanitizedContent = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
const comment = await prisma.comment.create({
data: {
content: sanitizedContent,
userId: req.user.userId
}
});
res.status(201).json(comment);
});
\`\`\`
### 驗證檢查清單
- [ ] 驗證所有使用者輸入
- [ ] 使用參數化查詢或 ORM
- [ ] 驗證資料型別(字串、數字、電子郵件等)
- [ ] 驗證資料範圍(最小/最大長度、數值範圍)
- [ ] 清理 HTML 內容
- [ ] 跳脫特殊字元
- [ ] 驗證檔案上傳(型別、大小、內容)
- [ ] 使用允許清單,而非封鎖清單
範例 3:速率限制與 DDoS 防護
## 實作速率限制
### 為什麼需要速率限制?
- 防止暴力破解攻擊
- 防範 DDoS
- 防止 API 濫用
- 確保公平使用
- 降低伺服器成本
### 使用 Express Rate Limit 實作
\`\`\`javascript
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');
// 建立 Redis 客戶端
const redis = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
});
// 通用 API 速率限制
const apiLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 'rl:api:'
}),
windowMs: 15 * 60 * 1000, // 15 分鐘
max: 100, // 每個時間視窗 100 次請求
message: {
error: '請求次數過多,請稍後再試',
retryAfter: 900 // 秒
},
standardHeaders: true, // 在標頭中回傳速率限制資訊
legacyHeaders: false,
// 自訂金鑰產生器(依使用者 ID 或 IP)
keyGenerator: (req) => {
return req.user?.userId || req.ip;
}
});
// 身分驗證端點的嚴格速率限制
const authLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 'rl:auth:'
}),
windowMs: 15 * 60 * 1000, // 15 分鐘
max: 5, // 每 15 分鐘僅允許 5 次登入嘗試
skipSuccessfulRequests: true, // 成功登入不計入
message: {
error: '登入嘗試次數過多,請稍後再試',
retryAfter: 900
}
});
// 套用速率限制器
app.use('/api/', apiLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
// 昂貴操作的客製速率限制器
const expensiveLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 小時
max: 10, // 每小時 10 次請求
message: {
error: '此操作已超出速率限制'
}
});
app.post('/api/reports/generate',
authenticateToken,
expensiveLimiter,
async (req, res) => {
// 昂貴操作
}
);
\`\`\`
### 進階:依使用者分級的速率限制
\`\`\`javascript
// 根據使用者等級設定不同限制
function createTieredRateLimiter() {
const limits = {
free: { windowMs: 60 * 60 * 1000, max: 100 },
pro: { windowMs: 60 * 60 * 1000, max: 1000 },
enterprise: { windowMs: 60 * 60 * 1000, max: 10000 }
};
return async (req, res, next) => {
const user = req.user;
const tier = user?.tier || 'free';
const limit = limits[tier];
const key = \`rl:user:\${user.userId}\`;
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, limit.windowMs / 1000);
}
if (current > limit.max) {
return res.status(429).json({
error: '超出速率限制',
limit: limit.max,
remaining: 0,
reset: await redis.ttl(key)
});
}
// 設定速率限制標頭
res.set({
'X-RateLimit-Limit': limit.max,
'X-RateLimit-Remaining': limit.max - current,
'X-RateLimit-Reset': await redis.ttl(key)
});
next();
};
}
app.use('/api/', authenticateToken, createTieredRateLimiter());
\`\`\`
### 使用 Helmet 進行 DDoS 防護
\`\`\`javascript
const helmet = require('helmet');
app.use(helmet({
// 內容安全政策
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:']
}
},
// 防止點擊劫持
frameguard: { action: 'deny' },
// 隱藏 X-Powered-By 標頭
hidePoweredBy: true,
// 防止 MIME 類型嗅探
noSniff: true,
// 啟用 HSTS
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
\`\`\`
### 速率限制回應標頭
\`\`\`
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1640000000
Retry-After: 900
\`\`\`
最佳實務
✅ 應該這樣做
- 全面使用 HTTPS - 絕不透過 HTTP 傳送敏感資料
- 實作身分驗證 - 對受保護的端點要求身分驗證
- 驗證所有輸入 - 絕不相信使用者輸入
- 使用參數化查詢 - 防止 SQL 注入
- 實作速率限制 - 防範暴力破解與 DDoS
- 雜湊密碼 - 使用 bcrypt,鹽輪次 >= 10
- 使用短效 Token - JWT 存取 Token 應快速過期
- 正確實作 CORS - 僅允許信任的來源
- 記錄安全事件 - 監控可疑活動
- 保持相依套件更新 - 定期更新套件
- 使用安全標頭 - 實作 Helmet.js
- 清理錯誤訊息 - 不要洩漏敏感資訊
❌ 不要這樣做
- 不要以明文儲存密碼 - 務必雜湊密碼
- 不要使用弱密鑰 - 使用強大、隨機的 JWT 密鑰
- 不要信任使用者輸入 - 務必驗證與清理
- 不要暴露堆疊追蹤 - 在生產環境隱藏錯誤細節
- 不要使用字串串接來組成 SQL - 使用參數化查詢
- 不要在 JWT 中儲存敏感資料 - JWT 未加密
- 不要忽略安全更新 - 定期更新相依套件
- 不要使用預設憑證 - 變更所有預設密碼
- 不要完全停用 CORS - 應正確設定
- 不要記錄敏感資料 - 清理日誌
常見陷阱
問題:JWT 密鑰暴露在程式碼中
症狀: JWT 密鑰寫死或提交到 Git
解決方案:
```javascript
// ❌ 不好
const tokenSigningKey = '[已刪除的弱值]';
// ✅ 好
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
throw new Error('需要設定環境變數 JWT_SECRET');
}
// 產生強密鑰
// node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
```
問題:密碼要求過弱
症狀: 使用者可以設定弱密碼,例如 "password123"
解決方案:
```javascript
const passwordSchema = z.string()
.min(12, '密碼長度至少 12 個字元')
.regex(/[A-Z]/, '必須包含大寫字母')
.regex(/[a-z]/, '必須包含小寫字母')
.regex(/[0-9]/, '必須包含數字')
.regex(/[^A-Za-z0-9]/, '必須包含特殊字元');
// 或使用密碼強度函式庫
const zxcvbn = require('zxcvbn');
const result = zxcvbn(password);
if (result.score < 3) {
return res.status(400).json({
error: '密碼太弱',
suggestions: result.feedback.suggestions
});
}
```
問題:缺少授權檢查
症狀: 使用者可以存取不該存取的資源
解決方案:
```javascript
// ❌ 不好:僅檢查身分驗證
app.delete('/api/posts/:id', authenticateToken, async (req, res) => {
await prisma.post.delete({ where: { id: req.params.id } });
res.json({ success: true });
});
// ✅ 好:同時檢查身分驗證與授權
app.delete('/api/posts/:id', authenticateToken, async (req, res) => {
const post = await prisma.post.findUnique({
where: { id: req.params.id }
});
if (!post) {
return res.status(404).json({ error: '找不到文章' });
}
// 檢查使用者是否為文章擁有者或管理員
if (post.userId !== req.user.userId && req.user.role !== 'admin') {
return res.status(403).json({
error: '無權限刪除此文章'
});
}
await prisma.post.delete({ where: { id: req.params.id } });
res.json({ success: true });
});
```
問題:錯誤訊息過於詳細
症狀: 錯誤訊息揭露系統細節
解決方案:
```javascript
// ❌ 不好:暴露資料庫細節
app.post('/api/users', async (req, res) => {
try {
const user = await prisma.user.create({ data: req.body });
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
// 錯誤:"Unique constraint failed on the fields: (email)"
}
});
// ✅ 好:通用錯誤訊息
app.post('/api/users', async (req, res) => {
try {
const user = await prisma.user.create({ data: req.body });
res.json(user);
} catch (error) {
console.error('建立使用者錯誤:', error); // 記錄完整錯誤
if (error.code === 'P2002') {
return res.status(400).json({
error: '電子郵件已存在'
});
}
res.status(500).json({
error: '建立使用者時發生錯誤'
});
}
});
```
安全檢查清單
身分驗證與授權
- [ ] 實作強身分驗證(JWT、OAuth 2.0)
- [ ] 所有端點使用 HTTPS
- [ ] 使用 bcrypt 雜湊密碼(鹽輪次 >= 10)
- [ ] 實作 Token 過期機制
- [ ] 加入 Refresh Token 機制
- [ ] 驗證每個請求的使用者授權
- [ ] 實作角色型存取控制(RBAC)
輸入驗證
- [ ] 驗證所有使用者輸入
- [ ] 使用參數化查詢或 ORM
- [ ] 清理 HTML 內容
- [ ] 驗證檔案上傳
- [ ] 實作請求結構驗證
- [ ] 使用允許清單,而非封鎖清單
速率限制與 DDoS 防護
- [ ] 對每個使用者/IP 實作速率限制
- [ ] 對身分驗證端點加入更嚴格的限制
- [ ] 使用 Redis 進行分散式速率限制
- [ ] 回傳適當的速率限制標頭
- [ ] 實作請求節流
資料保護
- [ ] 所有流量使用 HTTPS/TLS
- [ ] 靜態敏感資料加密
- [ ] 不要在 JWT 中儲存敏感資料
- [ ] 清理錯誤訊息
- [ ] 實作正確的 CORS 設定
- [ ] 使用安全標頭(Helmet.js)
監控與記錄
- [ ] 記錄安全事件
- [ ] 監控可疑活動
- [ ] 設定失敗驗證嘗試的警示
- [ ] 追蹤 API 使用模式
- [ ] 不要記錄敏感資料
OWASP API 安全十大風險
- 物件層級授權失效 - 始終驗證使用者能否存取資源
- 身分驗證失效 - 實作強身分驗證機制
- 物件屬性層級授權失效 - 驗證使用者可存取哪些屬性
- 資源消耗無限制 - 實作速率限制與配額
- 功能層級授權失效 - 驗證每個功能的使用者角色
- 敏感商業流程的無限制存取 - 保護關鍵工作流程
- 伺服器端請求偽造(SSRF) - 驗證並清理 URL
- 安全設定錯誤 - 使用安全最佳實務與標頭
- 庫存管理不當 - 記錄並保護所有 API 端點
- 不安全地使用 API - 驗證來自第三方 API 的資料
相關技能
@ethical-hacking-methodology- 安全測試觀點@sql-injection-testing- SQL 注入測試@xss-html-injection- XSS 弱點測試@broken-authentication- 身分驗證弱點@backend-dev-guidelines- 後端開發標準@systematic-debugging- 除錯安全問題
其他資源
- OWASP API Security Top 10
- JWT Best Practices
- Express Security Best Practices
- Node.js Security Checklist
- API Security Checklist
專業提示: 安全不是一次性任務 - 定期稽核您的 API、保持相依套件更新,並隨時了解新的弱點!
限制
- 僅在任務明確符合上述範圍時使用此技能。
- 請勿將輸出視為環境特定驗證、測試或專家審查的替代方案。
- 如果缺少必要的輸入、權限、安全邊界或成功標準,請停止並要求釐清。






