API安全最佳实践Skill api-security-best-practices

这个技能用于实现安全的API设计模式,包括身份认证、授权、输入验证、速率限制和防护常见API漏洞。适用于REST、GraphQL和WebSocket API的安全实施,帮助开发者构建和保护API接口,防止攻击如SQL注入、DDoS等。关键词:API安全、身份认证、授权、输入验证、速率限制、OWASP、JWT、OAuth、网络安全、后端开发。

安全审计 0 次安装 0 次浏览 更新于 3/22/2026

name: api-security-best-practices description: “实现安全的API设计模式,包括身份认证、授权、输入验证、速率限制和防护常见API漏洞” license: MIT metadata: version: “1.0.0” domain: architecture triggers: API安全, 身份认证, 授权, 输入验证, 速率限制, OWASP, JWT, OAuth role: 专家 scope: 实施 output-format: 代码 related-skills: 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令牌(中间件)

// 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. 保护路由

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. 实现令牌刷新

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注入防护

```markdown
## 防止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. 使用参数化查询

// ✅ 安全:参数化查询
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

// ✅ 安全:使用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实现请求验证

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

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防护

```markdown
## 实现速率限制

### 为什么需要速率限制?

- 防止暴力攻击
- 防护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) => {
    // 昂贵操作
  }
);

高级:按用户速率限制

// 基于用户层级的限制
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());

DDoS防护与Helmet

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 JWT_SECRET = 'my-secret-key';

// ✅ 正确
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” 解决方案:

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
  });
}

问题:缺少授权检查

症状: 用户可以访问不应访问的资源 解决方案:

// ❌ 错误:仅检查身份认证
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 });
});

问题:详细错误消息

症状: 错误消息泄露系统详情 解决方案:

// ❌ 错误:暴露数据库详情
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 });
    // 错误:“唯一约束失败于字段:(`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安全Top 10

  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,保持依赖更新,并了解新漏洞!