跳过内容

JSON 格式外链数据:$100 免费额度,无需绑卡,支持 Google 账号登录。

SEOMasters.dev
SEOMasters.dev注册

Next.js 中的 Web 反向链接检查器,密钥保留在服务器上

CORS 已开放,因此浏览器可以直接调用 API。使用付费密钥直接调用是一个错误。本页面展示了修复该问题的轻量级服务器层。

装甲电缆将玻璃房屋连接到带有重型锁的地下服务器机房

误区与实际情况

误区现实
"CORS 开放意味着我可以将密钥打包到 React 中。"CORS 开放意味着调用可以成功。任何人都仍可以从网络选项卡中读取密钥并消耗你的余额。
"表格必须在客户端渲染,因为它是仪表板。"在服务器端获取数据并发送 HTML。用户首次渲染时即可看到行数据,且密钥不会离开服务器。
"免费操作也需要保护。"domain、overview、stats 和 suggest 不需要密钥,因此浏览器可以直接调用它们。
"我应该在每次页面查看时重新获取数据。"索引大约每月重建一次。24 小时缓存不会丢失任何数据。
"代理应该转发客户端发送的所有内容。"仅允许使用你需要的操作。转发的 action=search 将消耗 3 个积分。

路由处理程序

App Router,文件 app/api/links/route.ts。环境变量不会进入客户端包,因为它没有 NEXT_PUBLIC_ 前缀。

ts
import { NextRequest, NextResponse } from "next/server";

const ALLOWED = new Set(["refdomains", "anchors", "toppages"]);

