先说最实用的结论:googlexxxx.html、google-site-verification Meta 标签和 OAuth credentials.json 解决的不是同一个问题。前两种用来证明“我能控制这个网站”,OAuth JSON 用来让一个应用请求“读取某个 Google 账户授权的数据”。Cloudflare 的安全验证则是在判断一次访问更像真人还是自动化请求。

它们都可能出现在 Search Console 接入过程中,所以很容易被混成“一份 Google 验证文件”。真正危险的不是叫错名字,而是把 OAuth 凭据当成网站验证文件上传到公开仓库。

Omnihex Lab 的仓库把 https://lab.omnihex.xyz/ 配置为 Search Console URL-prefix 属性;截至 2026-09-03,线上首页确实输出 Google 验证 Meta 标签。但当前 OAuth refresh 返回 HTTP 400,本轮无法通过 API 重新确认属性所有权、已提交 sitemap 或搜索表现数据。报告脚本从 macOS Keychain 读取 OAuth 客户端信息和 Refresh Token,仓库与网页不保存这些值。

网站所有权验证、OAuth API 授权与 Cloudflare 安全验证的职责对照

三种“文件”对应三个不同问题

1. googlexxxx.html:证明你能在站点根目录放文件

Search Console 的 HTML file 方法会给出一个名称和内容都必须保持不变的文件,例如 googlexxxx.html。把它放到 URL-prefix 属性对应站点的根目录后,Google 请求那个固定 URL,并检查内容是否匹配。

这份文件会公开可访问。它的任务只是站点所有权验证,不会赋予任何人读取 Search Console 数据的 API 权限,也不包含 OAuth Client Secret 或用户 Token。

对于静态站,这种方法很直接,但会在仓库里留下一个专用验证文件。如果站点重定向复杂,文件验证还可能因为跨域跳转而失败。

2. google-site-verification Meta 标签:证明你能修改首页 <head>

Meta 方法把 Google 给出的 Token 放进首页:

<meta name="google-site-verification" content="verification-token" />

Google 会访问不需要登录的首页,并检查 <head> 中是否存在对应标签。它同样是网站所有权验证,适用于 URL-prefix 属性;它不是 OAuth 登录,也不会读取搜索表现数据。

Omnihex Lab 选择了这个方法,因为 Astro 已经有统一的基础 Layout。代码只负责读取 PUBLIC_GOOGLE_SITE_VERIFICATION 环境变量并输出标签,真实 Token 由 Cloudflare 的生产环境变量提供,不写进仓库。

这里有一个容易忽略的边界:验证成功后,Google 仍会定期检查标签。如果把环境变量或 Meta 输出删掉,所有权状态可能失效。

3. OAuth credentials.json:让本地程序发起 API 授权

OAuth JSON 描述的是一个 Google Cloud OAuth Client。本文中的 credentials.json 特指下载的 Desktop app OAuth 客户端配置,不是同名的 Service Account JSON。它通常包含 Client ID,并可能包含 Client Secret、授权端点和回调信息。应用用这些信息发起 OAuth 2.0 流程,用户在 Google 页面确认授权范围,程序随后获得用于 API 请求的 Token。

这和“我能控制网站”是两回事。拥有 OAuth 客户端配置不会自动成为某个 Search Console 属性的 Owner;拥有网站验证文件也不会自动得到用户的 Search Console API 数据。API 请求仍需要经过用户授权,并携带有效的 OAuth Token。

Google 的安全文档要求客户端配置和用户 Token 使用安全存储,不要硬编码、提交到代码仓库或公开发布。对 Desktop app 而言,下载文件中的 Client Secret 不应被当成能独立证明客户端身份的长期机密;Refresh Token 和 Access Token 仍是可代表用户授权的敏感资产。本项目因此没有把 credentials.json 放进网站。

