在支付系统中,IP 限流是最基础也是最重要的安全防线之一。一个没有限流的支付接口,在遭遇恶意刷单、CC 攻击或爬虫抓取时,轻则服务器被打满、接口响应变慢,重则被刷走大量资金造成直接经济损失。本文将从算法原理到生产实战,完整讲解支付系统中 IP 限流的实现方案。

为什么支付系统需要 IP 限流?

支付接口天然是高价值目标。以我们的转卡码系统为例,一个没有任何限流的订单查询接口,攻击者可以用一台肉鸡每秒发起数百次请求,通过遍历订单号的方式窃取敏感信息,或者利用并发漏洞绕过库存检查重复发货。

以下是支付系统面临的典型 IP 攻击场景:

  • 订单枚举攻击:遍历查询接口,尝试批量获取订单信息
  • 并发刷单:短时间内大量提交支付请求,利用竞态条件绕过金额校验
  • 接口探测:扫描支付网关的所有 API 端点,寻找未鉴权的接口
  • 回调重放:重复发送支付回调通知,触发多次发货
  • 爬虫采集:批量抓取商城商品信息和价格

一个设计良好的 IP 限流策略,可以在不影响正常用户的前提下,有效阻断上述攻击行为。

四种核心限流算法对比

在动手实现之前,我们需要先理解限流算法的基本原理。以下是支付系统中最常用的四种算法:

1. 计数器算法(固定窗口)

最简单的方案:对每个 IP 维护一个计数器,在固定时间窗口内计数,超过阈值则拒绝请求。但临界问题明显——在窗口边界可能出现两倍于阈值的请求。

# 固定窗口计数器实现
import time
from collections import defaultdict

class FixedWindowRateLimiter:
    def __init__(self, limit=60, window=60):
        self.limit = limit        # 最多允许的请求数
        self.window = window      # 时间窗口(秒)
        self.counters = defaultdict(lambda: [0, 0])  # ip -> [count, window_start]

    def allow(self, ip):
        now = time.time()
        count, start = self.counters[ip]
        if now - start > self.window:
            # 重置窗口
            self.counters[ip] = [1, now]
            return True
        if count >= self.limit:
            return False
        self.counters[ip][0] += 1
        return True

2. 滑动窗口算法

改进计数器算法的临界缺陷,将窗口细分为多个小格子,实现更平滑的限流。Redis 的有序集合(ZSET)是实现滑动窗口的最佳选择。

import redis
import time

r = redis.Redis(host='localhost', port=6379, db=0)

def sliding_window_allow(ip, limit=60, window=60):
    key = f"rate_limit:{ip}"
    now = time.time()
    cutoff = now - window
    
    # 移除窗口外的记录
    r.zremrangebyscore(key, '-inf', cutoff)
    
    # 检查当前计数
    current_count = r.zcard(key)
    if current_count >= limit:
        return False
    
    # 添加当前请求
    r.zadd(key, {str(now): now})
    r.expire(key, window + 1)
    return True

3. 令牌桶算法

令牌桶是生产环境中最常用的限流算法。它允许一定程度的突发流量,同时限制长期平均速率。系统以恒定速率向桶中添加令牌,请求到达时消耗一个令牌,桶空则拒绝。

import time
from threading import Lock

class TokenBucket:
    def __init__(self, rate=10, burst=20):
        self.rate = rate           # 令牌填充速率(个/秒)
        self.burst = burst         # 桶容量(最大突发)
        self.tokens = burst
        self.last_refill = time.time()
        self.lock = Lock()
    
    def _refill(self):
        now = time.time()
        elapsed = now - self.last_refill
        new_tokens = elapsed * self.rate
        if new_tokens > 0:
            self.tokens = min(self.burst, self.tokens + new_tokens)
            self.last_refill = now
    
    def allow(self, tokens=1):
        with self.lock:
            self._refill()
            if self.tokens >= tokens:
                self.tokens -= tokens
                return True
            return False

4. 漏桶算法

漏桶算法与令牌桶相反——请求先进入桶中,然后以固定速率流出。它强制了完全平滑的流出速率,适合保护下游系统不被突发流量冲垮,但会拒绝超过桶容量的所有请求。

Nginx 层 IP 限流配置

在应用层实现限流之前,我们建议先在 Nginx 层做一层流量过滤。Nginx 自带的 limit_req 模块性能极高,单机可以处理万级 QPS 的限流判断。

