Cloudflare Waiting Room 完整指南:从原理到生产级安全实践
乔文飞 Lv8

Cloudflare Waiting Room 是一个运行在边缘网络的虚拟排队系统。当流量突然激增(大促、抢票、疫苗预约),它把超出承载能力的用户引导到等候页面,而不是让服务器直接崩溃。

Waiting Room 是什么?

核心能力:

  • 实时更新预计等待时间
  • 通过 Cookie 记录排队位置,防止刷新后丢失
  • 无需修改应用代码,Dashboard 填 5 个字段即可启用
  • 支持自定义 HTML/CSS 等候页面

工作原理

底层架构

Waiting Room 本质上是运行在 Cloudflare Workers 上的边缘程序,覆盖全球 300+ 节点:

1
用户请求 → Cloudflare Edge → Waiting Room Worker → 放行 / 排队

两个核心阈值

参数 含义
Total Active Users 同时允许在源站上的最大并发用户数
New Users Per Minute 每分钟允许新进入源站的用户上限

任意一个触及上限,新用户就开始排队。

全球状态同步

各数据中心不依赖中央节点(避免延迟),通过 Cloudflare Durable Objects 构建的聚合管道在秒级完成全球同步。

Session Duration 的双重作用

Session Duration 不只是”多久踢出用户”,它还驱动了动态出流:当用户 Cookie 因不活跃过期时,空位立即释放,等待时间更短、更准确。

与 Pages + Workers 的组合架构

三者都运行在同一个边缘网络,请求链路不需要离开 Cloudflare,延迟极低:

1
2
3
4
5
用户
└→ Waiting Room Worker(判断放行 / 排队)
├→ Cloudflare Pages(页面渲染,SSR / 静态 + CDN)
└→ Workers(API、鉴权、A/B 测试、边缘逻辑)
└→ KV / D1 / R2 / Durable Objects(数据层)

Waiting Room 只负责”放不放行”,放行后完全不干涉后续流程,架构上非常干净。

Pages Functions vs 独立 Worker

一句话原则:逻辑是否只属于这个 Pages 站点?

Pages Functions

  • 与 Pages 项目同仓库,functions/ 目录按文件自动路由
  • 适合:页面级鉴权 middleware、SSR、请求改写、A/B 测试
  • 不支持 Cron、Queue 等独立触发器

独立 Worker

  • 独立仓库 + wrangler.toml,独立部署和版本管理
  • 适合:跨站点共享的 API、复杂业务逻辑、Cron 定时任务
  • 可被 Pages Functions 通过 Service Binding 零延迟调用

最常见的搭配

1
2
3
Pages Functions  →  鉴权 / 请求改写 / SSR
↓ Service Binding(零网络开销,同进程调用)
独立 Worker → 业务 API / 数据读写 / 复杂计算

两者不互斥,Service Binding 让你把复杂逻辑抽到独立 Worker,同时保持 Pages Functions 的部署简便性。

Waiting Room Bypass 规则

内部系统调用、VIP 用户、健康检查、静态资源不应该被排队。Cloudflare 提供了基于 Wirefilter 表达式的 Waiting Room Rules

配置路径: Traffic → Waiting Room → Rules 标签页

1
2
3
4
5
6
7
8
9
10
11
12
# 内部 IP / VPN
ip.src in {10.0.0.0/8 172.16.0.0/12 192.168.0.0/16}

# 健康检查 / 监控 Bot
http.user_agent contains "HealthChecker" or
http.user_agent contains "UptimeRobot"

# 静态资源(JS/CSS/图片)
http.request.uri.path matches ".*\\.(js|css|png|svg|woff2|ico)$"

# Worker 内部调用(见下文安全方案)
http.request.headers["x-bypass-verified"] == "1"

注意:规则按顺序执行,首条匹配即停止。把范围窄、优先级高的规则放前面。

HMAC 签名 Bypass Token:生产级安全方案

