ngrok 是一款提供安全内网穿透能力的隧道工具,通过在本地主机与公网边缘节点之间建立双向 TLS 反向连接,使公网第三方服务能够直接访问处于局域网或 NAT 后的本地服务。在支付回调、事件通知等 Webhook 场景中,ngrok 结合其实时流量审查与请求重放功能,能够解决本地开发环境下外部回调无法触达与重复触发成本高的问题。
官方文档与参考链接:ngrok 官方文档 与 Creem 开发者文档。
在现代 Web 应用开发中,涉及支付订单状态同步、退款结算、订阅生命周期管理等业务时,第三方支付服务平台(如 Creem)普遍采用 Webhook 机制,通过异步 HTTP POST 请求将事件推送至开发者指定的服务端端点。针对本地私网环境无法接收公网回调的问题,Creem 官方文档在本地 Webhook 调试指南中同样推荐使用 ngrok 构建反向代理隧道。
在本地开发阶段,开发机通常处于内部局域网(LAN)或网络地址转换(NAT)路由之后,分配的是私有 IP 地址(如 192.168.x.x 或 127.0.0.1):
ngrok 采用反向代理(Reverse Proxy)与边缘隧道(Edge Tunneling)模型,无需修改本地网络防火墙规则即可建立公网访问链路:
https://xxxx.ngrok-free.app),由其负载均衡网关统一终结外部 HTTPS 流量。http://127.0.0.1:5173)。使用 ngrok 进行联调前,需完成控制台产品模式选择、CLI 客户端安装与身份认证凭证绑定。
完成账号注册后,ngrok 控制台引导页会呈现三个产品选项(Where do you want to begin?),分别对应不同的网络接入模式:
Get started 按钮即可直接进入客户端安装指引与 Authtoken 凭据页面)。根据操作系统类型,使用官方推荐的包管理器执行安装:
在 macOS 环境下使用 Homebrew 安装:
brew install ngrok亦可在 ngrok 下载页 直接获取对应平台架构的预编译二进制压缩包,解压后放置于系统 PATH 目录内即可执行。
ngrok 要求所有隧道连接均需经过身份验证。在官网注册账号后,在控制台的“Your Authtoken”页面获取唯一的凭证令牌。
执行命令写入全局凭据:
ngrok config add-authtoken <YOUR_NGROK_AUTHTOKEN>~/.config/ngrok/ngrok.yml,Windows 位于 %LOCALAPPDATA%\ngrok\ngrok.yml)。ngrok version 打印版本号,若成功输出则表示环境初始化完成。在本地服务启动后,即可利用 ngrok 暴露目标端口并建立网络映射。
假设本地 Web 服务运行于 5173 端口(如 Vite / React Router 开发服务):
启动本地服务(保持运行):
npm run dev在独立终端窗口启动 HTTP 隧道:
ngrok http 5173Session Status:显示为 online 表示隧道已正常连通。Account:当前授权的账户名称及套餐类别。Forwarding:公网访问入口映射关系,例如 https://9a2f-xx-xx-xx-xx.ngrok-free.app -> http://localhost:5173。Web Interface:本地流量监控面板地址,默认为 http://127.0.0.1:4040。在免费或基础账户模式下,如果不加参数直接运行 ngrok http <port>,每次重启进程都会生成一个全新的随机公网域名。这意味着每次调试中断或重启后,均需登录第三方后台修改 Webhook 接收地址。
ngrok 免费层支持每个账号申请并保留 1 个免费静态域名(Static Domain):
在 ngrok 控制台“Domains”菜单中认领免费域名后,通过指定 --url 参数启动隧道:
ngrok http 5173 --url=chosen-domain-name.ngrok-free.app亦可将隧道规则固化至 ngrok.yml 配置文件中:
编辑配置文件:
version: "2"
authtoken: <YOUR_NGROK_AUTHTOKEN>
tunnels:
creem-dev:
proto: http
addr: 5173
domain: chosen-domain-name.ngrok-free.app使用已命名的隧道配置一键启动:
ngrok start creem-devCreem 官方开发者文档在其本地 Webhook 测试说明中,推荐开发者利用 ngrok 等内网穿透工具将本地开发端口暴露至公网,以便在沙箱环境下完整验证支付与退款等事件流。本节以接入 Creem 支付平台为例,展示端到端的配置与验证流程。
在服务端框架中(如 React Router / Express / NestJS),Webhook 接收接口通常需要满足以下要求:
/api/webhooks/creem。以 TypeScript 实现的端点处理逻辑如下:
import type { Route } from "./+types/api.webhooks.creem";
import crypto from "node:crypto";
/**
* 校验 Creem Webhook 签名 (HMAC-SHA256)
*/
function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
if (!signature || !secret) {
return false;
}
const expected = crypto.createHmac("sha256", secret).update(payload).digest("hex");
if (expected.length !== signature.length) {
return false;
}
try {
return crypto.timingSafeEqual(Buffer.from(expected, "utf8"), Buffer.from(signature, "utf8"));
} catch {
return false;
}
}
export async function action({ request }: Route.ActionArgs) {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
// 获取原始未解析的文本载荷
const rawPayload = await request.text();
const signature = request.headers.get("creem-signature");
const webhookSecret = process.env.CREEM_WEBHOOK_SECRET;
if (!webhookSecret || !signature || !verifyWebhookSignature(rawPayload, signature, webhookSecret)) {
return new Response("Invalid signature", { status: 400 });
}
let event: any;
try {
event = JSON.parse(rawPayload);
} catch {
return new Response("Bad request", { status: 400 });
}
const eventType = event.event_type || event.eventType;
// 根据事件类型分发业务处理
switch (eventType) {
case "checkout.completed":
// 处理支付成功与履约逻辑
break;
case "refund.created":
// 处理退款结算逻辑
break;
case "dispute.created":
// 处理银行争议逻辑
break;
default:
break;
}
return new Response("OK", { status: 200 });
}express.json() 中间件的框架,必须针对 Webhook 路由单独配置 express.raw({ type: 'application/json' }),避免请求体被反序列化后再通过 JSON.stringify 还原导致空格或键顺序微变,进而使签名验证失败。在本地隧道与开发服务启动后,将公网地址登记至 Creem 管理控制台:
https://<YOUR-STATIC-DOMAIN>.ngrok-free.app/api/webhooks/creem。Developers -> Webhooks 模块。Add Endpoint 或编辑现有端点,填入上述完整的公网 HTTPS URL。checkout.completed、refund.created、dispute.created)。Webhook Signing Secret。将获得的密钥同步至本地开发环境配置文件:
在本地 .env 文件中配置:
# Creem 身份与 Webhook 签名凭据
CREEM_API_KEY=creem_test_xxxxxx
CREEM_WEBHOOK_SECRET=whsec_xxxxxxngrok 的终端以及本地 Web 服务日志,确认收到来自 Creem 的 POST 回调,并返回 HTTP 200 响应。Webhook 联调中最耗时的环节是每次修改处理代码后,都需要在第三方控制台重新发起一次结账或退款操作。ngrok 自带的本地 Inspect 面板提供了请求捕获与免重复下单重放能力。
只要 ngrok 处于启动状态,即可在浏览器直接访问:
http://127.0.0.1:4040该控制台提供了实时流量抓包视图:
/api/webhooks/creem)、响应状态码(200、400、500)与耗时。creem-signature、content-type)与原始 Raw Payload。通过本地 Web 界面可直接执行重放操作,加速开发迭代:
http://127.0.0.1:4040 中选中任意一条已接收的历史 Webhook 请求,点击右上角的 Replay 按钮,ngrok 将携带完全一致的请求头与请求体重新向本地服务投递一次。metadata、模拟非法签名),验证服务端的边界异常防御能力。查询本地所有活跃隧道信息:
curl -s http://127.0.0.1:4040/api/tunnels获取捕获的历史请求列表:
curl -s http://127.0.0.1:4040/api/requests/http将本地服务暴露给公网虽然极大降低了联调门槛,但在工程规范与安全方面必须执行严格管控。
在接收公网 Webhook 请求时,不得因处于本地调试阶段而跳过或注释签名校验代码:
crypto.timingSafeEqual,避免直接使用 === 进行字符串等值判断,规避基于时序侧信道的字符匹配推断攻击。.env 中的 CREEM_WEBHOOK_SECRET 与 Creem 控制台中为该端点配置的密钥保持同步。CREEM_WEBHOOK_SECRET、CREEM_API_KEY 以及 ngrok.yml 中的 authtoken 均包含高度敏感权限,禁止提交至 Git 版本控制系统。Ctrl + C 关闭隧道,切断外网向本地端口的连接通道。