API安全最佳实践 2026

API安全最佳实践 2026

REST API和GraphQL API的安全防护完全指南。认证、授权、速率限制等。

2026年3月16日8分钟阅读

前言:为什么API安全至关重要

在2026年,API已经成为现代软件架构的核心组成部分。从移动应用到微服务通信,从第三方集成到物联网设备,API无处不在。然而,API也是攻击者最常瞄准的目标之一。OWASP API Security Top 10每年都在更新,新的攻击手法不断出现。

一个安全漏洞可能导致数据泄露、服务中断、经济损失甚至法律诉讼。本指南将全面覆盖REST API和GraphQL API的安全防护策略,帮助你构建坚如磐石的API安全体系。

一、认证(Authentication)

认证回答的是"你是谁"的问题。选择正确的认证方案是API安全的第一步。

1.1 JWT(JSON Web Token)最佳实践

JWT是目前最流行的API认证方案之一,但错误的使用方式会带来严重的安全风险:

// Node.js中的JWT实现

import jwt from 'jsonwebtoken';

// 生成Token
function generateTokens(user) {
  // Access Token - 短期有效
  const accessToken = jwt.sign(
    {
      sub: user.id,
      role: user.role,
      type: 'access',
    },
    process.env.JWT_ACCESS_SECRET,
    {
      expiresIn: '15m',       // 15分钟过期
      issuer: 'your-app.com',
      audience: 'your-app-api',
      algorithm: 'RS256',     // 使用非对称算法
    }
  );

  // Refresh Token - 长期有效
  const refreshToken = jwt.sign(
    {
      sub: user.id,
      type: 'refresh',
      jti: generateUniqueId(), // 唯一标识符,用于黑名单
    },
    process.env.JWT_REFRESH_SECRET,
    {
      expiresIn: '7d',
      issuer: 'your-app.com',
    }
  );

  return { accessToken, refreshToken };
}

// 验证Token的中间件
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: '未提供认证令牌' });
  }

  try {
    const payload = jwt.verify(token, process.env.JWT_ACCESS_SECRET, {
      algorithms: ['RS256'],  // 严格限制算法
      issuer: 'your-app.com',
      audience: 'your-app-api',
    });
    req.user = payload;
    next();
  } catch (error) {
    if (error instanceof jwt.TokenExpiredError) {
      return res.status(401).json({ error: '令牌已过期' });
    }
    return res.status(403).json({ error: '无效的令牌' });
  }
}

JWT安全要点

  • 使用 RS256(非对称)而非 HS256(对称),防止密钥泄露
  • Access Token有效期控制在15分钟以内
  • 永远不要在JWT中存储敏感信息(密码、信用卡号等)
  • 实现Token黑名单机制以支持强制登出
  • 验证时严格指定允许的算法,防止算法切换攻击

1.2 OAuth 2.0与PKCE

对于需要第三方授权的场景,OAuth 2.0 + PKCE是推荐方案:

// PKCE流程 - 适用于SPA和移动应用
import crypto from 'crypto';

// 1. 生成code verifier和code challenge
function generatePKCE() {
  const codeVerifier = crypto.randomBytes(32)
    .toString('base64url');

  const codeChallenge = crypto
    .createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');

  return { codeVerifier, codeChallenge };
}

// 2. 构建授权URL
function buildAuthorizationUrl(codeChallenge) {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: process.env.CLIENT_ID,
    redirect_uri: 'https://your-app.com/callback',
    scope: 'openid profile email',
    state: crypto.randomBytes(16).toString('hex'),
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
  });

  return `https://auth-server.com/authorize?${params}`;
}

// 3. 用授权码+code verifier换取Token
async function exchangeCode(code, codeVerifier) {
  const response = await fetch('https://auth-server.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: 'https://your-app.com/callback',
      client_id: process.env.CLIENT_ID,
      code_verifier: codeVerifier,
    }),
  });

  return response.json();
}

1.3 API Key管理

对于服务器之间的通信,API Key是一种简单有效的认证方式:

// API Key验证中间件
function validateApiKey(req, res, next) {
  const apiKey = req.headers['x-api-key'];

  if (!apiKey) {
    return res.status(401).json({ error: '缺少API Key' });
  }

  // 使用时序安全的比较防止时序攻击
  const validKey = process.env.API_KEY;
  if (!crypto.timingSafeEqual(
    Buffer.from(apiKey),
    Buffer.from(validKey)
  )) {
    return res.status(403).json({ error: '无效的API Key' });
  }

  next();
}

API Key安全要点

  • 永远不要在客户端代码中暴露API Key
  • 为每个客户端/用途分配独立的Key
  • 实现Key轮换机制
  • 记录所有API Key的使用日志

二、授权(Authorization)

授权回答的是"你能做什么"的问题。

2.1 基于角色的访问控制(RBAC)

// 定义角色和权限
enum Permission {
  READ_USERS = 'read:users',
  WRITE_USERS = 'write:users',
  DELETE_USERS = 'delete:users',
  READ_POSTS = 'read:posts',
  WRITE_POSTS = 'write:posts',
  DELETE_POSTS = 'delete:posts',
  MANAGE_SETTINGS = 'manage:settings',
}

const rolePermissions: Record<string, Permission[]> = {
  admin: [
    Permission.READ_USERS,
    Permission.WRITE_USERS,
    Permission.DELETE_USERS,
    Permission.READ_POSTS,
    Permission.WRITE_POSTS,
    Permission.DELETE_POSTS,
    Permission.MANAGE_SETTINGS,
  ],
  editor: [
    Permission.READ_USERS,
    Permission.READ_POSTS,
    Permission.WRITE_POSTS,
    Permission.DELETE_POSTS,
  ],
  viewer: [
    Permission.READ_USERS,
    Permission.READ_POSTS,
  ],
};

// 授权中间件
function requirePermission(...permissions: Permission[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    const userRole = req.user?.role;
    if (!userRole) {
      return res.status(401).json({ error: '未认证' });
    }

    const userPermissions = rolePermissions[userRole] || [];
    const hasPermission = permissions.every(p =>
      userPermissions.includes(p)
    );

    if (!hasPermission) {
      return res.status(403).json({ error: '权限不足' });
    }

    next();
  };
}

// 使用
app.delete(
  '/api/users/:id',
  authenticateToken,
  requirePermission(Permission.DELETE_USERS),
  deleteUserHandler
);

2.2 基于属性的访问控制(ABAC)

对于更复杂的授权需求,ABAC提供更细粒度的控制:

// ABAC策略引擎
interface AccessContext {
  user: { id: string; role: string; department: string };
  resource: { type: string; ownerId: string; status: string };
  action: string;
  environment: { time: Date; ipAddress: string };
}

function evaluatePolicy(context: AccessContext): boolean {
  const { user, resource, action } = context;

  // 规则1:管理员可以执行任何操作
  if (user.role === 'admin') return true;

  // 规则2:用户只能修改自己的资源
  if (action === 'update' && resource.ownerId !== user.id) {
    return false;
  }

  // 规则3:已发布的资源只有编辑和管理员可以修改
  if (resource.status === 'published' && action === 'update') {
    return ['admin', 'editor'].includes(user.role);
  }

  // 规则4:删除操作需要同部门
  if (action === 'delete') {
    return user.department === resource.department;
  }

  return true;
}

三、输入验证与数据清理

输入验证是防御注入攻击的第一道防线。

3.1 请求验证

import { z } from 'zod';

// 使用Zod定义严格的输入Schema
const createUserSchema = z.object({
  name: z.string()
    .min(2, '名称至少2个字符')
    .max(50, '名称不超过50个字符')
    .trim(),
  email: z.string()
    .email('无效的邮箱格式')
    .toLowerCase(),
  password: z.string()
    .min(12, '密码至少12个字符')
    .regex(/[A-Z]/, '需要包含大写字母')
    .regex(/[a-z]/, '需要包含小写字母')
    .regex(/[0-9]/, '需要包含数字')
    .regex(/[^A-Za-z0-9]/, '需要包含特殊字符'),
  age: z.number()
    .int()
    .min(13, '年龄不能小于13')
    .max(150, '年龄不能大于150'),
  role: z.enum(['viewer', 'editor']),  // 不允许通过API创建admin
});