export async function GET(req: NextRequest) {
  const p = req.nextUrl.searchParams;
  const action = p.get("action") ?? "";
  const q = (p.get("q") ?? "").toLowerCase();
  if (!ALLOWED.has(action) || !/^[a-z0-9.-]+$/.test(q)) {
    return NextResponse.json({ ok: false, error: "bad request" }, { status: 400 });
  }
  const url = `https://seomasters.dev/api.php?action=${action}&q=${q}&per=100`;
  const upstream = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.SEO_API_KEY}` },
    next: { revalidate: 86400 },
  });
  if (upstream.status === 402) {
    return NextResponse.json({ ok: false, error: "data temporarily unavailable" }, { status: 503 });
  }
  return new NextResponse(upstream.body, { status: upstream.status, headers: { "content-type": "application/json" } });
}

为什么处理程序看起来是这样的

操作白名单

UI 中的 bug 不应意外触发 3 个积分的 search 调用。

q 上的正则表达式

只有裸主机名能通过。没有方案、路径或查询字符串被 smuggled 到上游。

revalidate: 86400

每个域名每天一次上游调用。对于月度数据,更长的时间也可以。

402 映射为 503

你的访客不需要知道你的余额已空。你需要知道,所以请记录它。

徒步旅行者走过石桥,无人机在雾谷上空盘旋

渲染:爬虫和访客看到的内容

如果此仪表板位于登录之后,服务器或客户端渲染是一个用户体验选择。如果其中任何部分是公开的,例如你想要被索引的免费查询页面,请在服务器端渲染第一个表格。

一个以空壳形式到达并在水合后填充的页面,对索引和慢速手机都是一个风险。Google 可以执行 JavaScript,但它会排队该步骤,并不总是能迅速完成。

将免费概览保留在服务器端

将便宜、免费的 overview 调用放在服务器组件中,让访客通过按钮触发付费的更深层次拉取。这种分离也使你的积分消耗可预测。

免费调用不花费任何积分,因此服务器渲染的概览不会增加积分消耗。只有付费拉取需要按钮和会话。

首先考虑空状态。访问空白表格的访客会认为工具已损坏,而快速的第二次渲染无法消除这种第一印象。

为第一屏选择免费操作,为深入查看选择付费操作。访问者打开页面看到免费摘要后,就没有理由意外点击付费按钮。

链接解释,而不仅仅是数字

连接数字是容易的部分。解释它们更难,因此从工具提示链接 DR 公式,并提及索引 每月刷新。

看到没有公式的评分的访客会猜测它,因此链接应位于屏幕上数字的旁边。

工匠在木制隔断后的锁抽屉旁拿着一把黄铜钥匙

仪表板的服务器端和客户端部分

大多数仪表板都需要这两种代码。所有权决定分离。密钥、数据和访问规则各有其归属。公开的 Web 反向链接检查器页面与登录后页面需要相同的分离。

服务器端获取,客户端排序

当用户排序、过滤和分页时,反向链接表格是一个合理的客户端组件。行数据应来自已经调用过你路由的服务器组件,这样浏览器就不会持有密钥或上游 URL。

在客户端排序十行是即时的。每次重新请求排序五千行则很慢,因此将完整数据集保留在你已加载的组件中。

保持简单。即时排序的表格优于每次点击都重新获取的表格。

将客户端边界保持在低位

尽可能将边界推至树的下层。动态导入的纯客户端小部件在浏览器中渲染,而获取行的组件保留在服务器上。以 props 形式传递行,并将获取保持在边界之上。

如果你将获取移到边界之下,浏览器将自行发起调用。这是密钥泄露部分警告的错误,因此在此边界每次更改后检查网络选项卡。

这条线究竟在哪里?先在纸上画出来,为获取画一个框,为每个交互控件画一个框,并计算跨越它的箭头数量。

外壳和会话检查保留在服务器端

布局和会话检查决定访客能看到什么,因此它们属于服务器端。交互表格、表单和图表可以在之后水合,而不改变这一决定。

客户端守卫可以隐藏按钮,但不能阻止请求。路由处理程序和服务器布局是实际执行访问控制的地方。

然后在不带 cookie 的情况下发送相同请求,并确认服务器拒绝它,而不仅仅是页面。

同一规则也适用于代理本身。仅在浏览器中进行的会话检查无法保护路由,因此路由在消耗积分之前也应检查会话。

按域名缓存,而非按访客缓存

路由根据域名构建上游 URL,因此缓存键也遵循域名。一个访客查询网站会为该网站的下一个访客预热页面。

这就是处理程序中每日重新验证的意义。同一域名的两个访客只消耗一次上游调用,一千个域名的一千个访客消耗一千次调用,因此缓存就是预算。

也要注意相反的情况。通过时间戳改变查询字符串的页面会完全破坏缓存,账单会在日志之前显示出来。

根据索引时间表重新验证。每日窗口适合月度数据,更短的窗口只会增加调用而不会增加新事实。

加固,按我们执行的顺序

处理程序是简单的部分。加固是密钥泄露或余额耗尽的地方,因此按顺序处理这些。

扫描你自己的网络选项卡

在你的仪表板上打开 Chrome DevTools 并阅读网络选项卡。仔细看。如果那里的请求显示 sk_live_,请立即轮换密钥。这个检查只需一分钟,但能发现最常见的泄露。

同时搜索响应体和请求头。错误消息中回显的密钥仍然是泄露。

限制每个用户的调用次数

接下来,添加每用户限制。将其放在路由前面。上面的处理程序为任何能访问它的人消耗积分,因此将其与会话绑定,并限制每小时调用次数。从 积分页面 上的预算数学中选择上限。

从慷慨开始,稍后再收紧。第一天就阻止真实用户的上限会让他们找到另一个工具。

明确谁负责充值

运行手册中还值得加一行:当余额低于设定水平时谁会被通知,以及谁负责充值。明确指定人员。

将充值步骤写为带有账户页面链接和预期周转时间的检查清单。仅存在于一个人头脑中的运行手册在该人休假时就会失效。步骤本身只需几分钟,因此编写文档是更大的工作。

在余额耗尽前记录余额

然后从每个上游响应中记录 _credits 到你的指标中。每日采样的剩余余额图会在 HTTP 402 出现前一周向你发出警告。

根据你的每周使用情况选择警报水平。从模板中选择的数字很少与你的实际调用匹配。

将最后看到的值保留在内存和日志中,以便路由在数字低于警报水平时拒绝付费拉取。这个检查只需一行代码,并能防止突发流量消耗剩余余额。

将白名单保留在一处

另一个习惯会带来回报。将白名单保留在共享常量中,由处理程序和测试导入,这样新操作无法在有人编辑可见列表且审查者注意到之前进入生产环境。

一个文件,一个导入。没有人需要在三个组件中搜索列表以知道代理会接受什么。

测试你部署到的运行时

最后,在 Node 20 或你选择的边缘运行时下运行处理程序并测试两者。如代码所示,流式传输上游主体在每种环境中都有效,但头部处理不同,值得测试。

添加一个使用无效操作调用路由的测试和一个使用有效操作调用路由的测试,并断言状态码。这两个测试可以捕获白名单和头部处理中的大多数回归。

按顺序连接代理

  1. 1

    在服务器端添加密钥

    在无公共前缀的服务器环境下存储密钥,然后重启开发服务器以加载它。

  2. 2

    先编写白名单

    列出你的屏幕实际调用的操作。其他任何操作都会从你的路由返回 400,而不会到达上游 API。

  3. 3

    从服务器组件获取你的路由

    在数据到达的地方渲染行。浏览器只调用你自己的域名,因此上游地址不会出现在页面中。

  4. 4

    监控余额

    记录每个上游响应的 _credits 并设置你可以采取行动的警报水平,以便在 402 以 503 的形式到达访客之前进行处理。