api-security-best-practices

api-security-best-practices

熱門

實作安全的 API 設計模式,包括身分驗證、授權、輸入驗證、速率限制,以及防範常見的 API 弱點

4.3萬星標
6856分支
更新於 2026/7/14
SKILL.md
readonlyread-only
name
api-security-best-practices
description

實作安全的 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 安全十大風險

  1. 物件層級授權失效 - 始終驗證使用者能否存取資源
  2. 身分驗證失效 - 實作強身分驗證機制
  3. 物件屬性層級授權失效 - 驗證使用者可存取哪些屬性
  4. 資源消耗無限制 - 實作速率限制與配額
  5. 功能層級授權失效 - 驗證每個功能的使用者角色
  6. 敏感商業流程的無限制存取 - 保護關鍵工作流程
  7. 伺服器端請求偽造(SSRF) - 驗證並清理 URL
  8. 安全設定錯誤 - 使用安全最佳實務與標頭
  9. 庫存管理不當 - 記錄並保護所有 API 端點
  10. 不安全地使用 API - 驗證來自第三方 API 的資料

相關技能

  • @ethical-hacking-methodology - 安全測試觀點
  • @sql-injection-testing - SQL 注入測試
  • @xss-html-injection - XSS 弱點測試
  • @broken-authentication - 身分驗證弱點
  • @backend-dev-guidelines - 後端開發標準
  • @systematic-debugging - 除錯安全問題

其他資源


專業提示: 安全不是一次性任務 - 定期稽核您的 API、保持相依套件更新,並隨時了解新的弱點!

限制

  • 僅在任務明確符合上述範圍時使用此技能。
  • 請勿將輸出視為環境特定驗證、測試或專家審查的替代方案。
  • 如果缺少必要的輸入、權限、安全邊界或成功標準,請停止並要求釐清。