# /etc/nginx/nginx.conf — HTTP 段配置
http {
    # 定义限流区域:10MB 共享内存,限制每个 IP 平均 30请求/秒
    limit_req_zone $binary_remote_addr zone=pay_limit:10m rate=30r/s;
    
    # 针对支付回调接口的更严格限流
    limit_req_zone $binary_remote_addr zone=callback_limit:10m rate=5r/s burst=10;
    
    server {
        listen 443 ssl;
        server_name greenfield.ltd;
        
        # 通用 API 限流
        location /api/ {
            limit_req zone=pay_limit burst=20 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }
        
        # 支付回调 — 严格限流
        location /callback/ {
            limit_req zone=callback_limit burst=5 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }
    }
}

配置说明:burst=20 允许突发队列最多 20 个请求排队,nodelay 表示排队请求不等待直接处理(但会计入速率限制)。当超过限制时返回 HTTP 429(Too Many Requests)。

Nginx 限流虽然高效,但只支持单机维度,在多台服务器负载均衡的场景下需要配合 Redis 实现分布式限流。

基于 Redis 的分布式 IP 限流实现

对于多实例部署的支付系统,单机限流不够——攻击者可以分散请求到不同服务器绕过限制。我们需要一个中心化的限流方案。

# Redis Lua 脚本实现原子性令牌桶(分布式安全)
local key = KEYS[1]
local now = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])    -- 每秒填充速率
local burst = tonumber(ARGV[3])   -- 桶容量
local cost = tonumber(ARGV[4])    -- 本次消耗令牌数

-- 获取当前令牌数和上次填充时间
local tokens = redis.call('hget', key, 'tokens')
local last_refill = redis.call('hget', key, 'last_refill')

if tokens == false then
    tokens = burst
    last_refill = now
else
    tokens = tonumber(tokens)
    last_refill = tonumber(last_refill)
    -- 补充令牌
    local elapsed = math.max(0, now - last_refill)
    local new_tokens = elapsed * rate
    tokens = math.min(burst, tokens + new_tokens)
end

if tokens >= cost then
    tokens = tokens - cost
    redis.call('hset', key, 'tokens', tokens)
    redis.call('hset', key, 'last_refill', now)
    redis.call('expire', key, 10)
    return 1  -- 允许
else
    return 0  -- 拒绝
end

将这段 Lua 脚本保存为 token_bucket.lua,然后在 Python 中调用:

import redis
import time

r = redis.Redis(decode_responses=True)

def check_ip_rate(ip, rate=10, burst=20):
    """分布式令牌桶检测"""
    key = f"bucket:{ip}"
    now = time.time()
    script = r.register_script(open('token_bucket.lua').read())
    result = script(keys=[key], args=[now, rate, burst, 1])
    return result == 1

动态限流:根据业务指标自动调整阈值

静态限流(固定阈值)最大的问题是无法应对流量波动——高峰期容易误伤正常用户,低峰期又可能存在安全漏洞。更先进的方案是动态限流,根据系统实时指标自动调整 IP 限流阈值。

以下是一个基于反馈环的动态限流器:

class DynamicRateLimiter:
    def __init__(self, base_limit=60, window=60):
        self.base_limit = base_limit
        self.window = window
        self.cpu_threshold = 80  # CPU 超过此值开始收紧限流
        self.error_threshold = 5  # 5xx 错误率超过此值开始收紧
    
    def _get_system_load(self):
        """获取系统负载指标"""
        import psutil
        cpu_percent = psutil.cpu_percent(interval=0.5)
        return cpu_percent
    
    def _get_effective_limit(self):
        """根据系统负载动态计算限流阈值"""
        cpu = self._get_system_load()
        if cpu > self.cpu_threshold:
            # 负载高,收紧限流到基础值的 40%
            factor = max(0.4, 1 - (cpu - self.cpu_threshold) / 50)
        else:
            factor = 1.0
        return int(self.base_limit * factor)
    
    def allow(self, ip):
        effective_limit = self._get_effective_limit()
        # 使用滑动窗口算法, 阈值为动态值
        return sliding_window_allow(ip, limit=effective_limit)

生产环境最佳实践

在源码商城的生产环境中,我们采用了多层限流架构,以下是几条关键经验:

1. 限流分层