本项目的授权合同指定 webmasters.readonly。报告脚本不会在每次运行时重新请求 Scope;它读取已有 OAuth 配置,以 Refresh Token 换取 Access Token 后调用 Search Console API。当前交换步骤返回 HTTP 400,因此不能把“代码存在”写成“读取链路当前可用”。

Cloudflare 安全验证为什么也会出现

访问由 Cloudflare WAF、Bot、DDoS、Under Attack Mode 或 Turnstile 等机制保护的页面时,浏览器可能遇到 Cloudflare Challenge。它会根据浏览器和请求信号判断访问者是否需要进一步验证,有时无需操作,有时需要点击确认。

这个步骤和 Google 的三件事都无关:

  • 它不验证 Search Console 网站所有权;
  • 它不创建 OAuth Client;
  • 它不授予 Search Console API Scope;
  • 通过 Challenge 只表示当前请求通过了 Cloudflare 的安全检查。

所以“Cloudflare 验证通过”不能作为 Google 授权完成的证据。真正的 OAuth 完成证据应该来自 Google 同意页面后的回调成功,以及脚本能够用只读 Scope 发出一次真实 API 请求。

本项目最终怎样分配这些信息

信息 用途 是否进入公开网页 实际存放位置
google-site-verification Token Search Console URL-prefix 所有权验证 会,作为首页 Meta 内容 Cloudflare 生产环境变量
HTML 验证文件 另一种所有权验证方法 若采用则会公开 本项目未采用
OAuth Desktop app 客户端配置 标识 API 客户端;下载文件内的 Secret 不是独立身份保证 否 macOS Keychain
Access / Refresh Token 代表用户调用 API 否 macOS Keychain
webmasters.readonly 限定 API 只读权限 同意页面可见,不是网站内容 OAuth 授权记录
Cloudflare Challenge 状态 判断当前访问是否通过安全检查 只在挑战流程中出现 Cloudflare 边缘侧处理

仓库中的 BaseLayout.astro 只包含“如何输出验证 Meta”的逻辑,search-console-report.mjs 只包含“如何从 Keychain 取值并请求 API”的逻辑。两边都没有真实凭据。这种分离让代码可以审查,同时不把秘密变成构建产物。

一份不容易放错文件的操作清单

  1. 先写清目标。 是验证网站所有权,还是让程序读取 API?不要先下载文件再猜用途。
  2. 验证文件和 Meta 可以公开,但只能用于验证。 不要在其中混入 Client Secret 或 Token。
  3. OAuth JSON 只进安全存储。 不上传网站、不提交 Git、不粘进 issue、日志或对话。
  4. 使用最小 Scope。 只做报告时申请 webmasters.readonly,不要因为“以后也许有用”扩大权限。
  5. 把回调成功和真实 API 读取分开验收。 OAuth 页面显示同意,不等于报告脚本已经能稳定读取数据。
  6. 把 Cloudflare Challenge 当成独立安全层。 它通过或失败,都不改变 Google 属性和 OAuth 权限。
  7. 用敏感信息扫描收尾。 检查仓库、构建目录、日志和截图,确认没有 Client ID、Secret、Token、邮箱或回调参数残留。

适用边界

本文描述的是一个个人静态站的 URL-prefix 属性和本地只读报告程序。多人组织、公开 OAuth 应用、服务端多用户授权或 Domain property 会有额外的身份、审核和密钥管理要求。

截至 2026-09-03,本地报告脚本在刷新 OAuth 时返回 HTTP 400,因此本轮没有取得新的点击、展现或趋势数据。当前状态是 OAuth DEGRADED,而不是零流量或样本不足。现有证据只能证明所有权 Meta、仓库实现和安全存储边界存在,不能证明当前 API 读取成功,也不能支持搜索表现改善的结论。

这也延续了先做出知识网站,再设计完整知识系统的做法:只引入解决当前问题所需的最小权限和最小基础设施,不把未来假设提前变成长期负担。

参考资料