// 验证中间件
function validateBody(schema: z.ZodSchema) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({
        error: '输入验证失败',
        details: result.error.issues.map(issue => ({
          field: issue.path.join('.'),
          message: issue.message,
        })),
      });
    }
    req.body = result.data;  // 使用验证后的数据
    next();
  };
}

app.post('/api/users', validateBody(createUserSchema), createUserHandler);

3.2 防止SQL注入

// 差:字符串拼接(极度危险!)
const query = `SELECT * FROM users WHERE email = '${email}'`;

// 好:使用参数化查询
const result = await db.query(
  'SELECT * FROM users WHERE email = $1',
  [email]
);

// 更好:使用ORM
const user = await prisma.user.findUnique({
  where: { email },
});

3.3 防止NoSQL注入

// MongoDB中的NoSQL注入防护

// 差:直接使用用户输入
const user = await User.findOne({ username: req.body.username });
// 攻击者可以发送: { "username": { "$gt": "" } }

// 好:确保输入是字符串类型
const username = String(req.body.username);
const user = await User.findOne({ username });

// 使用express-mongo-sanitize中间件
import mongoSanitize from 'express-mongo-sanitize';
app.use(mongoSanitize());

四、速率限制(Rate Limiting)

4.1 多层速率限制

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

// 全局速率限制
const globalLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
  windowMs: 15 * 60 * 1000,   // 15分钟窗口
  max: 1000,                    // 每窗口1000次请求
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: '请求过于频繁,请稍后再试' },
});

// 登录接口专用限制(更严格)
const loginLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
  windowMs: 15 * 60 * 1000,
  max: 5,                       // 15分钟内最多5次尝试
  skipSuccessfulRequests: true,  // 成功的请求不计入限制
  message: { error: '登录尝试次数过多,请15分钟后再试' },
});

// API密钥级别的限制
const apiKeyLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
  windowMs: 60 * 1000,          // 1分钟窗口
  max: 100,                      // 每分钟100次
  keyGenerator: (req) => req.headers['x-api-key'] || req.ip,
});

app.use('/api/', globalLimiter);
app.use('/api/auth/login', loginLimiter);
app.use('/api/v1/', apiKeyLimiter);

4.2 滑动窗口算法

对于更精确的速率控制,使用滑动窗口算法:

async function slidingWindowRateLimit(
  key: string,
  maxRequests: number,
  windowMs: number
): Promise<{ allowed: boolean; remaining: number; resetAt: number }> {
  const now = Date.now();
  const windowStart = now - windowMs;

  const pipeline = redis.pipeline();
  pipeline.zremrangebyscore(key, 0, windowStart);  // 清理过期记录
  pipeline.zadd(key, now, `${now}-${Math.random()}`);  // 添加当前请求
  pipeline.zcard(key);  // 获取窗口内请求数
  pipeline.pexpire(key, windowMs);  // 设置过期时间

  const results = await pipeline.exec();
  const requestCount = results[2][1] as number;

  return {
    allowed: requestCount <= maxRequests,
    remaining: Math.max(0, maxRequests - requestCount),
    resetAt: now + windowMs,
  };
}

五、安全响应头

import helmet from 'helmet';

// 使用Helmet设置安全响应头
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", 'data:', 'https:'],
      connectSrc: ["'self'", 'https://api.your-app.com'],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
}));

// 手动设置额外的安全头
app.use((req, res, next) => {
  // 防止MIME类型嗅探
  res.setHeader('X-Content-Type-Options', 'nosniff');
  // 防止点击劫持
  res.setHeader('X-Frame-Options', 'DENY');
  // 启用XSS保护
  res.setHeader('X-XSS-Protection', '1; mode=block');
  // 控制Referrer信息
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  // 限制浏览器功能
  res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
  next();
});

