feat(gateway): 支持微信域名校验文件(为网页授权登录铺路)
微信公众平台配置「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 <noreply@anthropic.com>
This commit is contained in:
@@ -31,12 +31,17 @@ services:
|
|||||||
ADMIN_USER_IDS: ${ADMIN_USER_IDS:-}
|
ADMIN_USER_IDS: ${ADMIN_USER_IDS:-}
|
||||||
CORS_ALLOW_ORIGIN: ${CORS_ALLOW_ORIGIN:-*}
|
CORS_ALLOW_ORIGIN: ${CORS_ALLOW_ORIGIN:-*}
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT: http://192.168.100.128:4318
|
OTEL_EXPORTER_OTLP_ENDPOINT: http://192.168.100.128:4318
|
||||||
|
# 微信域名校验文件目录(容器内路径);未设则该路由不注册
|
||||||
|
WECHAT_VERIFY_DIR: /etc/sundynix/wechat-verify
|
||||||
ports: ["3000:8080"] # frp 外网 → 132:3000 → 容器 8080
|
ports: ["3000:8080"] # frp 外网 → 132:3000 → 容器 8080
|
||||||
volumes:
|
volumes:
|
||||||
# 微信支付证书(商户私钥 + 微信支付公钥):宿主机 132 的目录只读挂进容器。
|
# 微信支付证书(商户私钥 + 微信支付公钥):宿主机 132 的目录只读挂进容器。
|
||||||
# ⚠️ admin「系统配置 → 支付」里填的路径必须是**容器内路径**(/etc/sundynix/wechat-cert/...),
|
# ⚠️ admin「系统配置 → 支付」里填的路径必须是**容器内路径**(/etc/sundynix/wechat-cert/...),
|
||||||
# 不是宿主机路径——容器看不到宿主机的 /home/workspace/...。私钥不进镜像、不进 git。
|
# 不是宿主机路径——容器看不到宿主机的 /home/workspace/...。私钥不进镜像、不进 git。
|
||||||
- /home/workspace/wechat-pay-cert:/etc/sundynix/wechat-cert:ro
|
- /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:
|
dispatcher:
|
||||||
build: { context: ../.., dockerfile: sundynix-dispatcher/Dockerfile }
|
build: { context: ../.., dockerfile: sundynix-dispatcher/Dockerfile }
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"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": "接口不存在"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "接口不存在"})
|
||||||
return
|
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 rel := strings.TrimPrefix(p, "/"); rel != "" {
|
||||||
if _, statErr := fs.Stat(adminDist, rel); statErr == nil {
|
if _, statErr := fs.Stat(adminDist, rel); statErr == nil {
|
||||||
adminServer.ServeHTTP(c.Writer, c.Request) // /assets/* 等真实文件
|
adminServer.ServeHTTP(c.Writer, c.Request) // /assets/* 等真实文件
|
||||||
|
|||||||
Reference in New Issue
Block a user