HTTPS 没强制——站点 http 和 https 都能打开

站点 http 和 https 都能访问,Google 收录两份副本,Chrome 把 http 标成 Not Secure。加一条 301 重定向再补上 HSTS 头就能修好。

你输入 http://yourdomain.com,页面直接打开,没有跳转到 HTTPS。Search Console 把每个 URL 的 http 和 https 版本分开各列一遍。Chrome 在 http 请求上显示 “Not Secure”。在 Google 眼里这就是一个重复站点,抓取预算被一分为二,那份未加密的副本还可能跟安全版并列、甚至盖过它。

**最快的修法:**打开主机自带的强制 HTTPS 开关(Vercel、Netlify、Cloudflare、Firebase 都是一个开关)。它会在请求到达你的 app 之前就发出 http 到 https 的 301。然后补一个 Strict-Transport-Security(HSTS)头,让浏览器在回访时直接跳过 http 那一跳。两步都是几分钟内生效。下面的平台路径截至 2026 年 6 月有效。

5 秒确认症状

curl -sI "http://yourdomain.com" | head -3
  • 返回 HTTP/1.1 200(或 HTTP/2 200)说明 http 被直接服务——没强制,继续往下看。
  • 返回 HTTP/1.1 301 Moved Permanently 且带 Location: https://... 说明重定向已经在工作;你剩下的问题多半是 HSTS 没设,或者 Search Console 索引还没更新。

你属于哪一类

curl -sI http://yourdomain.com 的症状可能原因跳到
每个路径都是 200主机的强制 HTTPS 开关没开第 1 步
301 但页面看着仍不安全 / 源站还是明文 httpCloudflare 用了 Flexible 模式第 3 步
301 正常,但 https 上没有 strict-transport-security没设 HSTS第 2 步
有些路径 301、有些 200(常是静态资源)重定向写在 app 代码里,不在平台层第 1 步
301 正常但 Search Console 还显示两个版本索引尚未合并第 5 步

常见原因

按命中率从高到低排。

1. 主机的强制 HTTPS 开关没开

大多数现代主机都能替你把 http 重定向到 https,但少数配置(旧的自建 Nginx、某些共享主机、CDN 后面自己管的源站)默认不开,要你手动打开。

怎么判断:curl -sI "http://yourdomain.com" | head -3 返回的是 200 而不是 301

2. Cloudflare 用了 “Flexible” SSL 模式

“Flexible” 在 Cloudflare 边缘终结 TLS,但跟你的源站之间走明文 HTTP。访客浏览器上是一把锁,可源站那一段是不加密的——这也会破坏源站上的 HTTPS-only 设置。Cloudflare 仍然支持 Flexible,但现在强烈建议用 Full 或 Full (strict),而且它的 Automatic SSL/TLS 功能(2024 年底起逐步推开)可能会自动帮你升级模式。

**怎么判断:**Cloudflare 控制台 → SSL/TLS → Overview。加密模式如果写着 “Flexible”,就是它了。

3. 没设 HSTS

哪怕 301 工作正常,浏览器在一个会话里第一次请求时还是会先走 http、再跟随重定向。HSTS 告诉浏览器”接下来 N 秒内这个域名一律用 HTTPS”,之后每次访问都直接跳过 http 那一跳。

怎么判断:

curl -sI "https://yourdomain.com" | grep -i strict-transport-security

输出为空就是没有 HSTS 头。

4. 重定向写在 app 代码里,不在平台层

写在 app 代码里的重定向(Express middleware、Astro middleware、框架配置)只有在请求到达 app 后才触发。更早被拦下的请求——被 CDN 拦的,或 app 代码运行之前就被服务的静态资源——根本轮不到它。

**怎么判断:**某些 URL 会重定向,而另一些(通常是 /favicon.ico/robots.txt 这类静态文件)在 http 上返回 200。把重定向挪到平台 / 边缘层。

5. CDN 或代理直接服务 http

源站前面的 CDN 可以被配成 http 和 https 都应答。没有显式的”重定向到 https”规则,http 就保持 http。

**怎么判断:**直接 curl 源站(绕过 CDN)。如果源站返回 301 而公开 URL 没有,问题就在 CDN 配置。

6. 混合内容削弱安全页

页面里有走 http 拉取的子资源——<img src="http://...">、一个脚本或一张样式表——会触发 “Mixed Content” 警告。这不会阻止 HSTS 头发出,但会破坏那把安全锁,值得一并修掉。

**怎么判断:**DevTools → Console → 找 Mixed Content 警告。

最短修复路径

第 1 步:打开平台层强制 HTTPS

Vercel — 自定义域名默认就强制 HTTPS,没有要手动开的开关。在 Project → Settings → Domains 里确认该域名显示一张有效证书即可。

NetlifyDomain management → HTTPS,然后开启 Force HTTPS。(旧的 “Site settings →” 前缀已经没了;截至 2026 年 6 月,这一项已是顶层的 Domain management。)只有等 Netlify 把 Let’s Encrypt 证书签发好之后,这个开关才会出现。

CloudflareSSL/TLS → Edge Certificates → Always Use HTTPS → 开。它会在 Cloudflare 边缘对每个 http 请求发出 301,覆盖所有路径和子域名,在请求到达源站之前就完成。

