PAYATHON 2026

如何通过 ngrok 向 Django 开发服务器接收 HTTPS webhook

支付阿杰

结论

ngrok 可以在公网接收 HTTPS 请求,解密后再通过 HTTP 转发给 Django,因此 Django 开发服务器不需要启用 HTTPS。

先启动 Django,再将 ngrok 的上游地址设为 HTTP:

python manage.py runserver 8000
./ngrok http 8000

也可以明确指定协议:

./ngrok http http://localhost:8000

不要使用:

./ngrok http https://localhost:8000

这个命令会让 ngrok 使用 HTTPS 连接本地 8000 端口,而 Django 开发服务器只接受 HTTP。协议不匹配时,会出现 TLS 握手失败,或类似“向 HTTP 服务发送了 HTTPS 请求”的错误。

请求是如何转发的

正确配置后,请求会沿着下面的链路转发:

Payment Service
    │
    │ HTTPS
    ▼
https://xxxx.ngrok.io/paymentNotification
    │
    │ ngrok 终止 TLS,并转发为 HTTP
    ▼
http://localhost:8000/paymentNotification
    │
    ▼
Django development server

公网连接使用 HTTPS,不代表 ngrok 也要通过 HTTPS 连接本地服务。ngrok http 8000 中的 http 表示创建 HTTP 隧道,不是说公网端只能使用 HTTP。ngrok 通常会同时提供 HTTP 和 HTTPS 公网地址。

启动成功后,映射关系应类似:

Forwarding  http://xxxx.ngrok.io  -> http://localhost:8000
Forwarding  https://xxxx.ngrok.io -> http://localhost:8000

把其中的 HTTPS 地址配置到支付平台:

https://xxxx.ngrok.io/paymentNotification

域名应以 ngrok 实际显示的地址为准,不要手动拼写。还要注意,产品名称和域名通常是 ngrok,不是 ngrock。

配置 Django 路由

例如,可以这样定义 webhook 路由:

# urls.py
from django.urls import path
from .views import payment_notification

urlpatterns = [
    path("paymentNotification", payment_notification, name="payment_notification"),
]

对应的处理函数可以写成:

# views.py
import json

from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt


@csrf_exempt
def payment_notification(request):
    if request.method != "POST":
        return HttpResponse(status=405)

    try:
        payload = json.loads(request.body)
    except json.JSONDecodeError:
        return JsonResponse({"error": "Invalid JSON"}, status=400)

    # 根据支付平台文档验证 webhook 签名。
    # 只有验证成功后,才应更新订单或支付状态。

    print(payload)

    return JsonResponse({"received": True})

第三方支付服务不会携带 Django 的 CSRF token,所以 webhook 端点通常需要豁免 CSRF 检查。但 @csrf_exempt 不能代替安全验证。生产代码必须按照支付平台的规则校验签名、时间戳或共享密钥,否则任何人都可能伪造支付通知。

检查 ALLOWED_HOSTS

ngrok 请求的 Host 头通常包含公网域名。如果 Django 返回 DisallowedHost,需要在开发环境中允许该域名:

# settings.py
ALLOWED_HOSTS = [
    "localhost",
    "127.0.0.1",
    "xxxx.ngrok.io",
]

临时调试时也可以使用:

ALLOWED_HOSTS = ["*"]

不要把这种宽松配置直接用于生产环境。ngrok 的临时域名可能在重启后发生变化,届时需要同时更新支付平台的 webhook URL 和 ALLOWED_HOSTS。

HTTPS 识别问题

TLS 在 ngrok 处终止,因此 Django 收到的本地连接是 HTTP。ngrok 通常会通过 X-Forwarded-Proto 告知上游,原始请求使用的是 HTTPS。

如果代码依赖 request.is_secure(),或者启用了 SECURE_SSL_REDIRECT,可以先确认代理会正确设置该请求头,再添加以下配置:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

这表示当 X-Forwarded-Proto 为 https 时,Django 会把原始请求视为安全连接。只有可信代理能够控制或覆盖该请求头时,才应启用这项配置,否则客户端可能伪造该信息。

本地验证

在交给支付平台调用前,可以先直接测试公网 HTTPS 地址:

curl -i \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"payment_id":"test-123","status":"paid"}' \
  https://xxxx.ngrok.io/paymentNotification

然后逐项检查:

  • Django 终端是否收到了请求;
  • ngrok 的本地检查界面是否显示了该请求;
  • 返回状态是否为 2xx;
  • URL 路径是否完全一致;
  • 支付平台要求的签名校验是否通过。

ngrok 的请求检查界面通常位于:

http://127.0.0.1:4040

通过这个界面可以查看请求头、请求体、响应内容和状态码,方便排查 webhook 问题。

注意路径末尾的斜杠

下面两个 URL 并不相同:

/paymentNotification
/paymentNotification/

如果 Django 路由定义带有末尾斜杠:

path("paymentNotification/", payment_notification)

支付平台也应配置为:

https://xxxx.ngrok.io/paymentNotification/

不要依赖 Django 自动重定向 webhook POST 请求。部分客户端经过重定向后,可能改变请求方法、丢失请求体,或拒绝跟随重定向。

最终只需确认两项配置:公网 webhook 使用 ngrok 提供的 HTTPS 地址,本地上游使用 http://localhost:8000。

备注:内容仅供参考。