api-security-best-practices

api-security-best-practices

热门

实现安全的API设计模式,包括身份验证、授权、输入验证、速率限制以及防范常见API漏洞

4.3万Star
6856Fork
更新于 2026/7/14
SKILL.md
readonly只读
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密钥)
  • 实现基于令牌的身份验证
  • 设置基于角色的访问控制(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令牌
4. 客户端安全存储令牌
5. 客户端每次请求发送令牌
6. 服务器验证令牌

### 实现

#### 1. 生成安全的JWT令牌

\`\`\`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令牌
    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'
      }
    );
    
    // 生成刷新令牌
    const refreshToken = jwt.sign(
      { userId: user.id },
      process.env.JWT_REFRESH_SECRET,
      { expiresIn: '7d' }
    );
    
    // 将刷新令牌存储在数据库中
    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令牌(中间件)

\`\`\`javascript
// middleware/auth.js
const jwt = require('jsonwebtoken');

function authenticateToken(req, res, next) {
  // 从头部获取令牌
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
  
  if (!token) {
    return res.status(401).json({ 
      error: '需要访问令牌' 
    });
  }
  
  // 验证令牌
  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: '令牌已过期' 
          });
        }
        return res.status(403).json({ 
          error: '无效的令牌' 
        });
      }
      
      // 将用户附加到请求
      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. 实现令牌刷新

\`\`\`javascript
app.post('/api/auth/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(401).json({ 
      error: '需要刷新令牌' 
    });
  }
  
  try {
    // 验证刷新令牌
    const decoded = jwt.verify(
      refreshToken, 
      process.env.JWT_REFRESH_SECRET
    );
    
    // 检查刷新令牌是否存在于数据库中
    const storedToken = await db.refreshToken.findFirst({
      where: {
        token: refreshToken,
        userId: decoded.userId,
        expiresAt: { gt: new Date() }
      }
    });
    
    if (!storedToken) {
      return res.status(403).json({ 
        error: '无效的刷新令牌' 
      });
    }
    
    // 生成新的访问令牌
    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: '无效的刷新令牌' 
    });
  }
});
\`\`\`

### 安全最佳实践

- ✅ 使用强JWT密钥(至少256位)
- ✅ 设置较短的过期时间(访问令牌1小时)
- ✅ 实现刷新令牌以支持长期会话
- ✅ 将刷新令牌存储在数据库中(可撤销)
- ✅ 仅使用HTTPS
- ✅ 不要在JWT负载中存储敏感数据
- ✅ 验证令牌颁发者和受众
- ✅ 实现令牌黑名单以支持登出

示例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
  • 使用短生命周期令牌 - JWT访问令牌应快速过期
  • 正确实现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)
  • [ ] 实现令牌过期
  • [ ] 添加刷新令牌机制
  • [ ] 验证每个请求的用户授权
  • [ ] 实现基于角色的访问控制(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,保持依赖更新,并随时了解新漏洞!

限制

  • 仅当任务明确匹配上述范围时使用此技能。
  • 不要将输出视为环境特定验证、测试或专家审查的替代品。
  • 如果缺少所需的输入、权限、安全边界或成功标准,请停止并请求澄清。