不要在单层完成所有限流。推荐三层架构:

  • 第一层:Nginx — 做粗粒度 IP 限流,过滤 90% 的恶意流量,阈值设为正常值的 3-5 倍
  • 第二层:应用中间件 — Flask/Django 中间件做细粒度接口级限流,区分不同 API 的重要等级
  • 第三层:业务层 — 关键操作(如下单、支付回调)做二次检查,确保资金安全

2. 限流过后的正确处理

被限流的请求不应该简单丢弃。正确的做法是:

  • 返回 HTTP 429 状态码,附带 Retry-After 头告诉客户端多久后重试
  • 记录限流日志到独立的日志文件,用于后续分析攻击模式
  • 触发告警:如果某个 IP 连续多次被限流,自动加入黑名单并通知管理员
# Flask 限流中间件示例
from flask import request, jsonify, g
import time

def rate_limiter_middleware():
    ip = request.remote_addr
    path = request.path
    
    # 不同接口使用不同限流策略
    rate_config = {
        '/api/order/create':  {'limit': 10, 'window': 60},
        '/api/order/query':   {'limit': 30, 'window': 60},
        '/api/callback/pay': {'limit': 5,  'window': 60},
    }
    
    config = rate_config.get(path, {'limit': 60, 'window': 60})
    
    if not check_ip_rate(ip, rate=config['limit']/config['window'], burst=config['limit']):
        return jsonify({
            'code': 429,
            'message': '请求频率过高,请稍后重试',
            'retry_after': 30
        }), 429, {'Retry-After': '30'}

3. 白名单与灰度策略

某些 IP 应该豁免限流或使用更宽松的策略:

  • 支付宝/微信回调 IP:支付网关的回调 IP 固定,应加入白名单
  • 内网 IP:管理后台的访问不走公网限流
  • VIP 客户:大客户 IP 可分配更高的配额
# IP 白名单管理
WHITELIST = {
    '110.75.0.0/16': 'alipay_callback',   # 支付宝回调网段
    '140.207.0.0/16': 'wechat_callback',   # 微信支付回调网段
    '10.0.0.0/8':     'internal',          # 内网
}

def is_whitelisted(ip):
    import ipaddress
    ip_obj = ipaddress.ip_address(ip)
    for cidr, name in WHITELIST.items():
        if ip_obj in ipaddress.ip_network(cidr):
            return True
    return False

4. 限流数据可视化

搭建一个简单的限流看板,实时监控每个 IP 的请求频率和限流触发情况,可以帮助你快速发现异常流量模式。推荐使用 Prometheus + Grafana 采集限流指标。

# Prometheus 指标暴露
from prometheus_client import Counter, Histogram, generate_latest

rate_limit_hits = Counter('rate_limit_hits_total', 
    'Total number of rate limited requests', ['ip', 'path'])
request_duration = Histogram('request_duration_seconds',
    'Request duration in seconds', ['path'])

实际案例分析

在运营源码商城转卡码系统的过程中,我们遭遇过一次典型的 IP 攻击。攻击者使用了 200 个代理 IP 池,以每个 IP 每秒 3 个请求的频率扫描我们的订单查询接口。如果没有限流,这相当于每秒 600 个请求,足以让单台服务器 CPU 飙到 100%。

我们的防御体系是这样工作的:

  1. Nginx 层:限制每个 IP 30r/s,瞬间拦下 95% 的请求
  2. 应用层滑动窗口:限制每个 IP 每分钟 60 次订单查询,进一步收紧
  3. IP 聚类检测:发现来自同一 C 段的大量相似请求模式,自动将该 C 段整体限流
  4. 验证码降级:对高频 IP 弹出滑块验证码,确认是人类操作

这套组合拳下来,攻击流量被有效阻断,正常用户完全不受影响。这里用的正是我们的 转卡码系统 v3 中内置的限流模块,它已经过上千个站点的生产验证。

总结

IP 限流是支付系统安全的第一道防线,但不是唯一一道。一个健壮的生产级限流方案应该:

  • 采用多层架构,Nginx + 应用层 + 业务层各司其职
  • 使用令牌桶或滑动窗口算法,平衡突发流量与平滑约束
  • 基于 Redis Lua 脚本实现分布式原子性操作
  • 引入 动态调整机制,根据系统负载自动缩放阈值
  • 配合 白名单、灰度策略和验证码,减少误伤

如果你想快速在自己的支付系统中引入成熟的限流方案,源码商城的 转卡码系统 内置了完整的 IP 限流 + 智能入口池 + 健康检查三合一防护模块,开箱即用,支持高并发生产环境。