📖 第24课:API安全加固

阶段五:实战项目

安全不是功能,是底线。一个未加固的API可能在几小时内被攻破——注入攻击、身份伪造、数据泄露、DDoS。本课覆盖OWASP API安全Top 10,从输入验证到HTTPS、从CORS到日志审计,构建纵深防御体系。

🛡️ OWASP API安全Top 10

#风险说明防护
1对象级授权失效(BOLA)用户A访问用户B的数据资源级权限检查
2认证失效弱密码、令牌泄露强密码策略+MFA
3对象属性级授权失效更新不该更新的字段白名单字段过滤
4资源消耗无限制无分页、无限制查询限流+分页+超时
5功能级授权失效(BFLA)普通用户调用管理API角色+权限检查
6批量赋值请求体包含不该设置的字段严格输入验证
7安全配置错误CORS过宽、调试信息泄露硬化配置
8注入攻击SQL/NoSQL/命令注入参数化查询+转义
9资产管理不当暴露调试端点、旧版本资产盘点+下线
10日志监控不足攻击无痕迹完整日志+告警

🛠️ 实战:安全加固API

// security-hardened-api.js - 安全加固的API
const express = require('express');
const crypto = require('crypto');
const helmet = require('helmet');
const app = express();

// ===== 1. HTTP安全头(Helmet)=====
app.use(helmet());
// 等效于手动设置:
// X-Content-Type-Options: nosniff
// X-Frame-Options: DENY
// X-XSS-Protection: 0
// Strict-Transport-Security: max-age=31536000
// Content-Security-Policy: default-src 'self'

// ===== 2. CORS配置 =====
const cors = require('cors');
app.use(cors({
  origin: (origin, callback) => {
    const allowed = ['https://app.example.com', 'https://admin.example.com'];
    if (!origin || allowed.includes(origin)) callback(null, true);
    else callback(new Error('CORS不允许'));
  },
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
  exposedHeaders: ['X-Request-ID', 'X-RateLimit-*'],
  credentials: true,
  maxAge: 86400,
}));

// ===== 3. 请求体大小限制 =====
app.use(express.json({ limit: '100kb' }));  // 防止大负载攻击
app.use(express.urlencoded({ extended: false, limit: '100kb' }));

// ===== 4. 请求ID =====
app.use((req, res, next) => {
  req.requestId = req.headers['x-request-id'] || crypto.randomUUID();
  res.setHeader('X-Request-ID', req.requestId);
  next();
});

// ===== 5. 输入验证 =====
class InputValidator {
  static sanitizeString(str) {
    if (typeof str !== 'string') return str;
    return str
      .replace(/[<>]/g, '')           // 移除尖括号(防XSS)
      .replace(/&/g, '&')          // 转义HTML实体
      .trim();
  }

  static validateEmail(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }

  static validateObjectId(id) {
    return /^\d+$/.test(String(id));  // 只允许数字ID
  }

  // 防止批量赋值:白名单过滤
  static filterFields(data, allowedFields) {
    const filtered = {};
    for (const field of allowedFields) {
      if (data[field] !== undefined) filtered[field] = data[field];
    }
    return filtered;
  }

  // 防止原型污染
  static preventPrototypePollution(data) {
    const dangerous = ['__proto__', 'constructor', 'prototype'];
    for (const key of Object.keys(data)) {
      if (dangerous.includes(key)) delete data[key];
      if (typeof data[key] === 'object' && data[key] !== null) {
        InputValidator.preventPrototypePollution(data[key]);
      }
    }
    return data;
  }
}

// ===== 6. 认证中间件(增强版)=====
const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(32).toString('hex');

function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ success: false, error: { code: 'AUTH_MISSING' } });
  }

  const token = authHeader.substring(7);

  // 检查令牌黑名单
  if (tokenBlacklist.has(token)) {
    return res.status(401).json({ success: false, error: { code: 'TOKEN_REVOKED' } });
  }

  try {
    const payload = verifyJWT(token);
    if (payload.exp < Date.now() / 1000) {
      return res.status(401).json({ success: false, error: { code: 'TOKEN_EXPIRED' } });
    }
    req.user = payload;
    next();
  } catch {
    return res.status(401).json({ success: false, error: { code: 'TOKEN_INVALID' } });
  }
}

