H5 站刚迁完,HTTPS 正常,接口正常,页面也正常。接下来要接微信,第一道门槛是「业务域名」。
业务域名,是微信允许你在微信内置浏览器里直接打开的域名白名单。不在白名单里的域名,用户在微信里点开就会被微信接管,跳到它自己的一张提示页。要让微信相信这个域名归你管,它给的办法是给你一个随机文件名和一段随机内容,让你放到「网站根目录」,然后微信的服务器自己来请求一次,比对返回的内容。
这次微信给的是新版校验形式。
文件名是一串随机的 32 位十六进制字符串,加.txt,旧版是MP_verify_xxxx.txt。文件内容是一段随机的 40 位十六进制字符串。部署位置,微信的说法是「网站根目录」。校验方式是微信服务器去请求https://<你的域名>/<校验文件名>.txt,然后比对响应体。
麻烦就在这个「网站根目录」上。我这台的根目录,根本不在我能随手 touch 的地方。
微信服务器 └─> 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 块里。
# 微信业务域名校验(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。
验证的时候别省后三条。
# ① 内容与微信的请求完全一致curl-shttps://h5.example.com/<校验文件名>.txt;echo# 期望:<校验内容># ② 字节数与 Content-Typecurl-shttps://h5.example.com/<校验文件名>.txt|wc-c# 期望 40curl-s-o/dev/null-w"%{http_code} %{content_type}\n"\https://h5.example.com/<校验文件名>.txt# 期望 200 text/plain# ③ 确认主站没被改坏(同机有别人的站点,这条必须做)curl-sIhttps://h5.example.com/|head-1# 期望 200curl-s-H'Host: api.example.com'http://127.0.0.1/api/v1/products|head-c60;echo# ④ 确认配置真的加载了(这条最有价值)dockerexec<面板容器>nginx-T2>&1|grep-A3<校验文件名>写完这段配置我心里其实是有点虚的。改的是整台机器上所有站点共用的反代。所以动配置之前先做了一次备份,而且把备份验了一遍,列得出全部域名才算成立。
BK=<备份目录>;TS=$(date+%Y%m%d_%H%M%S);mkdir-p"$BK"find/opt/1panel-name"*.conf"-typef2>/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 阶段,压根没机会执行。
这就是零文件的物理基础。
微信服务器 ---> OpenResty(精确匹配命中 ---> return 200) │ ✗ 不往下走:不查磁盘、不反代、不进容器 ↓ (后端容器完全无感)不需要文件,不需要root,不需要docker cp,也不需要重新构建镜像。H5 容器以后换镜像、换版本、重建,全部无感。
要是漏掉=会怎样?
写成location /<校验文件名>.txt就是前缀匹配,按优先级排在location ^~ /后面。^~先命中,请求被反代给 H5 容器,容器里没这个文件,于是 404。
我记得这个 404 特别迷惑。语法检查nginx -t是通过的,curl站点首页也是正常的,只有校验这一个路径失败。这是这条路上最常见的翻车点。
那内容为什么必须严格 40 字节?
微信比对的是响应体。return输出字符串本体,不带末尾换行;echo会带。所以走文件路线的话,得这么写:
printf'%s''<校验内容>'><目录>/<校验文件名>.txtwc-c<目录>/<校验文件名>.txt# 期望 40printf '%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验一下,我可能就带着它去改整台机器的反代了。