百度站长工具「加站 → 验证 → 文件同步」里那个 baidutoken.php,我是这么活下来的适用场景:百度搜索资源平台(ziyuan.baidu.com)加站流程第三步「验证网站」→ 选文件验证,要往站点根目录放一个百度给的验证文件,放好了百度才认这个站。代码落点:baidutoken.php(服务器端的「投递口」)+ baidu_flow.py 的 step3_v

扫码分享
二维码

详细介绍

百度站长工具「加站 → 验证 → 文件同步」里那个 baidutoken.php,我是这么活下来的

适用场景:百度搜索资源平台(ziyuan.baidu.com)加站流程第三步「验证网站」→ 选文件验证,
要往站点根目录放一个百度给的验证文件,放好了百度才认这个站。
代码落点:
baidutoken.php(服务器端的「投递口」)+ baidu_flow.py 的 step3_verify()(客户端的「投递员」)
一句话结论:**这个 php 只要老实干「写文件 / 删文件」两件事就够了。
真正把人坑死的不是代码,是顺序(先确认验证成功再删)、命名(协议 / www / 大小写都算不同站)
和「假 200」(文件根本不存在,站点也回你 200)。**

0. 先吐槽:为什么非得有这么个文件

我一开始的脑回路是:百度要验证文件,那我用 FTP / 宝塔面板手动传一下不就行了?
传第一次可以,第二次想死。整个流程是「加站 → 验证 → 拿 TOKEN → 推链接 → TOKEN 用完了删站 → 再加新站」,
这条链是要跑几千遍的。手传?做梦。

于是思路变成:既然软件跑在本地,服务器在远端,那就找个「远端能写文件」的口子。
最省事的口子就是——在网站根目录扔一个 php,本地 POST 过去,让它自己把文件写出来。
这就是
baidutoken.php。它不是什么高深玩意,就是个只认识两个字段的文件接收器:

  • 文件名:百度给啥就写啥,别自己拼,带后缀

  • 正文内容:百度给的内容原样落盘,一个字节都别改

再配一个「删文件」的动作,用完自己擦干净,收工。


1. 它在整个流程里站哪个位置

一句人话:它是「百度验证文件」从百度 → 你的站点服务器的搬运通道;顺便兼了个「用完删掉」的保洁。

百度(ziyuan.baidu.com)                本地软件(跑在你这台机器)              你的站点服务器
  │ 第三步页面给个下载链接                    │                                  │
  │ ──── /site/verifyfile?id=<站点ID> ──────▶ │(带 Cookie 请求,慢,别急)       │
  │ ◀──── 返回文件名 + 验证内容 ──────────────│                                  │
  │                                          │ ── POST 文件名 + 正文内容 ──────▶ │ baidutoken.php 落盘
  │                                          │ ◀── {"code":0,"file":...} ───── │
  │                                          │ ── GET https://域名/<文件名> ───▶ │ 期望 200 且内容一致
  │ ◀── POST /site/verify (id + type=file) ──│                                  │
  │ ──── {"status":0,...} 验证通过 ─────────▶│                                  │
  │                                          │ ── action=delete 删文件 ───────▶ │ baidutoken.php 删掉
  │  接着去 linksubmit 页面拿 API TOKEN     │                                  │

注意两个细节,后面坑清单里还会骂:

  1. 下载接口是「延时返回」的——/site/verifyfile?id=xxx 其实是百度在服务端现场生成这个文件,

所以它挂几秒甚至十几秒才吐数据是完全正常的,别当成超时然后疯狂重试。

  1. 验证通过之后才删文件——顺序反了,站点在百度那边会一直挂「未验证」。


2. 对外契约(软件侧就是这么调它的)

项目内容
接口地址https://{用户域名}/baidutoken.php(域名带不带 www、http 还是 https,是不同站点)
请求方式POST(application/x-www-form-urlencoded),GET 也留着方便自测
写入字段文件名、正文内容(就是中文字段名,别改成 file/content,除非你两边同时改)
删除字段action=delete + 文件名
连通自测action=ping —— 一个请求确认「够不够得着 + 目录能不能写」
建议安全项key=...,和本地 config.json 里的值对上才放行
成功返回建议 JSON:{"code":0,"msg":"ok","file":"...","size":123}

配套的软件侧动作(baidu_flow.py):

  • step3_verify():抓下载链接 → 落盘 → POST 投递 → GET 校验 → 点「完成验证」→ 删文件

  • 完成验证走的是 POST /site/verify,body id=<站点ID>&type=file,

返回 {"status":0,"isMainSite":false,"isFirstTime":true} —— 只有 status==0 才算真成功,
拿到别的值就把文件留着别删,然后 dump 现场(截图 + HTML)慢慢查。

  • 删站是另一套逻辑(站点列表页),必须在「目标站点那一行」里找删除按钮,

我干过一次全局找按钮的事,差点把别人站点删了,后背发凉。


