正在加载,请稍候…

API Security Best Practices: Authentication, Rate Limiting, and Input Validation

Secure your APIs with comprehensive best practices. Learn API key management, OAuth scopes, rate limiting, input validation, and OWASP API Security Top 10.

API Security Best Practices

OWASP API Security Top 10

# Risk Prevention
1 Broken Object Level Auth Validate ownership per request
2 Broken Authentication Strong auth + rate limits
3 Broken Object Property Auth Allowlist response fields
4 Unrestricted Resource Consumption Rate limits + request size limits
5 Function Level Auth RBAC for every endpoint
6 Unrestricted Access to Sensitive Flows Step-up auth for critical actions
7 SSRF Validate/allowlist URLs
8 Security Misconfiguration Disable debug, CORS, headers
9 Improper Inventory Management API versioning + deprecation
10 Unsafe API Consumption Validate third-party responses

API Key Management

import crypto from 'crypto'
import { hash, compare } from 'bcryptjs'

class APIKeyManager {
  // Generate API key with prefix for easy identification
  generate(prefix = 'sk') {
    const key = crypto.randomBytes(32).toString('base64url')
    const apiKey = `${prefix}_${key}`
    const keyId = crypto.randomUUID()
    return { keyId, apiKey }
  }

  // Store hashed key (never store plaintext)
  async store(keyId, apiKey, userId, scopes = []) {
    const hashedKey = await hash(apiKey, 12)
    await db.query(
      'INSERT INTO api_keys (id, hashed_key, user_id, scopes, created_at) VALUES ($1, $2, $3, $4, NOW())',
      [keyId, hashedKey, userId, JSON.stringify(scopes)]
    )
  }

  async validate(apiKey) {
    // Extract key prefix to find candidates
    const [prefix] = apiKey.split('_')
    const rows = await db.query('SELECT * FROM api_keys WHERE prefix = $1 AND revoked = false', [prefix])

    for (const row of rows) {
      if (await compare(apiKey, row.hashed_key)) {
        await this.recordUsage(row.id)
        return { userId: row.user_id, scopes: row.scopes }
      }
    }
    return null
  }

  async rotate(oldKeyId, userId) {
    const { keyId, apiKey } = this.generate()
    await this.store(keyId, apiKey, userId)
    await db.query('UPDATE api_keys SET revoked = true WHERE id = $1', [oldKeyId])
    return { keyId, apiKey }
  }
}

Broken Object Level Authorization (BOLA)

// VULNERABLE: No ownership check
app.get('/api/orders/:id', authMiddleware, async (req, res) => {
  const order = await db.findOne('orders', { id: req.params.id })
  res.json(order)  // Returns ANY order regardless of owner
})

// SECURE: Always validate ownership
app.get('/api/orders/:id', authMiddleware, async (req, res) => {
  const order = await db.findOne('orders', {
    id: req.params.id,
    user_id: req.user.id  // Enforce ownership
  })

  if (!order) return res.status(404).json({ error: 'Not found' })
  res.json(order)
})

// RBAC middleware
function requireScope(scope) {
  return (req, res, next) => {
    if (!req.user.scopes.includes(scope)) {
      return res.status(403).json({ error: `Requires scope: ${scope}` })
    }
    next()
  }
}

app.delete('/api/users/:id', authMiddleware, requireScope('users:delete'), handler)

Rate Limiting Strategies

import Bottleneck from 'bottleneck'
import { RateLimiterRedis } from 'rate-limiter-flexible'
import { createClient } from 'redis'

const redis = createClient()

// Per-user rate limiter
const userLimiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'rl_user',
  points: 100,      // requests
  duration: 60,     // per 60 seconds
  blockDuration: 600, // block for 10 minutes
})

// Per-IP rate limiter
const ipLimiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'rl_ip',
  points: 200,
  duration: 60,
})

async function rateLimitMiddleware(req, res, next) {
  const ip = req.ip
  const userId = req.user?.id

  try {
    // Check IP limit
    await ipLimiter.consume(ip)

    // Check user limit if authenticated
    if (userId) await userLimiter.consume(userId)

    next()
  } catch (rejInfo) {
    const retryAfter = Math.round(rejInfo.msBeforeNext / 1000)
    res.set('Retry-After', retryAfter)
    res.status(429).json({
      error: 'Too Many Requests',
      retry_after: retryAfter,
    })
  }
}

Request Validation with Zod

import { z } from 'zod'

const CreateOrderSchema = z.object({
  product_id: z.string().uuid(),
  quantity: z.number().int().positive().max(100),
  delivery_address: z.object({
    street: z.string().max(200),
    city: z.string().max(100),
    country: z.string().length(2).toUpperCase(),
    zip: z.string().regex(/^d{5}(-d{4})?$/),
  }),
  notes: z.string().max(500).optional(),
})

function validateBody(schema) {
  return (req, res, next) => {
    try {
      req.validatedBody = schema.parse(req.body)
      next()
    } catch (err) {
      res.status(400).json({
        error: 'Validation failed',
        details: err.errors.map(e => ({ path: e.path.join('.'), message: e.message })),
      })
    }
  }
}

app.post('/api/orders', authMiddleware, validateBody(CreateOrderSchema), handler)

SSRF Prevention

import { URL } from 'url'
import ipRangeCheck from 'ip-range-check'

const BLOCKED_RANGES = [
  '10.0.0.0/8',    // Private networks
  '172.16.0.0/12',
  '192.168.0.0/16',
  '127.0.0.0/8',   // Loopback
  '169.254.0.0/16', // Link-local
  '::1/128',        // IPv6 loopback
]

const ALLOWED_HOSTS = ['api.example.com', 'cdn.example.com']

async function safeRequest(url) {
  const parsed = new URL(url)

  // Allowlist check
  if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
    throw new Error(`Host not allowed: ${parsed.hostname}`)
  }

  // Resolve DNS and check IP
  const { address } = await dns.promises.lookup(parsed.hostname)
  if (ipRangeCheck(address, BLOCKED_RANGES)) {
    throw new Error(`IP address blocked: ${address}`)
  }

  // Only allow HTTPS
  if (parsed.protocol !== 'https:') {
    throw new Error('Only HTTPS allowed')
  }

  return fetch(url, {
    headers: { 'User-Agent': 'MyApp/1.0' },
    redirect: 'manual', // Don't follow redirects automatically
  })
}