用 Cloudflare 给网站做安全防护(网站上线后)
发给 ChatGPT / Claude 等 AI,可更快消化本页并上手上站。
用 Cloudflare 给网站做安全防护:通用上手教程
适用场景:任何经过 Cloudflare 代理的网站,不限框架。Next.js、TanStack Start、Nuxt、Remix、Astro、SvelteKit,或传统的 Node/PHP/Go 后端都适用;部署在 Cloudflare Workers/Pages,或者自己的服务器、域名 DNS 开了橙色云朵代理,也都适用。
目标:后台别人进不来、扫描器打不进来、封禁的账号登不上、数据库不被注入。
文中www.example.com、example.com、my-worker、/admin、/api/admin、/api/auth/sign-in都是示例,换成你自己的域名、Worker 名和路径。
全程跟着点就行,大部分操作不用改代码。
0. 先搞清楚:Cloudflare 默认帮你挡了什么
| 攻击类型 | Cloudflare 默认能挡吗 | 怎么补 |
|---|---|---|
| 流量型 DDoS(大量请求把网站打挂) | 能,免费版自带 | 不用管 |
| 简单爬虫 / 无头浏览器滥刷 | 不能自动拦 | 第 2 步 Bot Fight(可选,默认关);真人靠 Turnstile |
| AI 爬虫抓取内容 | 不能自动拦 | 第 2 步 AI Crawl Control(Free) |
接口被刷(/api/*) |
不能自动拦 | 第 2 步 1 条 Rate Limit(Free 限 1 条) |
| 慢慢试密码(暴力破解登录) | 不能自动识别 | 第 1 步 Zero Trust 保护后台 + 第 4 步限速 |
扫描器探测漏洞(偷 .env 等) |
不能自动拦 | 第 3 步自定义规则 |
| SQL 注入 | 免费版只覆盖少量 | 代码用参数化查询(第 5 步) |
| 被封账号继续登录 | 跟 Cloudflare 无关 | 代码服务端校验(第 6 步) |
一句话:Cloudflare 挡流量,后台和账号安全要靠你自己配和代码兜底。
前提:域名必须走 Cloudflare 代理(DNS 记录是橙色云朵,或者是 Worker 自定义域名)。灰色云朵(仅 DNS)的流量不经过 Cloudflare,下面的规则都不生效。
1. 用 Zero Trust 给后台加一道“门禁”(最重要)
这是什么
Zero Trust 是 Cloudflare 的免费安全服务(50 人以内免费)。其中的 Access 功能相当于在后台前面加一道门:
访问后台的请求还没到你的代码,Cloudflare 就先要求用你指定的邮箱验证一次。不在白名单里的人连后台登录页都看不到,就算密码被试出来也没用。跟你用什么框架写的没有关系。
操作步骤
第一步:开通
Cloudflare 控制台左侧进入 Zero Trust(新版叫 Cloudflare One),按提示开通免费计划。
第二步:确认登录方式
左侧 Integrations → Identity providers,确认有 One-time PIN(邮箱一次性验证码),一般默认就有。这是最简单的方式,不用去 Google Cloud 建 OAuth。
第三步:新建 Access 应用
- 左侧 Access controls → Applications → Add an application。
- 选 Self-hosted and private,下面的小标签选 Public DNS(网站用自己的域名时选这个;Workers 选项只用于保护 workers.dev 地址)。
- 点 Continue with Self-hosted and private。
第四步:填受保护的地址(Destinations → Public hostnames)
Subdomain 填 www(或你的子域名),Domain 选你的域名,Path 填后台路径。点 Add public hostname 逐行添加。先列出你项目里所有能进后台的地址,常见的有:
admin # 后台页面
api/admin # 后台接口
Path 填
admin会连同下面所有子路径(如/admin/users)一起保护。
多语言站点要注意:如果用了/zh/admin、/en/admin这种语言前缀路由(next-intl、TanStack Router 的$locale、Nuxt i18n 等都可能这样),每个语言前缀都要单独加一行,例如zh/admin、en/admin、ja/admin。可以在无痕窗口挨个试一下哪些地址能打开后台。
后台用独立子域名更省事:比如admin.example.com,Subdomain 填admin、Path 留空就保护了整个子域。
如果不带 www 的example.com也能直接打开网站(而不是跳转到 www),把上面的路径用空 Subdomain 再加一遍。
中间的 "browser-based RDP, SSH, or VNC" 保持 Off。
第五步:设置谁能进(Access policies)
新建一条 policy:
- Policy name:
only-me - Action:Allow
- 规则:Include → Emails,填你自己的邮箱(可以填多个)
保存。
验证
- 打开无痕窗口访问
https://www.example.com/admin,应该先出现 Cloudflare Access 登录页。 - 不要点上面的 "Cloudflare" 按钮(那是用 Cloudflare 账号登录,会带上 Cloudflare 账号的邮箱),直接在下面 Email 框手动输入白名单邮箱,点 Send login code。
- 收到 6 位验证码填进去,才会进入网站自己的登录页。
- 换一个不在白名单的邮箱试:页面同样显示“已发送”(这是故意的,不暴露谁有权限),但实际收不到验证码。
- 也可以用命令行检查,返回 302 跳到
xxx.cloudflareaccess.com就说明保护上了:curl -sI https://www.example.com/admin | grep -i location
常见问题
- Gmail 别名不算同一个邮箱:Cloudflare 按字面完全匹配。想用哪个邮箱进,就把哪个加进白名单。
- 不要把前台页面和登录接口(如
/api/auth)加进去,否则普通用户就登录不了。 - 前后端分离的项目:后台接口如果在另一个域名(如
api.example.com/admin),那个域名的路径也要加。
别忘了:关掉绕过 Cloudflare 的后门
Access 只保护你填的域名,其他能直接访问到网站的地址都是后门:
- 部署在 Workers / Pages:默认还有
xxx.workers.dev或xxx.pages.dev地址。进入 Workers & Pages → 你的项目 → Settings → Domains & Routes,把 workers.dev 和 Preview URLs 都关掉。Pages 项目的预览地址可以单独再加一个 Access 应用保护。 - 部署在自己的服务器:源站 IP 如果能直接访问,别人可以绕过 Cloudflare。服务器防火墙只放行 Cloudflare 的 IP 段,或者改用 Cloudflare Tunnel。
2. Free 域名实用防爬三件套(不另收费 · 人工与浏览器 AI 通用)
最高原则:宁可漏拦,不可误伤正常用户。 参数一律偏松;拿不准就跳过该项或保持 Allow。
费用说明:域名套餐 Free 就能开,不另收费。Workers Paid $5 与站点 Security 无关,不必升级 Pro。
Pro 才有的 Super Bot Fight Mode、更多 Rate Limit,本步骤不要开、不要升级。
目标域名示例:www.example.com(请替换为你自己的域名)。
前提:DNS 橙色云朵。灰色云朵则下列设置不生效。
给操作者 / 浏览器 AI 的总指令(可整段复制)
你正在 Cloudflare Dashboard 为域名 www.example.com 配置 Free 防爬。
硬约束(必须遵守):
- 禁止误伤正常真人用户:正常打开首页、浏览内容、登录、支付等相关业务页面必须畅通;出现验证码死循环、整站打不开、正常 API 被 429/Block,立刻回滚该项(关掉或放宽)。
- 只做下面 B → C → A(A 可选);不要升级套餐;不要改 Zero Trust / Access;不要新建第 2 条 Rate Limit。
- 搜索引擎爬虫(Googlebot、Bingbot、DuckDuckBot 等)一律 Allow,不要 Block。
- Rate Limit 必须用偏松频率;动作优先 Managed Challenge(没有再用 Block);缓解时间选最短。
- Bot Fight Mode 先不要开;仅当 B、C 完成后且自检通过,再确认是否开启 A;未明确要求「开 Bot Fight」则跳过 A。
- 每做完一项用中文回报:「已完成 / 已存在跳过 / 因防误伤跳过 / 失败原因」。
B. 配置 AI Crawl Control(优先做 · 对真人几乎无感)
- 打开 https://dash.cloudflare.com,进入域名 example.com。
- 左侧进入 AI Crawl Control(或 Manage AI crawlers;文案以 Dashboard 为准)。
- 只 Block「明确用于 AI 训练/抓取的爬虫」,例如列表里的:GPTBot、ClaudeBot、Bytespider、Google-Extended、CCBot、Amazonbot(若标注为 AI)、meta-externalagent 等。
- 以下必须保持 Allow(禁止改成 Block):
- Googlebot、Googlebot-Image、Bingbot、Slurp、DuckDuckBot、Baiduspider、YandexBot 等常规搜索引擎
- 任何名称含 search / seo、或说明为「search indexing」的条目
- 看不清用途的条目 → 保持默认/Allow,不要 Block
- 不要对「Verified bots」或「Good bots」一键全拦。
- 同页若有 Block AI bots:可以开(只针对已识别 AI bot)。AI Labyrinth 若说明会迷惑爬虫且不影响真人,可开;若提示可能影响正常抓取/不确定,则跳过。
- 保存;若弹窗要求升级 Pro → 取消,不要升级。
完成标准:仅 AI 训练类爬虫为 Block;所有搜索引擎类为 Allow;未升级套餐。
C. 新建 1 条 Rate Limit(盯 /api/ · 偏松,防刷不防人)
Free 只能 1 条 Rate Limit。已有规则则先读内容:已覆盖
/api且偏松 → 跳过;过严(如 10 秒内 >5)→ 放宽到下表,勿再新建第二条。
- Security → Security rules → Create rule,类型 Rate limiting rules(不是 Custom / Managed)。
- 按表填写(字段名略有差异时选最接近的;禁止选过严数值):
| 项 | 建议值(防误伤) |
|---|---|
| Rule name | api-rate-limit-soft |
| 匹配条件 | URI Path starts with /api/ |
| 排除(若界面有) | Verified Bot / 已知良好爬虫 → 不计入或跳过;没有该字段就忽略 |
| 计数维度 | 同一 IP |
| 频率 | 10 秒内超过 100 次(或 Free 界面里 ≥100 的最松档;若最高只能选更小,选允许的最大值,并在回报里写明实际值) |
| Action | 优先 Managed Challenge;没有则 Challenge;再没有才用 Block |
| 缓解时长 | 最短(如 10 秒) |
- 禁止把匹配改成整站
/或所有路径——只会误伤静态页与正常浏览。 - 禁止对
/_next/、静态资源建限流(本条也不要匹配它们)。 - Deploy。不要再建第 2 条。
完成标准:仅 1 条、匹配 /api/、频率偏松(约 10s/100+)、动作非永久封禁风格。
A. Bot Fight Mode(可选 · 默认跳过,易误伤)
Free 的 Bot Fight Mode 整站对「疑似简单机器人」发计算挑战,无法按路径白名单跳过,隐私浏览器、公司网、部分正常用户可能被反复验证。
默认:跳过,保持 Off。 仅当明确需要开启时才执行下面步骤。
- Security → Bots,确认当前是 Bot Fight Mode(不是 Super Bot Fight)。
- 打开 On。
- 不要开 Super Bot Fight(需 Pro)。
- 打开后立刻用无痕窗口访问首页;若出现反复人机验证、页面打不开 → 立即改回 Off,并记录「因误伤已关闭」。
完成标准:要么保持 Off(推荐),要么 On 且无痕访问首页一次成功、无死循环验证。
自检(必须做;任一失败则回滚对应项)
用无痕窗口(不要带管理员 Cookie)依次访问,全部应成功(可正常出页面,无 Cloudflare 死循环):
https://www.example.com/首页- 任意一个详情页(站内点进一个内容或商品页即可)
- 打开登录弹窗/登录页,确认能看到登录 UI(不必真的登录成功)
若 C 导致正常操作被拦:把频率改到界面最大,或暂时 Disable 该 Rate Limit 规则并排查。
若 A 导致验证死循环:立刻 Off Bot Fight Mode。
B 一般不影响真人;若误伤搜索收录,把对应爬虫改回 Allow。
与业务层的关系(本步不要改代码)
| 层 | 内容 |
|---|---|
| 本步 Cloudflare | AI Crawl Control(必做)+ 偏松 /api/ Rate Limit(必做)+ Bot Fight(默认不做) |
| 业务限流 | 应用内接口限流,代码已有则不动 |
| Turnstile | 登录/关键操作人机验证,已有则不动 |
真人交互主要靠 Turnstile + 业务限流;Cloudflare 本步只挡明显 AI 爬虫和极端刷接口,参数必须偏松。
3. 用自定义规则拦截漏洞扫描器
为什么要做
日志里经常能看到这类请求,1 分钟内试几十个路径:
/userfiles?path=../../../../.env
/api/fs/read?path=/app/.env
/api/proc/self/environ
/graphql /inngest /api/fs/exec
这是自动化扫描器在找漏洞、偷 .env。如果项目里没有这些路径,只会 404,不会泄露东西,但会刷屏日志、浪费请求额度。直接在 Cloudflare 门口拦掉。
操作步骤
- Cloudflare 控制台进入你的域名,左侧 Security → Security rules(旧版叫 WAF)。
- 点 Create rule,类型选 Custom rules(不是 Rate limiting,也不是 Managed rules)。
- Rule name 填
block-scanners。 - 点 Edit expression 切到手写模式,整段粘贴:
(lower(url_decode(http.request.uri)) contains ".env")
or (lower(url_decode(http.request.uri)) contains "/proc/self")
or (lower(url_decode(http.request.uri)) contains "../")
or (lower(url_decode(http.request.uri)) contains "/.git")
or (lower(url_decode(http.request.uri)) contains "/wp-admin")
or (lower(url_decode(http.request.uri)) contains "/wp-login")
- Choose action 选 Block,点 Deploy。
说明:
http.request.uri同时包含路径和?后面的参数;url_decode把%2F还原成/,防止编码绕过;lower防止大小写绕过。
按自己的项目删改:如果你的站本身就是 WordPress,去掉/wp-admin、/wp-login两行;如果有合法路径包含.env字样(很少见),把那行改得更精确。加规则前先想一下有没有正常页面会被误伤。
验证
浏览器打开 https://www.example.com/userfiles?path=../.env,看到 Cloudflare 拦截页就生效了。正常页面不受影响。
4. 防暴力破解:给登录接口限速(可选与配合)
注意:Free 只有 1 条 Rate Limit。若已按 第 2 步 建了盯
/api/*的规则,不要再新建第二条;登录暴力破解已被/api/覆盖时,本节能跳过。只有在你把唯一那条规则改成「只限登录路径、更严」时,才按下面改匹配条件。
Cloudflare 不会自动识别“慢慢试密码”。如果后台已经用第 1 步保护,前台登录可以再加一层:
- Security → Security rules → Create rule,类型选 Rate limiting rules。
- 条件:URI Path 以你的登录接口开头。常见写法:better-auth 是
/api/auth/sign-in,NextAuth/Auth.js 是/api/auth/callback/credentials,自己写的就按实际路径。不确定的话,打开浏览器开发者工具的 Network 面板,登录一次看请求地址。 - 频率:同一 IP 每 10 秒超过 5 次就 Block 一段时间(免费版可选的数值有限,按界面能选的来)。
- Deploy。
如果代码里自己做了按 IP 限流,但计数存在内存里,在 Workers 或多实例部署下各实例各算各的,只能挡一部分,所以门口这层更可靠。
5. SQL 注入:代码是真正的防线
原则
用户输入只能以参数形式传给数据库,不能拼进 SQL 字符串。主流 ORM 和查询构建器(Drizzle、Prisma、Kysely、TypeORM、Sequelize 等)默认都是参数化的,只要不用它们的“原始 SQL”接口拼字符串就安全。
写代码的规矩
// 正确:参数化,安全
db.select().from(user).where(eq(user.email, email)); // Drizzle
prisma.user.findUnique({ where: { email } }); // Prisma
sql`select * from user where email = ${email}`; // 模板标签里的变量会被绑定成参数
env.DB.prepare('select * from user where email = ?').bind(email); // D1 / SQLite 原生
// 错误:字符串拼接,危险
sql.raw(`SELECT * FROM user WHERE email = '${email}'`); // Drizzle
prisma.$queryRawUnsafe(`SELECT * FROM user WHERE email = '${email}'`); // Prisma
env.DB.prepare(`select * from user where email = '${email}'`);
自查方法
在项目里全局搜这些关键词,逐个看有没有把用户输入拼进去:
sql.raw $queryRawUnsafe $executeRawUnsafe .prepare(` query(`
搜不到,或者搜到的地方都没有拼接用户输入,就没问题。
Cloudflare 这边
- 免费版自带 Free Managed Ruleset,只覆盖少量高危漏洞。到 Security rules → Managed rules 确认它是开启的。
- 完整的 SQL 注入 / XSS 规则(Cloudflare Managed Ruleset、OWASP 规则集)需要 Pro 及以上。
- 只要代码层是参数化的,WAF 是额外保险,不必专门为此升级。
6. 封禁账号:必须在服务端拦截
常见错误
很多项目的封禁只在前端做:登录页先调一个接口问“这个邮箱是否被封”,被封就弹提示。但服务端不检查,只要有人绕过页面直接调登录接口,密码正确就能登进去;如果这个账号还挂着管理员角色,后台照样能用。而且那个“是否被封”的接口谁都能调,可以拿来探测哪些邮箱注册过。
正确做法(不管用什么框架和认证库)
- 登录时拦:账号密码、第三方登录(Google/GitHub)、验证邮件后自动登录,所有创建会话的入口都要在服务端检查封禁,被封就拒绝,不发登录 cookie。认证库一般有钩子可以挂,例如 better-auth 的
databaseHooks.session.create.before、Auth.js 的signIn回调,自己写的就在签发 token 前检查。 - 已登录的也要拦:封禁时同时删掉该用户所有会话;服务端“获取当前用户”的公共函数里,被封账号一律当作未登录。这样被封的管理员也进不了后台页面,调不了后台接口。
- 防探测:登录前不要提供“查询某邮箱是否被封”的公开接口,只有密码验证通过后才返回封禁原因。
- 管理员不能封禁自己,避免误操作把自己锁在外面。
管理员的操作建议
- 后台主账号用第三方登录(如 Google),并设为超管。Google 账号自带两步验证,比密码安全。
- 不再使用的邮箱密码管理员账号:先移除它的管理员角色,再封禁。
- 部署后验证:用被封账号输对密码登录,应该被拦住;用主账号确认能正常进后台。
7. 看日志:怎么排查攻击与异常
Workers 项目在 Workers & Pages → 你的项目 → Logs 查看或导出日志;自己服务器的项目看服务器日志,或在 Cloudflare 的 Security → Events 看被拦下的请求。
| 现象 | 结论 |
|---|---|
1 分钟内大量请求不存在的路径(.env、/graphql、/proc/self 等),全是 404 |
扫描器,不是 bug,用第 3 步规则拦掉 |
请求带 ?_rsc=xxx(Next.js)或 /_serverFn/(TanStack Start)等框架内部参数 |
框架自己的页面跳转、预取或服务端函数调用,是正常用户在操作,不是攻击 |
Network connection lost 偶尔一次 |
Cloudflare 网络偶发中断,不用管 |
| 同一个正常页面反复报错 | 代码问题,需要看完整错误原文 |
日志里只有几行
at worker.js:xxxx的堆栈时,错误原文往往在同一个 requestId 下的另一条日志里。Workers 项目也可以用npx wrangler tail 你的Worker名实时看线上日志,再去页面上复现。
检查清单
- [ ] 域名 DNS 是橙色云朵(走 Cloudflare 代理)
- [ ] 第 1 步:Zero Trust 已开通,Access 应用保护了所有后台页面和后台接口路径(含各语言前缀)
- [ ] 第 1 步:Access policy 只放行自己的邮箱,已用无痕窗口验证
- [ ] 第 1 步:workers.dev、pages.dev、Preview URLs 已关闭,或源站 IP 已限制只允许 Cloudflare 访问
- [ ] 第 2 步:AI Crawl Control 只 Block AI 训练爬虫,搜索引擎保持 Allow
- [ ] 第 2 步:仅 1 条偏松 Rate Limit(约 10s/100+,匹配
/api/);无痕访问首页/详情页/登录正常 - [ ] 第 2 步:Bot Fight Mode 默认 Off;若开启则无痕验证无死循环(未开 Super Bot Fight / 未升级 Pro)
- [ ] 第 3 步:自定义规则
block-scanners已部署并验证 - [ ] 第 4 步:(可选)登录接口限速——仅当未用第 2 步的
/api/规则时再单独配置 - [ ] 第 5 步:Free Managed Ruleset 已开启,代码已检查无原生 SQL 字符串拼接
- [ ] 第 6 步:所有登录入口和“获取当前用户”都在服务端检查封禁,旧管理员账号已降权
- [ ] 第 7 步:掌握查看 Security Events 与实时排查日志的方法