在基于Flask构建对外提供数据的Web接口时,接口暴露在公网意味着任何客户端都能发起HTTP请求。爬虫程序往往会在短时间内高频调用接口,不仅占用带宽和数据库连接,还可能批量窃取业务数据。通过校验请求头里的User-Agent以及实施访问频率限制,可以在不引入复杂风控系统的前提下,拦截大部分低级爬虫并缓解恶意高频请求。

理解User-Agent在请求中的角色与伪造风险
User-Agent是HTTP协议定义的标准请求头字段,用于告知服务器发起请求的客户端类型、操作系统及版本等信息。在Flask中,可以通过request.headers.get('User-Agent')直接读取该字段。正常浏览器访问时,User-Agent通常包含明确的浏览器标识,例如Mozilla/5.0 (Windows NT 10.0; Win64; x64)前缀。而很多简单爬虫脚本使用Python的requests库时,默认UA为python-requests/2.31.0,这就成了识别线索。
不过必须清楚,User-Agent完全由客户端控制,攻击者可以随意修改。使用curl命令时加上-A参数就能伪装成任意浏览器。因此,仅靠UA做放行或拦截并不可靠,它更适合作为第一道粗筛:把明显为空、包含爬虫特征字符串的请求直接拒绝,减少后续逻辑的资源消耗。对于伪造为浏览器的爬虫,需要配合频率限制来约束行为。
下面示例展示了一个最基础的UA过滤装饰器,将常见爬虫标识加入黑名单,在接口函数执行前先做判断:
from flask import Flask, request, abort
import functools
app = Flask(__name__)
BAD_UA_KEYWORDS = ['python-requests', 'curl', 'scrapy', 'bot']
def block_bad_ua(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
ua = request.headers.get('User-Agent', '')
for kw in BAD_UA_KEYWORDS:
if kw in ua.lower():
abort(403, description='Forbidden User-Agent')
return func(*args, **kwargs)
return wrapper
@app.route('/api/data')
@block_bad_ua
def api_data():
return {'msg': 'ok'}
基于Flask实现单机频率限制方案
频率限制的核心思想是记录每个客户端的请求时间戳,当单位时间内的请求数超过阈值时拒绝服务。在单进程、单实例部署的Flask应用中,使用Python标准库中的collections.defaultdict配合列表存储时间戳即可实现固定窗口限流。例如限制每个IP每六十秒最多访问三十次,每次请求时清理掉超过一分钟前的记录再统计数量。
这种内存方案代码简单、无外部依赖,但存在明显局限:Flask以多线程或多worker模式运行时,各进程内存不共享,限流状态会分散,导致实际允许的总请求数随worker数量倍增。此外,服务重启后限制记录清空,攻击者可以利用重启间隙突破限制。下面的代码演示了在单进程模式下用全局字典实现的限流装饰器:
from flask import Flask, request, abort
from collections import defaultdict
import time
app = Flask(__name__)
REQUEST_RECORDS = defaultdict(list)
MAX_REQUESTS = 30
WINDOW_SECONDS = 60
def rate_limit_ip(func):
def wrapper(*args, **kwargs):
client_ip = request.remote_addr
now = time.time()
records = REQUEST_RECORDS[client_ip]
# 清理过期时间戳
while records and records[0] < now - WINDOW_SECONDS:
records.pop(0)
if len(records) >= MAX_REQUESTS:
abort(429, description='Too Many Requests')
records.append(now)
return func(*args, **kwargs)
return wrapper
@app.route('/api/limited')
@rate_limit_ip
def api_limited():
return {'msg': 'within limit'}
若希望避免列表频繁弹出带来的性能损耗,也可采用令牌桶思路:为每个IP维护一个令牌数,定时补充。但无论哪种内存方案,都只适用于演示或极小流量内部接口。当项目使用Gunicorn多worker或者部署在多个容器实例时,必须引入集中式存储。
结合Redis的分布式限流与UA联合策略
在分布式环境中,所有Flask实例应连接同一个Redis服务,利用Redis的原子操作实现精确限流。常用做法是使用有序集合记录请求时间,或通过INCR配合EXPIRE做计数器。有序集合方案能支持滑动窗口,避免固定窗口在边界时刻允许双倍流量的问题。同时,我们把UA校验与Redis限流组合进同一个前置钩子,既拦爬虫标识又控访问速率。
具体实现中,可以用redis-py库,在每次请求时用ZREMRANGEBYSCORE删除旧记录,再用ZCARD获取当前窗口计数。若超过限制则返回429,否则用ZADD写入当前时间戳。由于Redis命令执行是单线程模型,多实例并发下也能准确计数。下表对比了三种方案的差异:
| 方案 | 部署适应性 | 精确度 | 额外依赖 |
|---|---|---|---|
| UA黑名单 | 任意 | 低,可伪造绕过 | 无 |
| 内存限流 | 仅单进程 | 中,重启失效 | 无 |
| Redis限流 | 多实例分布式 | 高,滑动窗口 | Redis服务 |
以下代码给出了UA加Redis限流的整合示例,假设已创建全局redis_client对象:
import time
from flask import Flask, request, abort
import redis
app = Flask(__name__)
redis_client = redis.StrictRedis(host='127.0.0.1', port=6379, db=0)
BAD_UA = ['python-requests', 'scrapy']
RATE_KEY_PREFIX = 'flask_rate:'
WINDOW = 60
MAX_COUNT = 50
def anti_crawl():
ua = request.headers.get('User-Agent', '').lower()
for kw in BAD_UA:
if kw in ua:
abort(403)
ip = request.remote_addr
key = RATE_KEY_PREFIX + ip
now = time.time()
# 移除窗口外记录
redis_client.zremrangebyscore(key, 0, now - WINDOW)
count = redis_client.zcard(key)
if count >= MAX_COUNT:
abort(429)
redis_client.zadd(key, {str(now): now})
redis_client.expire(key, WINDOW)
@app.before_request
def before():
if request.path.startswith('/api/'):
anti_crawl()
@app.route('/api/info')
def api_info():
return {'data': 'protected'}
在实际业务中,还可以将频率限制细化到登录用户令牌而非仅IP,防止同一局域网共用出口IP导致误伤。对于极重要的接口,建议在网关层如Nginx上叠加限流,形成多层防护。通过User-Agent初筛与Redis频率限制的组合,Flask接口能以较低开发成本抵御绝大多数自动化抓取行为。
FlaskUser_Agent频率限制修改时间:2026-08-17 19:16:14