function verifyJWT(token) {
  const parts = token.split('.');
  if (parts.length !== 3) throw new Error('Invalid token');
  const [headerB64, payloadB64, signatureB64] = parts;
  const expectedSig = crypto.createHmac('sha256', JWT_SECRET)
    .update(`${headerB64}.${payloadB64}`).digest('base64url');
  if (signatureB64 !== expectedSig) throw new Error('Invalid signature');
  return JSON.parse(Buffer.from(payloadB64, 'base64').toString());
}

const tokenBlacklist = new Set();

// ===== 7. 授权中间件 =====
function authorize(resource, action) {
  return (req, res, next) => {
    if (!req.user) return res.status(401).json({ success: false, error: { code: 'AUTH_REQUIRED' } });

    const role = req.user.role;
    const permissions = {
      admin:   { users: ['read','write','delete'], orders: ['read','write','delete'], products: ['read','write','delete'] },
      editor:  { users: ['read'], orders: ['read','write'], products: ['read','write'] },
      viewer:  { users: ['read'], orders: ['read'], products: ['read'] },
    };

    const userPerms = permissions[role]?.[resource] || [];
    if (!userPerms.includes(action)) {
      // 记录未授权访问
      securityLog('UNAUTHORIZED_ACCESS', { userId: req.user.sub, resource, action, ip: req.ip });
      return res.status(403).json({ success: false, error: { code: 'FORBIDDEN', message: `需要 ${resource}:${action} 权限` } });
    }
    next();
  };
}

// ===== 8. 资源级授权(防BOLA)=====
function checkResourceOwnership(resourceType) {
  return async (req, res, next) => {
    const resourceId = req.params.id;
    const userId = req.user.sub;

    // 管理员可访问所有资源
    if (req.user.role === 'admin') return next();

    // 普通用户只能访问自己的资源
    const resource = dataStore.get(resourceType, resourceId);
    if (!resource) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
    if (resource.userId !== userId && resource.ownerId !== userId) {
      securityLog('BOLA_ATTEMPT', { userId, resourceType, resourceId, ip: req.ip });
      return res.status(403).json({ success: false, error: { code: 'FORBIDDEN' } });
    }
    next();
  };
}

// ===== 9. 安全日志 =====
const securityLogs = [];
function securityLog(event, details) {
  const entry = {
    event,
    timestamp: new Date().toISOString(),
    ...details,
  };
  securityLogs.push(entry);
  console.warn(`⚠️ 安全事件: ${event}`, details);
}

// 请求审计日志
app.use((req, res, next) => {
  if (req.user) {
    securityLog('API_ACCESS', {
      userId: req.user.sub,
      method: req.method,
      path: req.path,
      ip: req.ip,
      userAgent: req.headers['user-agent'],
    });
  }
  next();
});

// ===== 10. 限流(防暴力破解)=====
const loginAttempts = new Map();
function loginRateLimit(req, res, next) {
  const ip = req.ip;
  const attempts = loginAttempts.get(ip) || { count: 0, lastAttempt: 0 };

  if (attempts.count >= 5 && Date.now() - attempts.lastAttempt < 300000) {
    securityLog('BRUTE_FORCE_BLOCKED', { ip, attempts: attempts.count });
    return res.status(429).json({ success: false, error: { code: 'TOO_MANY_ATTEMPTS', message: '登录尝试过多,请5分钟后重试' } });
  }

  next();
}

// ===== 路由 =====

const dataStore = {
  users: new Map([
    ['1', { id: '1', name: '张三', email: 'zhang@example.com', role: 'admin', password: crypto.createHash('sha256').update('admin123+salt').digest('hex') }],
    ['2', { id: '2', name: '李四', email: 'li@example.com', role: 'viewer', password: crypto.createHash('sha256').update('viewer123+salt').digest('hex') }],
  ]),
  get(type, id) { return this[type]?.get(id); },
};

