From a8a497b4cea0c41e27ca4ce7911978df985f5440 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Mon, 20 Jul 2026 17:20:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(gateway):=20=E6=94=AF=E6=8C=81=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E5=9F=9F=E5=90=8D=E6=A0=A1=E9=AA=8C=E6=96=87=E4=BB=B6?= =?UTF-8?q?=EF=BC=88=E4=B8=BA=E7=BD=91=E9=A1=B5=E6=8E=88=E6=9D=83=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E9=93=BA=E8=B7=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 微信公众平台配置「JS接口安全域名 / 网页授权域名」时会下发 MP_verify_xxx.txt, 要求能从域名根目录直接访问。文件放宿主机 /home/workspace/wechat-verify, 只读挂进容器(不进镜像、不进 git —— 它随时可能重发,且属站点凭证类文件)。 未设 WECHAT_VERIFY_DIR 时这段完全不生效。 **第一版写成了独立路由 `/:mpfile`,直接把官网打挂**:它匹配所有单段路径, 于是 /pricing、/download 全变 404(实测确认)。改为并进 NoRoute 的 SPA 兜底里, 在"已排除 /api/ 与内嵌静态文件"之后、回退 index.html 之前处理。 文件名白名单:必须 MP_verify_ 前缀 + .txt 后缀、不含路径分隔符与 .., 否则落 SPA 兜底 —— 避免把挂载目录变成任意文件下载口子。 回归验证:/ /pricing /download /admin 均 200,校验文件取到正确内容, /passwd.txt 与路径穿越都只拿到 index.html。 Co-Authored-By: Claude Opus 4.8 --- deploy/132-app/docker-compose.yml | 5 +++++ sundynix-gateway/internal/router/router.go | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/deploy/132-app/docker-compose.yml b/deploy/132-app/docker-compose.yml index 185e36b..e9a9754 100644 --- a/deploy/132-app/docker-compose.yml +++ b/deploy/132-app/docker-compose.yml @@ -31,12 +31,17 @@ services: ADMIN_USER_IDS: ${ADMIN_USER_IDS:-} CORS_ALLOW_ORIGIN: ${CORS_ALLOW_ORIGIN:-*} OTEL_EXPORTER_OTLP_ENDPOINT: http://192.168.100.128:4318 + # 微信域名校验文件目录(容器内路径);未设则该路由不注册 + WECHAT_VERIFY_DIR: /etc/sundynix/wechat-verify ports: ["3000:8080"] # frp 外网 → 132:3000 → 容器 8080 volumes: # 微信支付证书(商户私钥 + 微信支付公钥):宿主机 132 的目录只读挂进容器。 # ⚠️ admin「系统配置 → 支付」里填的路径必须是**容器内路径**(/etc/sundynix/wechat-cert/...), # 不是宿主机路径——容器看不到宿主机的 /home/workspace/...。私钥不进镜像、不进 git。 - /home/workspace/wechat-pay-cert:/etc/sundynix/wechat-cert:ro + # 微信域名校验文件(MP_verify_xxx.txt):配置 JS安全域名/网页授权域名时微信要求 + # 能从域名根目录访问到。放宿主机、只读挂进来,不进镜像也不进 git。 + - /home/workspace/wechat-verify:/etc/sundynix/wechat-verify:ro dispatcher: build: { context: ../.., dockerfile: sundynix-dispatcher/Dockerfile } diff --git a/sundynix-gateway/internal/router/router.go b/sundynix-gateway/internal/router/router.go index 12fdef6..f858cf4 100644 --- a/sundynix-gateway/internal/router/router.go +++ b/sundynix-gateway/internal/router/router.go @@ -7,6 +7,7 @@ import ( "log" "net/http" "os" + "path/filepath" "strings" "github.com/gin-gonic/gin" @@ -195,6 +196,20 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob. c.JSON(http.StatusNotFound, gin.H{"error": "接口不存在"}) return } + // 微信域名校验文件(MP_verify_xxx.txt):配置 JS安全域名/网页授权域名时微信要求 + // 能从域名根目录直接访问。文件放 WECHAT_VERIFY_DIR(容器里挂宿主机目录), + // 不进镜像也不进 git。 + // 放在 NoRoute 里而不是注册 /:mpfile —— 后者会匹配**所有单段路径**, + // 把官网的 /pricing、/download 全变成 404(实测踩过)。 + if dir := os.Getenv("WECHAT_VERIFY_DIR"); dir != "" { + name := strings.TrimPrefix(p, "/") + // 只放行这一种文件名,且不含路径分隔符,避免变成任意文件下载口子 + if strings.HasPrefix(name, "MP_verify_") && strings.HasSuffix(name, ".txt") && + !strings.ContainsAny(name, `/\`) && !strings.Contains(name, "..") { + c.File(filepath.Join(dir, name)) + return + } + } if rel := strings.TrimPrefix(p, "/"); rel != "" { if _, statErr := fs.Stat(adminDist, rel); statErr == nil { adminServer.ServeHTTP(c.Writer, c.Request) // /assets/* 等真实文件