H5 站刚迁完,HTTPS 正常,接口正常,页面也正常。接下来要接微信,第一道门槛是「业务域名」。
业务域名,是微信允许你在微信内置浏览器里直接打开的域名白名单。不在白名单里的域名,用户在微信里点开就会被微信接管,跳到它自己的一张提示页。要让微信相信这个域名归你管,它给的办法是给你一个随机文件名和一段随机内容,让你放到「网站根目录」,然后微信的服务器自己来请求一次,比对返回的内容。
这次微信给的是新版校验形式。
文件名是一串随机的 32 位十六进制字符串,加 .txt,旧版是 MP_verify_xxxx.txt。文件内容是一段随机的 40 位十六进制字符串。部署位置,微信的说法是「网站根目录」。校验方式是微信服务器去请求 https://<你的域名>/<校验文件名>.txt,然后比对响应体。
麻烦就在这个「网站根目录」上。我这台的根目录,根本不在我能随手 touch 的地方。
text
微信服务器
└─> https://h5.example.com/<校验文件名>.txt
└─> DNS → 宿主 443
└─> 1Panel 的 OpenResty 容器(listen 443,server_name h5.example.com) ← 请求先到这儿
└─> location ^~ / → proxy_pass http://127.0.0.1:<端口>
└─> H5 容器(nginx:80,静态根 /usr/share/nginx/html/)
H5 是跑在容器里的静态站点,前面还挡着一层 1Panel 的 OpenResty。同机又跑着 6 个别人的站点,面板配置动错了是要出事故的。
微信在问的其实是:谁能控制这个域名的 HTTP 响应。
在这一层拓扑里,答案是反代配置,不是文件系统。只要我能让 OpenResty 对 GET /<校验文件名>.txt 返回那 40 个字节,微信就满意了。它不管这 40 个字节来自磁盘上的一个文件,还是来自配置里的一行字符串。
落地方式我数过,有四种,而且差别不小。
直接让 nginx return 文本最短,不用建文件,零路径,跟容器完全解耦,三个站点通用,换文件名时改一次配置就行。指向文件那条要建文件,配置里必须写容器内路径,同样跟容器解耦、三个站点通用,只是换文件名得同时改配置和换文件,多一步。
容器直投就是 docker cp 进容器,见效最快,可容器一重建文件就没了,而且只有那个有静态服务的站能这么做,多个站点得逐个 cp。
我选了第一种。理由不是它最短,而是它把「域名归属」这件事用配置直接答完了:不碰文件系统、不碰容器、也不依赖镜像构建。
配置加在该站点的 server 块里。
nginx
# 微信业务域名校验(2026-09-22)
location = /<校验文件名>.txt {
default_type text/plain;
return 200 '<校验内容>';
}
三个站点各贴同样一段。微信的业务域名校验是按域名逐个发起的,哪个域名在微信里被拦了,就给哪个加,内容一模一样。
写这段的时候有几个点,是我从实测和文档里抠出来的。
必须精确匹配。漏掉 =,写成 location /<校验文件名>.txt,会被 location ^~ / 先命中,请求反代给后端,回 404。= 后面留个空格,官方语法是 location [ = | ~ | ~* | ^~ ] uri,=/路径 解析器也认,但没必要冒这个险。
末尾不带换行。return 输出的是字符串本体,不含末尾换行,正好 40 字节,跟微信期望的严格对上。
不需要 root。return 在 rewrite 阶段就返回了,后面的 root、try_files 根本不执行。
default_type 让 Content-Type 是 text/plain,微信只看 body,写不写都能过,显式写下来更明确。位置放哪儿都行,精确匹配 = 优先级最高,server 块内放哪都不会被 location ^~ / 吃掉。
分号别漏,return 200 '...';。
改完之后,面板保存会自动 reload,手工改的话先 nginx -t 再 nginx -s reload。
验证的时候别省后三条。
bash
# ① 内容与微信的请求完全一致
curl -s https://h5.example.com/<校验文件名>.txt; echo
# 期望:<校验内容>
# ② 字节数与 Content-Type
curl -s https://h5.example.com/<校验文件名>.txt | wc -c # 期望 40
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" \
https://h5.example.com/<校验文件名>.txt # 期望 200 text/plain
# ③ 确认主站没被改坏(同机有别人的站点,这条必须做)
curl -sI https://h5.example.com/ | head -1 # 期望 200
curl -s -H 'Host: api.example.com' http://127.0.0.1/api/v1/products | head -c 60; echo
# ④ 确认配置真的加载了(这条最有价值)
docker exec <面板容器> nginx -T 2>&1 | grep -A3 <校验文件名>
写完这段配置我心里其实是有点虚的。改的是整台机器上所有站点共用的反代。所以动配置之前先做了一次备份,而且把备份验了一遍,列得出全部域名才算成立。
bash
BK=<备份目录>; TS=$(date +%Y%m%d_%H%M%S); mkdir -p "$BK"
find /opt/1panel -name "*.conf" -type f 2>/dev/null | tar -czf "$BK/openresty_all_$TS.tar.gz" -T -
tar -tzf "$BK/openresty_all_$TS.tar.gz" | grep -E "example|<他人域名A>|<他人域名B>" # 列得出域名才算备份成立
这里我踩过一次。早先那条备份命令写成 find ... | head -1,抓到的是项目自带的 conf.d。打出来一个 1.5K 的包,里面不含任何站点配置,等于没备份。
备份这件事,不验证就等于没做。
这 4 行配置能成立,靠的是 nginx 的匹配顺序。nginx 处理请求时,location 的匹配顺序是:精确匹配 = 先,然后是前缀匹配 ^~,再是正则,最后是普通前缀。1Panel 生成的站点配置,主入口是 location ^~ /,它把所有请求都反代给后端容器。
而 location = /xxx.txt 是精确匹配,优先级最高。命中之后,return 在 rewrite 阶段就把请求结束了,proxy_pass 属于后面的 content 阶段,压根没机会执行。
这就是零文件的物理基础。
text
微信服务器 ---> OpenResty(精确匹配命中 ---> return 200)
│
✗ 不往下走:不查磁盘、不反代、不进容器
↓
(后端容器完全无感)
不需要文件,不需要 root,不需要 docker cp,也不需要重新构建镜像。H5 容器以后换镜像、换版本、重建,全部无感。
要是漏掉 = 会怎样?
写成 location /<校验文件名>.txt 就是前缀匹配,按优先级排在 location ^~ / 后面。^~ 先命中,请求被反代给 H5 容器,容器里没这个文件,于是 404。
我记得这个 404 特别迷惑。语法检查 nginx -t 是通过的,curl 站点首页也是正常的,只有校验这一个路径失败。这是这条路上最常见的翻车点。
那内容为什么必须严格 40 字节?
微信比对的是响应体。return 输出字符串本体,不带末尾换行;echo 会带。所以走文件路线的话,得这么写:
bash
printf '%s' '<校验内容>' > <目录>/<校验文件名>.txt
wc -c <目录>/<校验文件名>.txt # 期望 40
printf '%s' 和 echo 差的那一个字节,就是校验通过与校验失败的距离。
指向文件那条我没选,还有一个原因:配置里的 root 必须写容器内路径。OpenResty 本身是个容器,挂载关系是宿主 /opt/1panel/www 对应容器 /www。写成宿主路径 /opt/1panel/www/sites/...,nginx 在容器里找不到,请求变成 404。而 nginx -t 照样通过,非常隐蔽。
容器直投那条用 docker cp 把文件塞进 H5 容器的静态根,大约 30 秒就能见效,但它只对有静态文件服务的那个站有效,而且容器一重建就没了。要想持久,就得把文件放进前端的 public/ 再 commit,等于把运营期的临时校验写进了代码仓库。
微信重新校验时会换一套文件名和内容,这时候几条路的代价就不一样了。
直接 return 文本,改一次配置,换掉 location 的文件名和 return 的内容,大约 30 秒。
指向文件加精确匹配,改配置换文件名,再换一个新文件。
指向文件加正则匹配,最省事,什么都不用改,把新文件丢进那个目录就行------前提是主 location 不是 ^~。
容器直投最麻烦,重新 cp 一遍,要持久还得改代码、重建镜像。
正则写法是 location ~ ^/[0-9a-f]{32}\.txt$ 这种,只在主 location 是 location / 的时候才可用。面板生成的是 ^~ /,^~ 优先级高于正则,会先把请求吃掉。动手前先确认一下:
docker exec <面板容器> nginx -T 2>&1 | grep -E "^\s*location" | sort -u
检查项我挨个过一遍。
备份做了没做而且验没验过,用 tar -tzf 去 grep 域名,列得出域名才算。语法通没通过,docker exec <面板容器> nginx -t,要回 syntax is ok。
配置真加载了没,nginx -T 2>&1 | grep -A3 <校验文件名>,要回显出那段 location。
路径返回的内容对不对,curl -s https://h5.example.com/<校验文件名>.txt,要和微信给的一致。接上 wc -c 要是 40。状态码和类型对不对,curl -s -o /dev/null -w "%{http_code} %{content_type}",要是 200 text/plain。
主站有没有被改坏,我用 curl -sI https://h5.example.com/ | head -1 看,要 200,接口要回 JSON。同机有别人的站点,这条必须做。站点基线变没变,用多域名 Host 探测,要和改动前一致。其余站点加没加,把上一条换个域名重跑,同样要返回内容。
平台侧提交校验,回公众平台点提交。微信里访问还拦不拦,用微信打开 H5,要能正常显示。若微信那个页面已经超时,回公众平台重新发起一次就行,已经部署好的内容不会失效。
微信问的是「谁控制这个域名的响应」,我能答这个问题的那一层是反代配置,跟文件系统没关系。精确匹配 = 是这套写法成立的前提,漏掉它就会静默 404;return 不带换行,所以是严格 40 字节。备份不验证等于没备份,tar -tzf 列得出域名才算成立。
写完之后我又想了一遍那个 1.5K 的假备份。要是当时没顺手 tar -tzf 验一下,我可能就带着它去改整台机器的反代了。