明文 token(如 vip_pass=abc123)一旦泄露就永久有效,且无法追溯是谁在使用。用 HMAC-SHA256 签名 + 过期时间戳可以解决这两个问题。

Token 格式

1
2
{userId}.{expireAt}.{hmac_hex}
示例:u123.1735000000.a3f9c2d1e4b8...

生成端(调用方 Worker)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async function generateBypassToken(
userId: string,
env: { BYPASS_SECRET: string }
): Promise<string> {
const expireAt = Math.floor(Date.now() / 1000) + 300 // 5 分钟有效期
const payload = `${userId}.${expireAt}`

const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(env.BYPASS_SECRET),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
)

const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload))
const sigHex = Array.from(new Uint8Array(sig))
.map(b => b.toString(16).padStart(2, "0"))
.join("")

return `${payload}.${sigHex}`
}

const token = await generateBypassToken("u123", env)
const res = await fetch("https://example.com/api/order", {
headers: { "x-bypass-token": token }
})

验证端(Waiting Room 前的 Worker 或 Pages Middleware)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
async function verifyBypassToken(
token: string,
env: { BYPASS_SECRET: string }
): Promise<{ valid: boolean; userId?: string }> {
const parts = token.split(".")
if (parts.length !== 3) return { valid: false }

const [userId, expireAtStr, receivedSig] = parts
const expireAt = parseInt(expireAtStr)

if (Math.floor(Date.now() / 1000) > expireAt) return { valid: false }

const payload = `${userId}.${expireAtStr}`
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(env.BYPASS_SECRET),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
)
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload))
const expectedSig = Array.from(new Uint8Array(sig))
.map(b => b.toString(16).padStart(2, "0"))
.join("")

if (!timingSafeEqual(expectedSig, receivedSig)) return { valid: false }

return { valid: true, userId }
}

// 时间恒定比对:防止攻击者通过响应时间差逐字符猜签名
function timingSafeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false
let diff = 0
for (let i = 0; i < a.length; i++) {
diff |= a.charCodeAt(i) ^ b.charCodeAt(i)
}
return diff === 0
}

SECRET_KEY 管理

1
2
3
4
5
# 生成随机密钥
openssl rand -hex 32

# 两端(生成侧和验证侧)都执行,输入同一个随机字符串
wrangler secret put BYPASS_SECRET

Secret 加密存储在 Workers 环境变量中,不出现在代码仓库里。

三个关键安全属性

属性 机制
有效期 过期时间戳内嵌在 payload,泄露后自动失效
可追溯 userId 内嵌在 payload,知道是谁在使用
不可伪造 没有 SECRET_KEY 无法构造合法签名

为什么需要 timingSafeEqual?

普通 === 比对在签名不匹配时会提前返回,攻击者可以通过测量响应时间差,逐字符猜出正确签名。timingSafeEqual 用异或累积差值,始终跑完全部字符再判断,消除了这个侧信道。

Token 放在哪里?

  • Worker 内部调用 → 放 HTTP Header,干净、不可见
  • 浏览器端携带 → 放 HttpOnly Cookie,JS 不可读
  • 永远不要放 URL query string → 会出现在服务器日志、Referer 头、浏览器历史里

快速参考

场景 方案
内部系统调用绕过排队 HMAC 签名 token + header
VIP 用户绕过排队 HMAC token 签发后存入 HttpOnly Cookie
健康检查绕过 User-Agent 规则匹配
静态资源绕过 URI path 正则匹配
页面逻辑 Pages Functions
跨服务 API / Cron 独立 Worker + Service Binding
边缘数据存储 KV(简单 K/V)/ D1(SQL)/ R2(对象存储)
  • 本文标题:Cloudflare Waiting Room 完整指南:从原理到生产级安全实践
  • 本文作者:乔文飞
  • 创建时间:2026-05-20 08:00:00
  • 本文链接:http://www.feidom.com/2026/05/20/cloudflare-waiting-room-guide/
  • 版权声明:本博客所有文章为作者学习笔记,有转载其他前端大佬的文章。转载时请注明出处。