> 摘要:内容抓取已经从简单脚本演变为基于无头浏览器、自动化框架和代理池的复杂行为。仅依赖 IP 封禁或 CAPTCHA 往往不够。本文从内容抓取原理出发,介绍如何结合 WAF、行为限制、浏览器指纹与 Bot 检测,在关键数据接口前建立一套更可靠的反抓取防护链路。
---
## 一、什么是内容抓取?
内容抓取,也叫 Web Scraping,指通过自动化脚本或 Bot 从网站中批量提取数据的行为。
对于普通静态页面,攻击者可能只需要一个简单的 HTTP 客户端,例如:
- `wget`
- `curl`
- Python `requests`
- Node.js `axios`
就可以直接请求网页 HTML,然后用正则、XPath 或 CSS Selector 提取内容。
但对于现代网站,尤其是前端渲染、接口异步加载的应用,简单 HTTP 抓取已经不够。于是更复杂的抓取程序开始使用:
- Puppeteer
- Playwright
- Selenium
- Headless Chrome
- 带 stealth 插件的自动化浏览器
- 住宅代理 / 数据中心代理池
这类 Bot 能执行 JavaScript、点击按钮、滚动页面、等待接口返回,行为上越来越接近真实用户,因此检测难度也显著提升。
---
## 二、哪些内容最容易成为抓取目标?
通常,越“有商业价值”或“计算成本高”的数据,越容易被抓取,例如:
- 航班搜索结果
- 酒店与旅游价格
- 电商商品价格
- 房产列表
- 招聘信息
- 用户公开资料
- 金融行情
- AI 训练语料
以航班搜索为例,一次搜索可能需要聚合多个供应商、计算价格、查询库存。如果竞争对手或恶意 Bot 批量调用搜索接口,不仅会造成数据泄露,还会增加后端成本,甚至影响真实用户体验。
---
## 三、传统防护方式的价值与局限
### 1. WAF:第一道防线,但不是终点
Web Application Firewall,即 WAF,可以根据规则拦截异常流量,例如:
- 已知恶意 IP
- 数据中心 IP 段
- 特定国家或地区访问
- 高频请求
- 异常 User-Agent
- 特征明显的攻击请求
WAF 的优势是部署简单、拦截直接,非常适合作为第一层防护。
但它也有明显局限:
- 攻击者可以频繁更换代理 IP
- 住宅代理会伪装成真实用户网络
- 高级 Bot 的请求头可能非常接近真实浏览器
- 仅靠 IP 维度难以判断“人”与“自动化浏览器”
因此,WAF 更适合作为“粗粒度过滤器”,而不是完整的反抓取方案。
---
### 2. CAPTCHA:有效,但伤害用户体验
CAPTCHA 可以要求用户证明自己是人类,例如:
- 图片选择
- 滑块验证
- 文字识别
- 行为验证码
它确实能拦截大量低成本 Bot,但问题也很明显:
- 打断用户操作流程
- 降低转化率
- 对移动端用户不友好
- 对无障碍用户不友好
- 高级 Bot 可能借助打码平台绕过
所以实践中,CAPTCHA 更适合作为“风险升高后的二次验证”,而不是每次请求都强制触发。
---
## 四、为什么要引入浏览器指纹与 Bot 检测?
高级抓取器往往会使用真实浏览器内核,例如 Headless Chrome。它们可以执行 JavaScript,也能渲染页面,因此仅靠“是否支持 JS”已经无法有效识别。
不过,自动化浏览器和真实用户浏览器之间仍然存在大量差异,例如:
- 浏览器 API 返回值异常
- WebDriver 痕迹
- Headless 环境特征
- 字体、Canvas、WebGL、Audio 指纹异常
- 插件与语言环境不一致
- 网络层行为异常
- 自动化框架注入痕迹
- 浏览器属性被篡改
- 事件行为缺失或不自然
浏览器指纹与 Bot 检测的核心思路是:
> 不只看“请求来自哪里”,而是分析“发起请求的浏览器环境是否可信”。
---
## 五、推荐架构:在关键数据接口前做 Bot 校验
一个比较稳妥的防护架构如下:
```text
用户浏览器
|
| 1. 加载前端页面
v
前端应用
|
| 2. 采集浏览器环境信号
v
Bot 检测服务
|
| 3. 返回 requestId
v
前端应用
|
| 4. 携带 requestId 请求业务接口
v
后端服务
|
| 5. 根据 requestId 查询检测结果
| 6. 校验是否为 Bot、时间、来源、IP
v
返回业务数据 / 拒绝访问
```
这种模式适合保护高价值数据接口,例如:
- `/api/flights/search`
- `/api/products/prices`
- `/api/real-estate/listings`
- `/api/user/profile/search`
它的关键点是:**不要只在前端判断 Bot,而要把最终决策放在服务端。**
因为前端代码可以被修改、调试、绕过,而服务端校验更难被直接篡改。
---
## 六、前端接入:在发起敏感请求前采集浏览器信号
假设我们要保护一个航班搜索接口。用户选择出发地和目的地后,点击“搜索航班”。
在发起真正的业务请求前,前端先调用 Bot 检测 SDK,获取一次检测请求的 `requestId`。
示例伪代码如下:
```javascript
// 初始化浏览器指纹 / Bot 检测 Agent
const fpPromise = loadFingerprintAgent({
publicKey: "<your-public-api-key>",
endpoint: "https://metrics.yourdomain.com"
})
// 用户点击搜索航班
async function onSearchFlights(from, to) {
// 1. 采集浏览器环境信号,并发送到检测服务
const detectionResult = await fpPromise.get()
// 2. 获取本次检测请求 ID
const requestId = detectionResult.requestId
// 3. 携带 requestId 请求后端业务接口
const response = await fetch("/api/flights", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
from,
to,
requestId
})
})
return await response.json()
}
```
### 技术原理
前端 SDK 会采集浏览器环境中的多维度信号,包括但不限于:
- User-Agent
- WebGL 信息
- Canvas 渲染差异
- 浏览器 API 行为
- Headless 特征
- 自动化框架痕迹
- 网络环境
- 时区、语言、平台信息
- 属性一致性检查
这些信号会被发送到检测服务,生成一次检测事件,并返回 `requestId`。
后端拿到 `requestId` 后,再去服务端 API 查询这次检测的真实结果。
### 有效性
这种方式比单纯前端判断更可靠,因为:
- Bot 很难完美伪造所有浏览器信号
- 结果由服务端查询,不依赖客户端自报
- 可以识别无头浏览器、自动化框架、篡改环境等
- 可与 WAF、风控系统、限流策略联动
### 局限性
它并不适合所有场景:
- 对首次 HTML 直出内容保护有限,因为页面初始加载时还没有完成浏览器信号采集
- 对公开静态资源无法完全阻止下载
- 依赖第三方检测服务时,需要考虑可用性、隐私和合规问题
- 极高级攻击者可能通过真实浏览器集群降低检测命中率
所以,更推荐将它用于“需要前端交互后才返回的数据接口”。
---
## 七、后端校验:不要信任客户端传来的 requestId
前端传来的 `requestId` 只能作为索引,不能直接信任。
服务端需要做几类关键校验:
1. `requestId` 是否真实存在
2. 检测结果是否为恶意 Bot
3. 检测事件是否足够新
4. 检测请求来源是否与当前业务请求一致
5. 检测请求 IP 是否与当前业务请求 IP 一致
下面用伪代码拆解。
---
## 八、服务端查询 Bot 检测结果
```javascript
async function flightsEndpoint(req, res) {
const { from, to, requestId } = req.body
let botEvent
try {
// 使用服务端密钥初始化检测服务客户端
const client = createBotDetectionClient({
apiKey: "<your-server-api-key>",
region: "global"
})
// 根据 requestId 查询本次浏览器检测事件
botEvent = await client.getEvent(requestId)
} catch (error) {
return res.status(403).json({
message: "requestId 无效,疑似伪造请求"
})
}
// 后续继续校验 botEvent
}
```
### 技术原理
`requestId` 是前端采集浏览器信号后生成的检测事件 ID。
后端通过服务端密钥查询检测服务,拿到可信的 Bot 检测结果,而不是相信前端自己传来的判断。
### 有效性
这一点非常重要。
错误做法是:
```javascript
// 不推荐:直接相信前端传来的 isBot
if (req.body.isBot === false) {
return getFlights()
}
```
因为攻击者可以直接修改请求体:
```json
{
"isBot": false
}
```
正确做法是:
```javascript
// 推荐:服务端根据 requestId 查询真实检测结果
const event = await botDetectionServerApi.getEvent(requestId)
```
### 局限性
如果攻击者拿到了一个真实用户生成的旧 `requestId`,仍然可能尝试重放。因此还需要做防重放校验。
---
## 九、判断是否为恶意 Bot
```javascript
const botResult = botEvent.products.botd.data.bot.result
if (botResult === "bad") {
return res.status(403).json({
message: "检测到恶意 Bot,拒绝访问航班数据"
})
}
```
检测结果通常可以分为:
```json
{
"bot": {
"result": "bad",
"type": "headlessChrome"
},
"userAgent": "Mozilla/5.0 ... HeadlessChrome ...",
"ip": "61.127.217.15",
"url": "https://yourdomain.com/search",
"time": "2023-09-08T16:43:23.241Z",
"requestId": "1234557403227.AbclEC"
}
```
常见结果包括:
| 结果 | 含义 |
|---|---|
| `bad` | 恶意或自动化 Bot,例如 Headless Chrome |
| `good` | 合法 Bot,例如搜索引擎爬虫 |
| `notDetected` | 未检测到 Bot 特征 |
### 实践建议
不要对所有 Bot 一刀切。
例如:
- 搜索引擎爬虫可以允许访问公开页面
- 价格接口、库存接口不应对爬虫开放
- 对 `notDetected` 用户可以正常放行
- 对高风险但不确定的请求可以触发限流或 CAPTCHA
推荐策略:
```javascript
if (botResult === "bad") {
deny()
} else if (botResult === "good") {
allowPublicOnly()
} else {
allow()
}
```
---
## 十、防重放攻击:校验检测事件的新鲜度
攻击者可能先通过真实浏览器获取一个合法 `requestId`,然后在自动化脚本中重复使用它。
这就是典型的 Replay Attack,即重放攻击。
因此,服务端必须检查检测事件是否足够新。
```javascript
const eventTime = new Date(botEvent.time).getTime()
const now = Date.now()
// 检测事件必须在 3 秒内生成
if (now - eventTime > 3000) {
return res.status(403).json({
message: "检测事件已过期,疑似重放攻击"
})
}
```
### 技术原理
正常用户点击“搜索”时,前端采集信号和后端搜索请求之间的时间间隔很短,通常在几百毫秒到数秒以内。
如果某个 `requestId` 是几分钟前、几小时前甚至几天前生成的,却被用来请求当前接口,就很可疑。
### 有效性
时间窗口可以显著降低旧 `requestId` 被复用的风险。
实践中可以根据业务调整:
| 场景 | 建议有效期 |
|---|---|
| 登录验证 | 5-10 秒 |
| 搜索接口 | 3-5 秒 |
| 表单提交 | 10-30 秒 |
| 支付/下单 | 更严格,并结合会话校验 |
### 局限性
时间窗口不能防御“实时中继”攻击。
例如攻击者控制一个真实浏览器实时获取 `requestId`,再立即转发给抓取程序。此时还需要结合 IP、会话、行为序列等因素进一步判断。
---
## 十一、校验 Origin:防止跨站伪造
服务端还应该校验 Bot 检测事件的来源页面是否和当前请求来源一致。
```javascript
const detectionOrigin = new URL(botEvent.url).origin
const requestOrigin = req.headers["origin"]
if (
detectionOrigin !== "https://yourdomain.com" ||
requestOrigin !== "https://yourdomain.com" ||
detectionOrigin !== requestOrigin
) {
return res.status(403).json({
message: "来源不一致,疑似伪造请求"
})
}
```
### 技术原理
正常情况下:
- 浏览器指纹检测请求来自你的网站页面
- 业务接口请求也来自你的网站页面
两者的 Origin 应该一致。
如果攻击者从其他域名、脚本环境或伪造页面中获取检测事件,再拿来请求你的接口,就可能出现 Origin 不匹配。
### 有效性
Origin 校验能拦截一部分跨站伪造和拼接请求。
### 局限性
需要注意:
- 某些环境下 `Origin` 可能不存在
- 反向代理、CDN、网关可能改写请求头
- 不应只依赖 `Origin` 做安全决策
- 要结合 CSRF Token、Session、SameSite Cookie 等机制
---
## 十二、校验 IP:防止 requestId 被转移使用
如果检测事件来自 IP A,而业务请求来自 IP B,就需要警惕。
```javascript
function getClientIp(req) {
// 注意:生产环境需确保 x-forwarded-for 只由可信代理设置
return req.headers["x-forwarded-for"]?.split(",")[0]
|| req.connection.remoteAddress
}
const detectionIp = botEvent.ip
const requestIp = getClientIp(req)
if (detectionIp !== requestIp) {
return res.status(403).json({
message: "IP 不一致,疑似 requestId 被转移使用"
})
}
```
### 技术原理
`requestId` 应该由同一个访问者在同一次请求链路中生成和使用。
如果两次请求 IP 不一致,可能意味着:
- requestId 被复制到其他脚本中使用
- 存在代理切换
- 存在中继攻击
- 存在多设备复用
### 有效性
IP 校验对于阻止低成本重放非常有效。
### 局限性
IP 并不总是稳定:
- 移动网络可能频繁切换出口 IP
- 企业网络可能经过多层代理
- IPv6 隐私地址可能变化
- CDN 后面需要正确解析真实客户端 IP
因此 IP 不一致不一定代表恶意,但在高价值接口中,通常可以作为强风险信号。
---
## 十三、完整服务端防护流程示例
下面是一个整合后的伪代码:
```javascript
async function protectedFlightsApi(req, res) {
const { from, to, requestId } = req.body
// 1. 基础参数检查
if (!from || !to || !requestId) {
return res.status(400).json({
message: "参数缺失"
})
}
let event
// 2. 根据 requestId 查询检测事件
try {
event = await botDetectionApi.getEvent(requestId)
} catch (err) {
return res.status(403).json({
message: "无效 requestId"
})
}
const botData = event.products.botd.data
// 3. 拦截恶意 Bot
if (botData.bot.result === "bad") {
// 可选:同步到 WAF 或风控系统
// waf.blockIp(botData.ip)
return res.status(403).json({
message: "检测到恶意 Bot"
})
}
// 4. 防重放:检查时间窗口
const eventTime = new Date(botData.time).getTime()
if (Date.now() - eventTime > 3000) {
return res.status(403).json({
message: "检测事件过期"
})
}
// 5. 校验来源
const detectionOrigin = new URL(botData.url).origin
const requestOrigin = req.headers["origin"]
if (
detectionOrigin !== "https://yourdomain.com" ||
requestOrigin !== "https://yourdomain.com"
) {
return res.status(403).json({
message: "请求来源异常"
})
}
// 6. 校验 IP
const requestIp = getClientIp(req)
if (botData.ip !== requestIp) {
return res.status(403).json({
message: "请求 IP 与检测 IP 不一致"
})
}
// 7. 通过校验,返回真实业务数据
const flights = await getFlightResults(from, to)
return res.status(200).json({
flights
})
}
```
---
## 十四、与 WAF 联动:从一次检测到持续封禁
当检测到恶意 Bot 后,不建议只返回 403。
更好的做法是把检测结果写入风控系统,并与 WAF、限流系统联动。
例如:
```javascript
if (botData.bot.result === "bad") {
riskEngine.record({
ip: botData.ip,
userAgent: botData.userAgent,
botType: botData.bot.type,
requestId: botData.requestId,
url: botData.url,
time: botData.time
})
if (riskEngine.score(botData.ip) > 80) {
waf.blockIp(botData.ip, {
ttl: "1h",
reason: "bad bot scraping"
})
}
return deny()
}
```
实践中可以设置多级策略:
| 风险等级 | 处理方式 |
|---|---|
| 低风险 | 正常放行,记录日志 |
| 中风险 | 限流、延迟响应 |
| 高风险 | CAPTCHA 或二次验证 |
| 严重风险 | 403 拒绝、WAF 封禁 |
| 持续攻击 | 黑名单、账户冻结、人工审核 |
这样可以避免误伤,也能提高攻击成本。
---
## 十五、本地测试思路:用自动化浏览器验证防护效果
为了验证反抓取方案是否有效,可以使用 Puppeteer、Playwright 或 Browserless 这类自动化环境模拟 Bot 访问。
示例伪代码:
```javascript
async function runBot(page) {
await page.goto("https://yourdomain.com/search")
await page.select("#from", "SFO")
await page.select("#to", "NYC")
await page.click("#search")
const response = await page.waitForResponse((res) => {
return res.url().includes("/api/flights")
})
console.log(response.status())
console.log(await response.text())
}
```
预期结果:
```json
{
"message": "检测到恶意 Bot,拒绝访问"
}
```
如果关闭 Bot 检测后,自动化浏览器可以正常抓取数据;开启后返回 403,说明基础防护链路已经生效。
---
## 十六、最佳实践总结
### 1. 不要只靠 IP 封禁
IP 封禁对低级 Bot 有效,但面对代理池、住宅代理时效果有限。
建议结合:
- IP 信誉
- 访问频率
- 浏览器指纹
- 行为特征
- 账号风险
- 请求上下文
---
### 2. 不要把安全判断放在前端
前端可以采集信号,但不应负责最终决策。
错误示例:
```javascript
if (!isBot) {
fetch("/api/data")
}
```
正确思路:
```text
前端采集信号 -> 后端查询检测结果 -> 后端决定是否返回数据
```
---
### 3. requestId 必须做完整性校验
至少检查:
- 是否存在
- 是否过期
- 是否来自同一 Origin
- 是否来自同一 IP
- 是否对应当前会话或用户
---
### 4. 对高价值接口单独加固
没有必要对所有接口使用同样强度的检测。
优先保护:
- 搜索接口
- 价格接口
- 库存接口
- 批量查询接口
- 用户数据接口
- 导出接口
---
### 5. 区分好 Bot 和坏 Bot
搜索引擎、监控服务、合作伙伴爬虫可能是“好 Bot”。
建议建立白名单机制,但白名单也要严格验证:
- IP 段
- DNS 反查
- User-Agent
- 访问路径
- 请求频率
---
## 十七、方案的局限性:没有银弹
浏览器指纹和 Bot 检测非常有价值,但它不是绝对防护。
常见局限包括:
1. **无法保护已经返回给浏览器的内容**
如果数据已经完整渲染在 HTML 中,就很难阻止被复制。
2. **对真实浏览器集群攻击成本更高,但不是完全无效**
攻击者可以控制真实浏览器、真实设备或真人众包降低检测率。
3. **可能存在误判**
某些隐私浏览器、企业安全软件、反指纹插件可能表现异常。
4. **需要持续调优**
Bot 对抗是动态过程,策略需要结合日志和业务反馈不断迭代。
5. **需要注意隐私合规**
浏览器指纹涉及设备与环境信号采集,应根据地区法规做好告知、合规评估和数据治理。
---
## 结语
防止内容被 Bot 抓取,不能依赖单点方案。更成熟的做法是构建多层防线:
```text
WAF / CDN
+
限流与访问频控
+
浏览器指纹与 Bot 检测
+
服务端 requestId 校验
+
Origin / IP / 时间窗口校验
+
风控系统与封禁策略
```
从实践经验看,最有效的方式不是“彻底消灭抓取”,而是持续提高攻击成本,让批量抓取在经济上变得不划算,同时尽量不影响真实用户体验。
本文仅供技术研究与学习交流,请勿用于违法违规用途。