162 lines
8.6 KiB
Markdown
162 lines
8.6 KiB
Markdown
# 根路径站点验证文件
|
||
|
||
放在这个目录里的文件,会被 Go 服务直接吐到**域名根路径**,用于各类「域名所有权 / 站点所有权」验证:
|
||
|
||
- 备案与域名所有权验证:`1XXl8fPHr9.txt`(阿里云、腾讯云)
|
||
- 搜索引擎站点验证:`googleXXXX.html`、`msXXXXXXXX.txt`、`BingSiteAuth.xml`
|
||
- 第三方平台域名校验:各平台下发的 `.txt` / `.html`
|
||
|
||
## 为什么需要这个目录
|
||
|
||
线上 nginx 对所有域名都是「全量反代给 Go」(`location /` → `proxy_pass`),
|
||
Go 不认识的路径一律回 beego 404 页,所以放在服务器网站根目录里的验证文件根本取不到。
|
||
改成由 Go 统一托管后,不再依赖 nginx 规则 —— 宝塔重生成 vhost 也不会被冲掉。
|
||
|
||
实现见 `go/routers/verifyfiles.go`(过滤器)与 `go/pkg/verifyfiles/`(路径解析与安全校验)。
|
||
|
||
## 生效规则
|
||
|
||
- 只处理 `GET` / `HEAD`;
|
||
- 只认**根路径下的单段文件名**(如 `/1XXl8fPHr9.txt`),不支持子目录;
|
||
- 文件名必须形如「字母或数字开头 + 字母数字与 `.` `_` `-`」,长度 ≤ 64;
|
||
- 扩展名必须在允许列表内(默认 `.txt` / `.html` / `.htm`);
|
||
- 文件必须真实存在,否则放行给正常路由(最终仍是 beego 404,行为不变)。
|
||
|
||
配置项在 `app.conf`:
|
||
|
||
| 配置 | 说明 | 默认值 |
|
||
|---|---|---|
|
||
| `verify_dir` | 验证文件目录 | `<服务根目录>/verify` |
|
||
| `verify_exts` | 允许的扩展名,逗号分隔 | `.txt,.html,.htm` |
|
||
| `verify_hosts` | 限定生效域名,逗号分隔(不含端口) | 空 = 所有走本服务的域名 |
|
||
|
||
> 「服务根目录」= `conf/app.conf` 所在目录的上一级。
|
||
> 线上即 `/www/wwwroot/api.yunzer.cn`(systemd 的 `WorkingDirectory`),
|
||
> 所以验证文件目录是 `/www/wwwroot/api.yunzer.cn/verify`。
|
||
> 目录跟着 `app.conf` 走,不管用 `go run main.go` 还是编译后的二进制、
|
||
> 不管从哪个目录启动,结果都一致。
|
||
|
||
## 新增一个验证文件
|
||
|
||
1. 把平台给的验证文件放进本目录(文件名保持原样,**内容不要加多余字符**);
|
||
2. `git add && git commit && git push`;
|
||
3. 服务器上 `git pull` 后重启 Go 服务:`systemctl restart go-api`(或 `bash scripts/service.sh restart`);
|
||
4. 访问 `https://api.yunzer.cn/1XXl8fPHr9.txt` 验证。
|
||
|
||
> 注意:本目录下的 `.md` 等非白名单扩展名不会被吐出去,README 不会被当成静态文件泄露。
|
||
|
||
## 部署注意:本仓库(allinone)与服务器不是同一个仓库
|
||
|
||
排查问题时确认过,线上是**两套独立的 git 仓库**,别混用:
|
||
|
||
| | 位置 | 角色 |
|
||
|---|---|---|
|
||
| 源仓库 | 本地 `yunzerwebsiteallinone`(远端 `10.31.100.4:8418/.../yunzerwebsiteallinone.git`) | 唯一真相源,`go/` 只是其中一个子目录 |
|
||
| 部署目录 | 服务器 `/www/wwwroot/api.yunzer.cn` | 只放 `go/` 的内容,里面是**另一个** Go 仓库(HEAD 与 allinone 不一致,例如 `269fbd0`) |
|
||
|
||
所以:
|
||
|
||
- **allinone 的 `master` 与服务器仓库的 `master` 是两条线**,`git pull` 拉不到对方;
|
||
- 部署 = 把 `go/` 的内容同步到服务器目录(手工拷文件 / scp / rsync),**新增目录最容易漏**——
|
||
本目录就是漏传过一次导致验证 404 的典型;
|
||
- 同步本目录用**文件路径**,别用目录路径,否则会多出一层 `verify/verify`:
|
||
|
||
```powershell
|
||
# Windows 本地 → 服务器(只同步新增的验证文件)
|
||
scp "E:\Demo\Go\yunzerwebsiteallinone\go\verify\1XXl8fPHr9.txt" root@10.31.100.2:/www/wwwroot/api.yunzer.cn/verify/
|
||
```
|
||
|
||
```bash
|
||
# 服务器上确认
|
||
ls -l /www/wwwroot/api.yunzer.cn/verify/
|
||
curl -i http://127.0.0.1:9000/1XXl8fPHr9.txt # 直连 Go,绕过 nginx
|
||
```
|
||
|
||
> Go 每次请求都会实时 `os.Stat` 验证目录,**文件放进去立即生效,不需要重启服务**。
|
||
|
||
## ⚠️ 本项目 nginx 的两个既定事实(改配置时别踩)
|
||
|
||
api.yunzer.cn 的 vhost(`/www/server/panel/vhost/nginx/api.yunzer.cn.conf`)有两个特点,
|
||
直接影响本机制的配置方式:
|
||
|
||
1. **`proxy_set_header Host 10.31.100.2;`** —— 转发给 Go 时 Host 头被改成了 IP,
|
||
Go 侧**看不到 `api.yunzer.cn` 这个域名**。
|
||
→ 因此 `verify_hosts` **必须留空**(留空 = 所有走本服务的域名都放行)。
|
||
一旦填了 `api.yunzer.cn`,Go 会因为 Host 对不上而拒绝提供文件。
|
||
2. **`location ^~ / { proxy_pass ... }`** —— `^~` 匹配后 nginx 不再检查正则 location,
|
||
后面那两条「禁止访问的敏感文件 / 敏感目录」的 `location ~*` 规则在这个站点实际不生效。
|
||
目前没有泄露风险(Go 侧不提供工作目录文件,未知路径一律 404),但要知道这层保护是空的。
|
||
|
||
## ⚠️ 微信 / 小程序业务域名校验:必须让 http 也能直接访问
|
||
|
||
微信公众平台配置「业务域名」时,会去 **`http://`** 你的域名根目录抓取校验文件
|
||
(`http://api.yunzer.cn/1XXl8fPHr9.txt`)。宝塔默认的 HTTP→HTTPS 强制跳转会把
|
||
这个请求 301 到 https,微信拿到重定向就直接判「校验文件验证失败」。
|
||
|
||
**症状**:浏览器访问 `https://域名/校验文件.txt` 完全正常,微信后台却报
|
||
「校验文件验证失败,请确认已在域名根目录正确放置校验文件」。
|
||
|
||
**修法**:宝塔 → 网站 → 对应站点 → 设置 → 配置文件,在 `#HTTP_TO_HTTPS_START` 块里
|
||
加一条例外(放在 `if ($isRedcert != 1)` 之前):
|
||
|
||
```nginx
|
||
# 域名校验文件(微信 / 搜索引擎 / 第三方平台)必须能用 http 直接访问,不能 301 跳 https
|
||
if ( $uri ~ "^/[A-Za-z0-9._-]+\.txt$" ) {
|
||
set $isRedcert 1;
|
||
}
|
||
```
|
||
|
||
> 注意:这条必须加在 **HTTP_TO_HTTPS 块里**。在 `location` 里加 `location = /xxx.txt`
|
||
> 没用——server 层的 `rewrite` 在 location 匹配之前就执行了,重定向照样发生。
|
||
|
||
改完 `nginx -t && nginx -s reload`,然后确认:
|
||
|
||
```bash
|
||
curl -i http://api.yunzer.cn/1XXl8fPHr9.txt # 期望 200 + 文件内容,且没有 Location 头
|
||
curl -i https://api.yunzer.cn/1XXl8fPHr9.txt # 仍是 200
|
||
```
|
||
|
||
其它常见原因(按排查顺序):
|
||
|
||
1. **文件必须原样**:文件名区分大小写、不能改后缀;内容与微信下载的那份字节级一致,
|
||
不能带 BOM / 结尾换行。`xxd verify/xxx.txt` 确认没有 `efbbbf` 开头、没有结尾 `0a`。
|
||
2. **备案要同步到微信侧**:截图里写明「新备案域名需 24 小时后才可配置」,工信部数据同步
|
||
到微信侧通常 24~48 小时;备案主体须与小程序主体一致,接入商须与服务器提供商一致。
|
||
部分省份还要求已完成**公安联网备案**。
|
||
3. **微信侧缓存**:全部正确仍失败时,重新点「下载文件」换一个随机文件名放进 `verify/`,
|
||
再保存,可绕过平台短时缓存。
|
||
4. **小程序主体类型**:业务域名**只支持企业主体**小程序,个人主体 / 海外类型小程序
|
||
在微信侧根本不支持配置业务域名(报错形态与校验失败一样)。
|
||
5. **修改次数限制**:服务器域名每月只能改 5 次,业务域名至多 20 个,别反复试。
|
||
|
||
> 判定要点:如果访问日志里能看到微信爬虫(UA 类似 `Mozilla/4.0`)请求校验文件并返回 **200**,
|
||
> 说明**文件校验已通过**,剩下的失败只能是备案 / 主体 / 缓存这类平台侧原因,
|
||
> 不要再改服务器配置。
|
||
|
||
### 用访问日志判读卡在哪一步
|
||
|
||
改完 nginx 后,点微信「保存」的**同时**在另一个窗口执行:
|
||
|
||
```bash
|
||
tail -f /www/wwwlogs/api.yunzer.cn.log | grep -i "1XXl8fPHr9\|MP_verify"
|
||
```
|
||
|
||
| 日志表现 | 卡在哪 | 怎么办 |
|
||
|---|---|---|
|
||
| 新增一行,状态 `200` | 文件链路正常 | 剩下是备案同步 / 微信缓存,等 24~48 小时或换文件名重试 |
|
||
| 新增一行,状态 `404` | 微信要的文件名和你放的不一致 | 重新「下载文件」,原文件原样传进 `verify/` |
|
||
| 新增一行,状态 `301` | nginx 例外没生效 | 检查 `if ( $uri ~ ^/[A-Za-z0-9._-]+[.]txt$ )` 是否加在 `if ($isRedcert != 1)` 之前 |
|
||
| **一行都没有** | 微信压根没来拉 | 微信在**备案校验**阶段就拦下了,与文件无关 → 等备案同步,核对主体/接入商 |
|
||
|
||
### 公网可达性自查
|
||
|
||
服务器本机 curl 走的是回环,不代表公网通。微信爬虫在公网,必须用外部验证:
|
||
|
||
```bash
|
||
# 从任意一台外部机器/手机流量执行
|
||
curl -i http://api.yunzer.cn/1XXl8fPHr9.txt # 期望 200 + 内容,且无 Location 头
|
||
```
|
||
|
||
或用手机切 4G/5G(不连 WiFi)打开 `http://api.yunzer.cn/1XXl8fPHr9.txt`,
|
||
能看到 `1XXl8fPHr9` 即为公网可达。
|