在支付系统中,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%。
我们的防御体系是这样工作的:
- Nginx 层:限制每个 IP 30r/s,瞬间拦下 95% 的请求
- 应用层滑动窗口:限制每个 IP 每分钟 60 次订单查询,进一步收紧
- IP 聚类检测:发现来自同一 C 段的大量相似请求模式,自动将该 C 段整体限流
- 验证码降级:对高频 IP 弹出滑块验证码,确认是人类操作
这套组合拳下来,攻击流量被有效阻断,正常用户完全不受影响。这里用的正是我们的 转卡码系统 v3 中内置的限流模块,它已经过上千个站点的生产验证。
总结
IP 限流是支付系统安全的第一道防线,但不是唯一一道。一个健壮的生产级限流方案应该:
- 采用多层架构,Nginx + 应用层 + 业务层各司其职
- 使用令牌桶或滑动窗口算法,平衡突发流量与平滑约束
- 基于 Redis Lua 脚本实现分布式原子性操作
- 引入 动态调整机制,根据系统负载自动缩放阈值
- 配合 白名单、灰度策略和验证码,减少误伤
如果你想快速在自己的支付系统中引入成熟的限流方案,源码商城的 转卡码系统 内置了完整的 IP 限流 + 智能入口池 + 健康检查三合一防护模块,开箱即用,支持高并发生产环境。