Firebase Hosting — 强制 HTTPS 是自动的:Firebase 默认在每个 Hosting 域名上把 http 用 301 跳到 https,所以你不需要firebase.json 里为此写重定向规则。(redirects 块只用于在不同 URL 之间跳转,不是用来强制协议的。)如果 http 仍然不跳转,那这个域名多半根本不是由 Firebase Hosting 在服务——回头检查 DNS 记录是否指向 Firebase。

第 2 步:加上 HSTS 头

Vercel — Vercel 在自定义域名上已经自动发送 Strict-Transport-Security: max-age=63072000,所以通常不用自己设。要自定义值(比如加上 includeSubDomains; preload),在 vercel.json 里设:

{
  "headers": [{
    "source": "/(.*)",
    "headers": [
      { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload" }
    ]
  }]
}

Netlify — 在发布目录里加一个 _headers 文件:

/*
  Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

CloudflareSSL/TLS → Edge Certificates → HTTP Strict Transport Security (HSTS) → Enable HSTS,然后在对话框里设置 Max-Age 和 includeSubDomains

先用一个短 max-age300 = 5 分钟)确认没把站搞坏,确认无误后再升到 63072000(2 年)。63072000 是常见的生产值;preload 要求的最低值是 31536000(1 年)。

第 3 步:Cloudflare 改用 Full (strict)

SSL/TLS → Overview → Full (strict)。这样 Cloudflare 到源站的连接既加密、又会校验源站证书,堵上 “Flexible” 留下的那道口子。

如果卡在源站证书上,先修源站——装一张平台证书,或者免费的 Cloudflare Origin Certificate。别为了”先跑起来”而关掉校验,那等于把 Flexible 的同款风险又请回来。

第 4 步:用 curl 验证

curl -sI "http://yourdomain.com" | head -5

应返回:

HTTP/1.1 301 Moved Permanently
Location: https://yourdomain.com/

https 这边:

curl -sI "https://yourdomain.com" | grep -i strict-transport-security

应打印出 HSTS 头。裸主域名和 www 都要测,再加至少一个静态资源路径(/favicon.ico)——这些正是 app 层重定向最容易漏掉的地方。

第 5 步:把 Search Console 重新指向 https

Search Console → URL Inspection → 对你最重要的几个 URL 请求收录 https 版。301 上线后,Google 会在接下来几次抓取里把 http URL 自动合并到 https canonical 上——你不必手动删掉那些 http 版。

第 6 步:(可选)HSTS preload

等 HSTS 平稳跑了几周后,到 hstspreload.org 提交域名。Preload 之后,浏览器从首次访问起就对你的域名强制 HTTPS,连联系你服务器之前就强制了。提交时发出的头必须包含至少 31536000(1 年)的 max-age,外加 includeSubDomainspreload

**注意:**从 preload 列表移除要好几周到几个月才能随浏览器版本扩散开。在你确定每一个现有及未来的子域名都会永远提供有效 HTTPS 之前,别 preload。

怎么确认修好了

  • curl -sI http://yourdomain.com 返回 301 且带 Location: https://... 头——主域名、www、以及一个静态资源路径都要测。
  • curl -sI https://yourdomain.com | grep -i strict-transport-security 能显示出该头。
  • 在 Chrome 里全新打开 http://yourdomain.com,落到 https、显示锁、没有 “Not Secure” 标记。
  • DevTools Console 里没有 Mixed Content 警告。

容易误判的情况

只设 <link rel="canonical" href="https://..."> 不能阻止 Google 收录 http 版——canonical 是建议、不是重定向。真正把 http URL 移出索引的是那条 301

预防建议

  • 第一天就在平台层强制 HTTPS。不要不开 HTTPS 就上线。
  • HTTPS 稳定运行 30+ 天后,加一个长 max-age(2 年)的 HSTS。
  • Cloudflare 用 Full (strict) SSL 模式,永远别用 Flexible
  • 任何基础设施变更后,重新跑 curl -sI http://yourdomain.com,确认它仍返回 301
  • 审计混合内容(<img src="http://...">),把子资源换成 https 或协议相对 URL。

FAQ

  • HSTS 会阻止回滚到 http 吗? 会,这正是它的作用——浏览器在 max-age 秒内拒绝走 http。所以升 max-age 之前,先测好 HTTPS 稳定可靠。要撤销,发送 Strict-Transport-Security: max-age=0;如果你还做了 preload,从 preload 列表移除要好几周。
  • 需要付费 SSL 证书吗? 不需要。Let’s Encrypt 或平台自带的证书足以覆盖绝大多数站点,零成本。
  • http→https 重定向用 301 还是 302?301(永久)。302 会告诉 Google 这次搬家是临时的,于是它可能继续保留 http URL 的索引。上面每个平台开关用的都是 301
  • 主机在 http 上返回 200,但浏览器里那把锁看着没问题——这就算好了吗? 没好。浏览器那把锁只反映你正好发出的那次 https 请求。只要 curl -sI http://... 还返回 200,http 副本就还活着、还可被收录。把重定向修好。
  • 强制 HTTPS 会伤 SEO 或排名吗? 不会——Google 从 2014 年起就把 HTTPS 当作(轻量的)排名信号,而合并到单一 https canonical 还能消除重复内容的稀释。预期的是一段短暂的重新抓取期,不是惩罚。

相关阅读

标签: #域名 #DNS #SSL #排查 #强制 HTTPS