3. 完整成功时序(照这个顺序排就不会错)

  1. 打开第三步「验证网站」,选文件验证方式

  1. 带 Cookie 请求 /site/verifyfile?id=<站点ID>,耐心等,拿到验证文件(文件名 + 正文)

  1. POST 到 https://{域名}/baidutoken.php:文件名 + 正文内容

  1. 服务端回 code:0

  1. GET https://{域名}/{文件名}?t=<随机数>(带随机数绕缓存),要求

    • HTTP 200

    • 响应正文和刚投递的内容逐字节一致

  1. 回百度页面点「完成验证」→ POST /site/verify → 必须 {"status":0}

  1. 确认 status==0 之后,再 POST action=delete 删掉验证文件

  1. 去 https://ziyuan.baidu.com/linksubmit/index?site=<带协议的域名> 抓 API 提交地址

(形如 http://data.zz.baidu.com/urls?site=<域名>&token=<TOKEN>)

  1. 推链接,直到当天剩余条数为 0 → 删站 → 回到第 1 步加新站


4. 参考实现:能直接把文件投递进去的 baidutoken.php

如果你服务器上那份丢了、或者要给别人复用,照这个写就行。
放到网站根目录,和要验证的站点同一个域名下。
<?php
/**
* baidutoken.php —— 百度站点验证文件的「投递口」
* 只干两件事:写文件、删文件。
* 字段:文件名 / 正文内容 / action / key
*/
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');

$SECRET  = 'CHANGE_ME_一串随机字符串';   // 必须和软件 config.json 里的 verify.key 一致
$baseDir = __DIR__;                       // 默认:php 所在目录(= 网站根目录)
$logFile = __DIR__ . '/baidutoken.log';

function out($code, $msg, $extra = array()) {
   echo json_encode(array_merge(array('code' => $code, 'msg' => $msg), $extra), JSON_UNESCAPED_UNICODE);
   exit;
}

function logline($logFile, $text) {
   if (@filesize($logFile) > 5 * 1024 * 1024) { @rename($logFile, $logFile . '.1'); }
   @file_put_contents($logFile, date('Y-m-d H:i:s') . ' ' . $text . "\n", FILE_APPEND | LOCK_EX);
}

$in     = array_merge($_GET, $_POST);
$action = isset($in['action']) ? strtolower(trim($in['action'])) : 'save';
$key    = isset($in['key']) ? trim($in['key']) : '';

if ($SECRET !== '' && $key !== $SECRET) {
   logline($logFile, 'bad key from ' . $_SERVER['REMOTE_ADDR']);
   out(403, 'key 不对');
}

if ($action === 'ping') {   // 连通性自测:够不够得着、目录能不能写
   out(is_writable($baseDir) ? 0 : 1, 'ping', array('dir' => $baseDir, 'writable' => is_writable($baseDir)));
}

/* 文件名过滤:只留一层文件名,挡掉路径穿越和稀奇古怪的字符 */
function safe_name($name) {
   $name = trim((string)$name);
   $name = str_replace('\\', '/', $name);
   $name = basename($name);                                   // 干掉 ../ 之类
   if ($name === '' || $name === '.' || $name === '..') return '';
   if (strpos($name, "\0") !== false) return '';
   if (!preg_match('/^[A-Za-z0-9._-]{1,120}$/', $name)) return '';  // 中文名在 Linux 上会变乱码
   if (substr($name, 0, 1) === '.') return '';                // 不放隐藏文件
   return $name;
}

$name = isset($in['文件名'])   ? $in['文件名']   : (isset($in['filename']) ? $in['filename'] : '');
$body = isset($in['正文内容']) ? $in['正文内容'] : (isset($in['content'])  ? $in['content']  : '');
$name = safe_name($name);
if ($name === '') { logline($logFile, 'bad filename'); out(400, '文件名不合法'); }

$path = $baseDir . DIRECTORY_SEPARATOR . $name;

if ($action === 'delete') {
   if (is_file($path) && @unlink($path)) {
       logline($logFile, 'delete ok ' . $name);
       out(0, 'deleted', array('file' => $name));
   }
   logline($logFile, 'delete fail ' . $name);
   out(1, '删除失败或文件不存在', array('file' => $name));
}

if ($action !== 'save') { out(400, '不认识的动作:' . $action); }

/* 原子写:先写临时文件再 rename,免得百度正好读到半截内容 */
$body = (string)$body;
if (strncmp($body, "\xEF\xBB\xBF", 3) === 0) { $body = substr($body, 3); }  // 去掉 BOM!否则百度校验不过
$tmp = $path . '.tmp' . getmypid();
if (@file_put_contents($tmp, $body, LOCK_EX) === false) {
   logline($logFile, 'write fail ' . $name);
   out(1, '写文件失败,检查目录权限');
}
if (!@rename($tmp, $path)) { @unlink($tmp); out(1, '改名失败'); }
@chmod($path, 0644);
logline($logFile, 'save ok ' . $name . ' ' . strlen($body) . 'B ' . $_SERVER['REMOTE_ADDR']);
out(0, 'ok', array('file' => $name, 'size' => strlen($body)));
---

## 5. 坑清单(我一条一条踩过来的)

1. **「假 200」是最阴的坑。** 站点开了伪静态 / 自定义 404 页,随便一个不存在的文件也回 200。
  你一看 200 就点「完成验证」,百度那边一对内容发现不对 → 白验证。
  **必须比对响应正文**,不能只看状态码。
2. **协议 / www / 端口 / 大小写都算不同站点。** `http://a.com`、`https://a.com`、`https://www.a.com`
  在百度眼里是三个站,TOKEN 不通用。我们真踩过:TOKEN 接口把域名写死成 `https://`,
  站点却是 `http://`,直接 `Bad Request / site is wrong`。
3. **删文件删早了。** 必须等 `/site/verify` 返回 `status==0` 再删,否则站点一直挂「未验证」,
  而且你还得重新走一遍第三步。
4. **删早了 → 重来;删错了 → 更大的事。** 删站按钮一定要限定在「目标站点那一行」里找。
5. **php 目录不可写。** 宝塔 / 容器 / 只读挂载都会中招,一定要先 `action=ping` 探一下。
6. **BOM 和多余换行。** 编辑器偷偷加 BOM、末尾自动加回车,百度对不上内容。落盘要按原字节写,
  BOM 直接在服务端剥掉(参考实现里有)。
7. **别自己拼文件名。** 百度给的名字长这样:`baidu_verify_code-xxxxxxxx.html`(也可能给 `.txt`),
  **原样用**。带中文 / 带空格的名字在 Linux 上会因为编码变成乱码文件名,百度自然也找不到。
8. **CDN / 反代缓存。** 刚写完 GET 到的是旧内容。校验 URL 加随机参数,或者 `Cache-Control: no-store`。
9. **下载接口慢是正常的。** `/site/verifyfile?id=xxx` 是现场生成,慢几秒到十几秒都正常,
  别当超时然后重试成灾(重试多了还容易触发风控)。
10. **用完记得删。** 验证文件留着就是垃圾文件,而且暴露了「能写文件」的入口。
11. **入口安全别偷懒。** 这个 php 能写文件,不加 key 就是个上传漏洞。
   我第一版没加,现在回想起来后背发凉。key + 文件名白名单 + 只允许一层文件名,缺一不可。
12. **别信 POST 的返回,要 GET 复检。** 返回 `code:0` 只代表「写完了」,
   不代表「百度能读到、内容还对」。
13. **`/site/add` 报 403 + `server: bfe` 空响应 = 账号级临时风控。**
   这时候第二步根本不会出现,别怀疑你的领域选择器。
   实测证据:连页面自己发的请求都被拦。解法:**重新登录百度资源平台重新导出 `cookies.txt`**(最有效)
   / 换网络(手机热点)/ 配代理 / 退避重试(15 → 30 → 60 → 120 秒,最多 12 次)。
14. **跨域不用管。** 本地软件 POST 到站点,是「服务器到服务器」,不涉及浏览器同源策略,
   别为这个去折腾 CORS。

---

## 6. 上线前自检 8 条

1. `?action=ping&key=xxx` 返回 `code:0`,且 `writable` 为真
2. 投一个测试文件,浏览器直接访问 `/{文件名}`,内容**逐字节一致**
3. 故意访问一个不存在的文件,确认站点**不会**回 200(防「假 200」)
4. `http://` 和 `https://` 两个域名各放一次(如果两个都是有效站点)
5. `www` 和非 `www` 分清,别把文件写到 A 站、验证 B 站
6. key 不对时返回 403(确认 key 真的生效了)
7. 文件名里塞 `../etc/passwd` 会被拒(确认过滤生效)
8. 走一遍完整链路:投递 → GET 校验 → 完成验证 `status==0` → 删除

---

## 7. 复用速查(几条 curl 就能测完)

bash

1. 够不够得着、能不能写

curl -s "https://你的域名/baidutoken.php?action=ping&key=你的KEY"

2. 投递(字段名就是中文;嫌编码麻烦可以用 filename / content 别名)

curl -s -X POST "https://你的域名/baidutoken.php" \
-d "key=你的KEY" \
-d "filename=baidu_verify_code-abcd1234.html" \
--data-urlencode "content=abcd1234token"

3. 自检(带随机数绕缓存;重点看正文,别被假 200 骗了)

curl -s -i "https://你的域名/baidu_verify_code-abcd1234.html?t=$RANDOM"

4. 删除(确认验证成功之后再执行)

curl -s -X POST "https://你的域名/baidutoken.php" \
-d "action=delete&key=你的KEY" \
-d "filename=baidu_verify_code-abcd1234.html"
```


8. 最后

这个 php 本身简单到不好意思单独写一篇文档。真正值得记下来的是这四句:

**原样搬运(字段名、文件名、正文一个都别改)+ 写完必复检(比内容,不看状态码)+
验证通过才删(
status==0)+ 入口必须上锁(key + 文件名白名单)。**

把顺序排对,这个环节基本就不会再回来看第二眼了。