在 Next.js 边缘中间件中集成 DataDome 机器人防护:从一键部署到源码级原理
【免费下载链接】examplesEnjoy our curated collection of examples and solutions. Use these patterns to build your own robust and scalable applications.项目地址: https://gitcode.com/GitHub_Trending/examples1/examples
DataDome 是一款实时机器人防护(Bot Protection)服务,能为任意网站提供 bot 识别、验证码挑战与其他安全防护能力。本仓库中的edge-middleware/bot-protection-datadome示例演示了如何通过 Next.js Edge Middleware,在请求到达应用之前就交给 DataDome 判定,从而把防护逻辑下沉到边缘网络。阅读本文后,你将掌握该示例的完整运行方式(一键部署与本地克隆)、三个演示路由的差异,以及lib/datadome.ts中请求校验、超时兜底、响应头回写等核心实现的底层原理。
示例概览:在边缘完成机器人判定
根据该示例 README 的说明,DataDome 提供实时机器人防护以及其它安全防护能力,而本模板的关键思路是在边缘(Edge Middleware)使用它:中间件把每个受保护请求的上下文信息转发给 DataDome 的校验接口,由 DataDome 返回"放行"或"拦截"的结论,拦截时可直接改写(rewrite)到 DataDome 的验证码页面。
模板自带一个在线演示地址(https://edge-functions-bot-protection-datadome.vercel.app),你可以通过它直观对比"受保护页面"与"未受保护页面"在响应头、延迟上的差异。
三个演示路由:受保护、被拦截与豁免
示例页面共三个路由,分别对应防护的三种状态,源码位于 pages 目录:
| 路由 | 页面文件 | 行为 |
|---|---|---|
/ | pages/index.tsx | 启用 DataDome 的首页,正常用户可直接访问,并展示响应中的x-datadome头 |
/blocked | pages/blocked.tsx | 强制触发拦截的演示页,会把你送入验证码挑战 |
/omit | pages/omit.tsx | 完全不走 DataDome 的对照页,便于观察防护带来的头部与延迟差异 |
在blocked页面中,README 页面文案提示:验证码通常只需要通过一次,之后刷新或再次访问不会再弹出,除非你在 DevTools 的 Application → Storage → Cookies 中手动删除datadomecookie。这也侧面说明 DataDome 依靠客户端 cookie 记忆"已通过验证"的身份。
快速开始
示例 README 提供了两种使用方式。
方式一:一键部署到 Vercel
点击 README 中的 "Deploy with Vercel" 按钮即可完成部署,部署时会要求配置两个环境变量:NEXT_PUBLIC_DATADOME_CLIENT_SIDE_KEY与DATADOME_SERVER_SIDE_KEY。该需求同时被 vercel.json 与 pages/_app.tsx 中的部署按钮配置所印证。
方式二:克隆到本地运行
使用create-next-app配合 pnpm 拉取该示例:
pnpm create next-app --example https://github.com/vercel/examples/tree/main/edge-middleware/bot-protection-datadome bot-protection-datadome运行前需要有一个 DataDome 账号,然后在示例目录中把环境变量示例文件复制为本地文件(该文件会被 Git 忽略):
cp .env.example .env.local接着打开.env.local,将环境变量替换为 DataDome 控制台中显示的密钥。README 指出密钥可在 DataDome 控制台的https://app.datadome.co/dashboard/config/protection/keys找到。最后启动开发服务器:
pnpm dev环境变量清单
结合 lib/datadome.ts 与 pages/_app.tsx 的源码,本示例实际读取的环境变量如下:
| 变量 | 是否必填 | 默认值 | 用途 |
|---|---|---|---|
DATADOME_SERVER_SIDE_KEY | 是 | 无 | 服务端密钥,作为校验请求中的Key字段(见datadome.ts中的requestData.Key) |
NEXT_PUBLIC_DATADOME_CLIENT_SIDE_KEY | 是 | 无 | 客户端密钥,注入到tags.js脚本的window.ddjskey中 |
DATADOME_TIMEOUT | 否 | 300 | 边缘侧等待 DataDome 响应的超时毫秒数 |
DATADOME_ENDPOINT | 否 | https://api.datadome.co | DataDome 校验 API 地址,validateEndpoint()会自动补全https://前缀 |
边缘中间件:只保护需要保护的路由
入口中间件位于 middleware.ts,其核心逻辑非常精简:
export const config = { // It's possible to run Datadome for all paths, but it's better to take // advantage of pattern matching and only protect from bots where required. matcher: ['/', '/blocked'], } export default async function middleware(req: NextRequest) { const { pathname } = req.nextUrl // Force the page to be blocked by DataDome if (pathname === '/blocked') { req.headers.set('user-agent', 'BLOCKUA') } return datadome(req) }两点值得注意:
- 用
matcher精确圈定保护范围:源码注释明确说明,虽然可以让 DataDome 覆盖所有路径,但更优的做法是利用matcher模式匹配,只在需要的地方启用防护。这也是为什么/omit路由不在matcher列表中——它天然绕过了中间件,成为对照组。 /blocked的拦截是"伪造"的:该路由会把请求头中的user-agent强行改成BLOCKUA,让 DataDome 把本应正常的请求判定为机器人,从而演示拦截与验证码流程。
中间件对datadome(req)的返回值有三种理解:有响应且带 rewrite,说明被拦截(应返回该响应,通常已改写为 DataDome 验证码页);有响应但无 rewrite,说明你不是机器人,响应会携带 DataDome 的回写头;无响应则直接放行。
源码级拆解:lib/datadome.ts 如何与 DataDome 通信
中间件真正的重头戏在 lib/datadome.ts(通过 tsconfig 中的@lib/*路径别名引入)。整个校验流程可以概括为:静态资源放行 → 组装请求特征 → POST 校验 → 超时兜底 → 按状态码处理 → 回写响应头。
1. 静态资源直接放行
const DATADOME_URI_REGEX_EXCLUSION = /\.(avi|flv|mka|mkv|mov|mp4|mpeg|mpg|mp3|flac|ogg|ogm|opus|wav|webm|webp|bmp|gif|ico|jpeg|jpg|png|svg|svgz|swf|eot|otf|ttf|woff|woff2|css|less|js|map)$/i对图片、音视频、字体、样式表、脚本等静态资源直接return,不占用 DataDome 配额,也避免边缘侧为每个静态请求增加额外延迟。
2. 组装完整的请求特征上报
requestData把一次 HTTP 请求的几乎所有上下文(方法、路径、查询参数、Host、各协议头、Cookie 长度、sec-ch-ua*客户端提示、sec-fetch-*等)收集起来,POST 到DATADOME_ENDPOINT + '/validate-request/'。其中两个细节很有参考价值:
- 客户端 IP 的取法:代码注释说明按规范应取
x-real-ip,但它在 Edge Middleware 上不可用,因此退而使用x-forwarded-for的第一段,本地无此头时回退到127.0.0.1(注释同时提醒:本地调试时 DataDome 通常不会拦截,除非使用真实 IP)。 - 字段长度裁剪:
truncateRequestData维护了一张字段长度上限表(如useragent: 768、referer: 1024、request: 2048,xforwardedforip为-512表示保留尾部),超长字段会被截断,避免上报体过大。
3. 超时兜底:Promise.race 竞速
const timeoutPromise = new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('Datadome timeout')) }, DATADOME_TIMEOUT) }) dataDomeRes = (await Promise.race([ dataDomeReq, timeoutPromise, ])) as NextResponse当 DataDome 服务不可用或响应过慢时,Promise.race会以DATADOME_TIMEOUT(默认 300ms)触发超时,代码捕获异常后console.error并return(即放行请求)。这是一个典型的fail-open设计:防护服务故障不应拖垮正常业务,宁可暂时放行也不阻塞用户。
4. 按状态码分流处理
switch (dataDomeRes.status) { case 400: // Something is wrong with our authentication return case 200: case 301: case 302: case 401: case 403: let res = NextResponse.next() if (dataDomeRes.status !== 200) { // blocked! res = new Response(dataDomeRes.body, {status: dataDomeRes.status}) as NextResponse ... } ... }- 400:说明服务端密钥等认证信息有问题,日志输出
statusText与响应体后放行; - 200:未拦截,构造
NextResponse.next()继续正常处理; - 301 / 302 / 401 / 403:被拦截,直接透传 DataDome 的响应体与状态码(通常是验证码页面)。命中 bot 时还会通过
x-datadome-isbot、x-datadome-botname、x-datadome-ruletype打印出机器人的名称与命中规则类型。
5. 回写 DataDome 响应头与 Cookie 域修复
DataDome 的响应通过x-datadome-headers头声明需要回写到浏览器的头列表,toHeaders会逐一取出并合并进最终响应。其中内置了一个知名 bug 的 workaround:当 DataDome 返回的set-cookie把域设置为整个公共后缀.vercel.app时,浏览器会拒绝写入该 cookie,因此代码将其改写为Domain=${req.headers.get('host')},保证datadomecookie 能正确种到当前域名下。
另外,源码中res.headers.set('x-datadome-latency', ...)这行带有注释:该延迟头仅为演示目的而加,生产环境并非必需。
客户端脚本注入:验证码与指纹的浏览器侧配合
DataDome 的防护并不只有服务端判定,浏览器侧脚本同样关键。pages/_app.tsx 使用next/script以lazyOnload策略加载两个脚本,脚本地址定义在 lib/constants.ts:
export const DATADOME_TAGS = 'https://js.datadome.co/tags.js' export const DATADOME_JS = 'https://api-js.datadome.co/js/'注入方式如下:
<Script strategy="lazyOnload" id="load-datadome">{` window.ddjskey = '${process.env.NEXT_PUBLIC_DATADOME_CLIENT_SIDE_KEY}' window.ddoptions = { endpoint: '${DATADOME_JS}' } `}</Script> <Script src={DATADOME_TAGS} strategy="lazyOnload" />ddjskey使用客户端密钥NEXT_PUBLIC_DATADOME_CLIENT_SIDE_KEY(带NEXT_PUBLIC_前缀,才能被 Next.js 暴露到浏览器);ddoptions.endpoint指向 DataDome 的 JS API 地址;- 两个脚本都采用
lazyOnload策略,避免阻塞页面首屏渲染。
页面如何直观展示防护效果
components/headers.tsx 是页面上的调试组件:它对指定路径发起HEAD请求,读取响应的x-datadome与x-datadome-latency两个头,连同实测延迟一并 JSON 展示。这样,访问/(受保护)与/omit(未受保护)时,你能直接看到:
- 受保护页面多出
x-datadome相关的响应头; x-datadome-latency与页面整体延迟的差异,即边缘侧引入 DataDome 校验的额外开销(可结合浏览器 DevTools Network 面板进一步确认)。
部署与工程化配置
项目脚本在 package.json 中定义(dev/build/start/lint),依赖方面使用next、react、react-dom以及@vercel/examples-ui示例 UI 库。vercel.json 中指定了pnpm turbo build作为构建命令,并用turbo-ignore实现"无相关变更不触发构建"的增量部署优化。
小结与生产实践建议
回顾本示例可以提炼出几条可直接复用的经验:
- 在边缘做防护:通过
matcher精确控制保护范围,静态资源与无关路由直接放行,兼顾安全与性能; - fail-open 超时策略:
Promise.race+ 默认 300ms 超时,防护服务异常时优雅降级,不阻塞正常流量; - 密钥分级:
DATADOME_SERVER_SIDE_KEY只存在于服务端(中间件内使用),浏览器侧只暴露NEXT_PUBLIC_DATADOME_CLIENT_SIDE_KEY; - 响应头透传:DataDome 通过
x-datadome-headers声明需要回写的头(含 cookie),并需注意公共后缀域名的 cookie 兼容问题; - 演示辅助头按需取舍:
x-datadome-latency属于演示用途,生产环境应评估是否需要暴露。
如果你想在自己的 Next.js 应用中接入 DataDome,最直接的方式就是克隆本示例,配置好两个密钥环境变量,再按需调整middleware.ts中的matcher覆盖范围即可。
【免费下载链接】examplesEnjoy our curated collection of examples and solutions. Use these patterns to build your own robust and scalable applications.项目地址: https://gitcode.com/GitHub_Trending/examples1/examples
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考