六、GraphQL特定安全措施

GraphQL有其独特的安全挑战:

6.1 查询复杂度限制

import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    depthLimit(5),                       // 最大查询深度
    createComplexityLimitRule(1000, {     // 最大查询复杂度
      scalarCost: 1,
      objectCost: 10,
      listFactor: 20,
    }),
  ],
});

6.2 禁用内省(生产环境)

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',
});

6.3 查询白名单(Persisted Queries)

// 只允许预定义的查询
const allowedQueries = {
  'abc123': 'query GetUser($id: ID!) { user(id: $id) { name email } }',
  'def456': 'query GetPosts { posts { title content } }',
};

app.use('/graphql', (req, res, next) => {
  if (process.env.NODE_ENV === 'production') {
    const queryId = req.body.extensions?.persistedQuery?.sha256Hash;
    if (!queryId || !allowedQueries[queryId]) {
      return res.status(400).json({ error: '不允许的查询' });
    }
    req.body.query = allowedQueries[queryId];
  }
  next();
});

七、日志与监控

7.1 安全日志记录

import winston from 'winston';

const securityLogger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json(),
  ),
  transports: [
    new winston.transports.File({ filename: 'security.log' }),
  ],
});

// 记录关键安全事件
function logSecurityEvent(event: {
  type: string;
  userId?: string;
  ip: string;
  details: Record<string, unknown>;
}) {
  securityLogger.warn('Security Event', {
    ...event,
    timestamp: new Date().toISOString(),
  });
}

// 在认证失败时记录
app.post('/api/auth/login', async (req, res) => {
  const result = await authenticate(req.body);

  if (!result.success) {
    logSecurityEvent({
      type: 'AUTH_FAILURE',
      ip: req.ip,
      details: {
        email: req.body.email,
        reason: result.reason,
        userAgent: req.headers['user-agent'],
      },
    });
  }
});

7.2 异常检测

监控以下指标来检测潜在的攻击:

  • 单个IP的异常高请求量
  • 大量的401/403错误
  • 异常的请求模式(如顺序扫描API端点)
  • 请求体中的可疑内容(SQL关键字、XSS载荷)
  • 非常规时间段的API访问

八、CORS配置

import cors from 'cors';

// 严格的CORS配置
const corsOptions = {
  origin: function (origin, callback) {
    const allowedOrigins = [
      'https://your-app.com',
      'https://admin.your-app.com',
    ];

    // 允许没有origin的请求(如移动应用和Postman)
    if (!origin) return callback(null, true);

    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('CORS策略不允许此来源'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
  credentials: true,
  maxAge: 86400,  // 预检请求缓存24小时
};

app.use(cors(corsOptions));

九、API安全检查清单

基础安全

  • 所有API端点都通过HTTPS提供服务
  • 实现了适当的认证机制(JWT/OAuth 2.0)
  • 实现了基于角色的访问控制
  • 所有用户输入都经过验证和清理
  • 使用参数化查询防止SQL注入
  • 配置了CORS策略

中级安全

  • 实现了多层速率限制
  • 设置了安全响应头(使用Helmet)
  • JWT使用非对称算法(RS256)
  • 实现了Token刷新和黑名单机制
  • API Key使用时序安全的比较
  • 记录了完整的安全日志

高级安全

  • 实现了异常检测和告警
  • GraphQL查询有深度和复杂度限制
  • 生产环境禁用了GraphQL内省
  • 实现了请求签名验证
  • 定期进行安全审计和渗透测试
  • 制定了安全事件响应计划

相关工具

在API开发和安全测试中,以下工具可以提供帮助:

结语

API安全不是一次性的工作,而是需要持续关注和改进的过程。攻击手法在不断进化,你的防御也需要与时俱进。从本指南中的基础检查清单开始,逐步建立起完整的API安全体系。

记住安全的核心原则:永不信任用户输入、最小权限原则、纵深防御、假设已被攻破。遵循这些原则,结合本文介绍的具体技术措施,你就能构建出安全可靠的API服务。

相关文章