摘要:前端直接调用或代理 AI 模型接口,极易遭遇恶意刷量和额度被偷。本文手把手教你如何用 Cloudflare Workers 配合 KV 数据库,在网络边缘对 IP 和已登录用户设置每日访问限额,并利用 24 小时 TTL 自动清理垃圾缓存,白嫖实现极低延迟的安全防刷。
这两天折腾大模型 API 的朋友肯定多多少少都有过这种体会:本来开开心心地在前端接了个对话界面,想给自己的小网站加点 AI 属性,结果一觉醒来,发现额度被脚本刷爆了,或者直接收到了服务商寄来的高额账单。说实话,我自己之前也遭遇过这种窘境。
天下苦接口防刷久矣。AI 时代的前端应用,如果直接暴露模型接口而没有任何访问频次限制,那就是在拿自己的钱包开玩笑。然而,如果为了限流专门去买一台云服务器部署一套 Redis 加后端,这成本和精力又觉得有点犯不上。
其实,我们完全可以白嫖 Cloudflare 全家桶。今天我就手把手带大家用 Cloudflare Workers 配合 KV 数据库,在网络边缘建立一道防线,对未登录的 IP 以及已登录的 JWT 用户进行每日访问频次限制,整个过程零成本、零服务器,且响应延迟极低。
为什么选择 Workers + KV 限制访问次数
以往我们做频次限制,通常的思路是「Nginx 限流」或者「后端 Node.js / Python 读写 Redis 计数器」。但对于纯静态网页或是全站托管在 Pages 上的无服务器架构来说,为了一个限流专门去买服务器和 Redis 数据库,简直是本末倒置。
使用 Cloudflare Workers 配合 KV 数据库有几个显而易见的优势:
- 零成本白嫖:Workers 每天提供 10 万次免费请求额度,KV 每天提供 10 万次免费读取与 1 千次免费写入额度,对于普通个人站点、测试项目完全够用。
- 边缘极速响应:Workers 和 KV 都是分布式部署在 Cloudflare 全球边缘节点上的,数据不需要回源到特定的服务器,查询和写入都在离访客最近的节点上完成,延迟极低。
- 开发极其简单:十几行 JavaScript 代码就能搞定,没有复杂的环境搭建,配置好
wrangler.toml一键即可发布上线。
实战开发第一步:配置 KV 命名空间
首先,我们需要在 Cloudflare 里面为我们的频次限制创建一个存储空间。大家可以登录控制台,进入「KV」选项卡,点击「新建命名空间」,命名为 AI_RATE_LIMITS。
创建完成后,为了让 Worker 脚本能够读写这个空间,我们需要在项目的 wrangler.toml 配置文件中进行绑定:
kv_namespaces = [
{ binding = "AI_RATE_LIMITS", id = "你创建的KV命名空间实际ID" }
]
这里的 binding 值就是我们在 Worker 代码中可以直接调用的全局变量名称。
实战开发第二步:确定唯一用户标识
要限制次数,就必须能唯一标记一个客户端。在实际的业务场景中,我们一般有两种识别维度:
1. 未登录的普通访客
对于这部分访客,我们通常使用客户端的 IP 地址来进行限流。在 Workers 中,我们可以直接通过请求头获取客户端真实 IP:
const clientIp = request.headers.get("cf-connecting-ip") || "unknown";
2. 已登录的注册用户
如果你的前端应用接入了会员系统,访客是通过 JWT 登录访问的,我们可以解析 Authorization 请求头,从中获取唯一的 userId:
const authHeader = request.headers.get("Authorization");
let userId = null;
if (authHeader && authHeader.startsWith('Bearer ')) {
// 这里简单演示解析 JWT payload 里的 id
const token = authHeader.split(' ')[1];
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
// 如果 JWT 中含有中文,直接 atob 会乱码报错,需按以下方式安全解码
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
const payload = JSON.parse(jsonPayload);
if (payload && payload.id) userId = payload.id;
}
区分这两种身份的好处是,我们可以为不同的角色制定不同的规则。例如:未登录的普通 IP 每天只能问 9 次,而登录用户每天可以问 64 次。既保护了接口不被脚本刷爆,又给正常用户留出了足够的额度。
实战开发第三步:解决北京时间时区问题
既然我们是要限制「每日访问次数」,就必须要判断日期。但这里有个非常容易踩的坑:Cloudflare Workers 运行时的默认时区是 UTC 时间。如果我们直接使用 new Date().toISOString(),会比北京时间晚 8 个小时,这就导致访问额度刷新清零的逻辑和访客感受到的跨天点完全对不上。
为了让刷新时间符合我们的日常作息,我们可以手动加上 8 小时的偏移量,计算出准确的北京时间日期:
const now = new Date();
// 修正为北京时间 (+8 小时)
const bjtTime = new Date(now.getTime() + 8 * 3600 * 1000);
const todayDate = bjtTime.toISOString().split('T')[0]; // 得到格式如 "2026-07-09"
接着,我们将用户标识和日期组合,拼接成在 KV 数据库中存储的唯一 Key:
- 未登录访客 Key:
UNAUTH:${clientIp}:${todayDate} - 已登录会员 Key:
AUTH:${userId}:${todayDate}
实战开发第四步:读写 KV 与自动过期 TTL 设置
有了唯一的 Key 之后,接下来的逻辑就很直观了:
- 每次收到请求,先用
await env.AI_RATE_LIMITS.get(cacheKey)读取当前已访问的次数。 - 如果是第一次请求,次数初始为 0。如果次数已经达到或超过限制,直接中断请求,返回 HTTP 429 状态码及错误信息。
- 如果没有超过限制,就将次数加 1,然后用
put方法写回 KV 数据库。
💡 关键细节:为什么必须设置 expirationTtl 写入参数?
在将数据写回 KV 时,我们必须传入 expirationTtl: 86400 (即 24 小时后自动过期) 参数。因为限流数据是具有很强时效性的,一旦过了今天,旧的数据就完全没有保存的必要了。如果不加 TTL 限制,KV 数据库里的无用 Key 会随着天数和 IP 数量的累积无限膨胀,不仅管理起来头疼,还可能产生额外的存储费用。而设置了 24 小时的生存时间,Cloudflare 就会在后台自动帮我们回收这些垃圾数据,不需要我们编写任何清理脚本,极其优雅。
完整 Workers 代理限流代码
下面是我整理好的一套可以直接复制运行的核心限流逻辑。大家可以直接集成到自己现有的 API 转发 Worker 脚本中:
export default {
async fetch(request, env) {
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
if (request.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
try {
const clientIp = request.headers.get("cf-connecting-ip") || "unknown";
const authHeader = request.headers.get("Authorization");
let userId = null;
if (authHeader && authHeader.startsWith('Bearer ')) {
try {
const token = authHeader.split(' ')[1];
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
const payload = JSON.parse(jsonPayload);
if (payload && payload.id) userId = payload.id;
} catch (e) {
console.error("JWT 解析失败:", e);
}
}
// 获取北京时间 YYYY-MM-DD
const now = new Date();
const bjtTime = new Date(now.getTime() + 8 * 3600 * 1000);
const todayDate = bjtTime.toISOString().split('T')[0];
// 确定限额配置
let limit = 9; // 未登录 IP 默认每日限额 9 次
let limitMsg = "今日免费问答次数已用完,请登录后继续使用。";
let cacheKey = UNAUTH:${clientIp}:${todayDate};
if (userId) {
limit = 64; // 已登录用户每日限额 64 次
limitMsg = "今日您的问答次数已达上限,请明日后再试。";
cacheKey = AUTH:${userId}:${todayDate};
}
// 频次判定
if (env.AI_RATE_LIMITS) {
const currentCountStr = await env.AI_RATE_LIMITS.get(cacheKey);
const currentCount = currentCountStr ? parseInt(currentCountStr) : 0;
if (currentCount >= limit) {
return new Response(JSON.stringify({ error: limitMsg }), {
status: 429,
headers: { "Content-Type": "application/json", ...corsHeaders }
});
}
// 递增计数并写回,设置 24 小时 (86400 秒) 的 TTL 自动回收
await env.AI_RATE_LIMITS.put(cacheKey, (currentCount + 1).toString(), { expirationTtl: 86400 });
}
// 限流通过,此处继续执行后续对 AI 模型 API 的请求转发...
return new Response(JSON.stringify({ success: true, message: "频次检测通过,请求已放行" }), {
status: 200,
headers: { "Content-Type": "application/json", ...corsHeaders }
});
} catch (e) {
return new Response(JSON.stringify({ error: e.message }), {
status: 500,
headers: { "Content-Type": "application/json", ...corsHeaders }
});
}
}
};
总结
说白了,利用 Cloudflare Workers 的边缘计算能力配合 KV 数据库,我们不需要花一分钱租用服务器,就能给自己的模型接口加装上一层极低延迟的「防弹衣」。
不过,基于客户端 IP 的限速也存在小小的局限,比如同一个局域网(如公司或学校)下的用户会共用一个公网 IP 出口,可能会导致误伤。但综合成本和开发难度来看,这绝对是初创项目和个人折腾系统里性价比最高的限流方案了。
如有疑问欢迎留言交流,我们下篇见。
加载评论中……