// 登录
app.post('/api/auth/login', loginRateLimit, (req, res) => {
  const { email, password } = InputValidator.filterFields(req.body, ['email', 'password']);
  const ip = req.ip;

  if (!email || !password) {
    return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR' } });
  }

  const user = [...dataStore.users.values()].find(u => u.email === email);
  const hashedPassword = crypto.createHash('sha256').update(password + '+salt').digest('hex');

  if (!user || user.password !== hashedPassword) {
    const attempts = loginAttempts.get(ip) || { count: 0, lastAttempt: 0 };
    attempts.count++;
    attempts.lastAttempt = Date.now();
    loginAttempts.set(ip, attempts);
    securityLog('LOGIN_FAILED', { email, ip, attempts: attempts.count });
    return res.status(401).json({ success: false, error: { code: 'INVALID_CREDENTIALS' } });
  }

  // 重置尝试计数
  loginAttempts.delete(ip);

  const token = createJWT({ sub: user.id, role: user.role, exp: Math.floor(Date.now() / 1000) + 900 });
  securityLog('LOGIN_SUCCESS', { userId: user.id, ip });
  res.json({ success: true, data: { token, expiresIn: 900 } });
});

function createJWT(payload) {
  const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
  const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
  const sig = crypto.createHmac('sha256', JWT_SECRET).update(`${header}.${body}`).digest('base64url');
  return `${header}.${body}.${sig}`;
}

// 用户列表(需要授权)
app.get('/api/users', authenticate, authorize('users', 'read'), (req, res) => {
  const users = [...dataStore.users.values()].map(u => {
    const { password, ...safe } = u;  // 永远不返回密码
    return safe;
  });
  res.json({ success: true, data: users });
});

// 用户详情(防BOLA)
app.get('/api/users/:id', authenticate, checkResourceOwnership('users'), (req, res) => {
  const user = dataStore.users.get(req.params.id);
  if (!user) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
  const { password, ...safe } = user;
  res.json({ success: true, data: safe });
});

// 更新用户(防批量赋值)
app.patch('/api/users/:id', authenticate, checkResourceOwnership('users'), (req, res) => {
  const allowedFields = ['name', 'email'];  // 白名单!不能改role/password
  const safeData = InputValidator.filterFields(req.body, allowedFields);
  InputValidator.preventPrototypePollution(safeData);

  const user = dataStore.users.get(req.params.id);
  if (!user) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });

  Object.assign(user, safeData);
  const { password, ...safe } = user;
  res.json({ success: true, data: safe });
});

// 安全审计日志
app.get('/api/admin/security-logs', authenticate, authorize('users', 'delete'), (req, res) => {
  res.json({ success: true, data: securityLogs.slice(-100) });
});

app.listen(3000, () => console.log('🛡️ 安全加固API运行在 http://localhost:3000'));

测试安全功能

# 安装依赖
npm install express helmet cors

# 启动
node security-hardened-api.js

# 1. 登录获取令牌
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"zhang@example.com","password":"admin123"}' | jq -r '.data.token')

# 2. 用令牌访问受保护资源
curl -s http://localhost:3000/api/users \
  -H "Authorization: Bearer $TOKEN" | jq .

# 3. 尝试批量赋值(改role字段)
curl -s -X PATCH http://localhost:3000/api/users/2 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"李四","role":"admin"}' | jq .
# → role字段被过滤,不会变更

# 4. 暴力破解保护(5次失败后锁定)
for i in $(seq 1 6); do
  curl -s -X POST http://localhost:3000/api/auth/login \
    -H "Content-Type: application/json" \
    -d '{"email":"zhang@example.com","password":"wrong"}' | jq -c '.error.code'
done
"INVALID_CREDENTIALS"
"INVALID_CREDENTIALS"
"INVALID_CREDENTIALS"
"INVALID_CREDENTIALS"
"INVALID_CREDENTIALS"
"TOO_MANY_ATTEMPTS"

📝 本课小结

  1. OWASP API安全Top 10是安全检查清单
  2. Helmet自动设置安全HTTP头
  3. CORS严格限制允许的来源
  4. 白名单过滤防止批量赋值
  5. 资源级授权(BOLA)检查:用户只能访问自己的数据
  6. 原型污染防护:过滤__proto__等危险键
  7. 安全日志记录所有敏感操作
  8. 暴力破解防护:登录失败次数限制
  9. 永远不要在响应中返回密码

💪 练习

练习1:实现HTTPS

使用Let's Encrypt证书配置HTTPS,设置HSTS头。

练习2:安全审计脚本

编写脚本自动检测API的常见安全问题:无认证端点、CORS配置、敏感数据泄露等。

🏆 本课成就:安全守卫