Chrome 中 localhost 被强制跳转 HTTPS 解决方法
在地址栏中输入 chrome://net-internals/#hsts,然后找到 Delete domain security policies 栏目,在下面的输入框中输入被强制跳转的地址(例如 localhost)然后点击右边的 Delete 按钮
HSTS 是什么,为什么会影响 localhost
HSTS(HTTP Strict Transport Security)是一个服务端下发的安全策略。服务器在 HTTPS 响应里带上这个头:
Strict-Transport-Security: max-age=31536000; includeSubDomains
浏览器收到后就会记住这个域名,在 max-age 指定的时间内(这个例子是一年),所有对该域名的 HTTP 请求都会在浏览器内部直接改写成 HTTPS,请求根本不会发出去。
注意最后这句:它不是服务端的 301 重定向,是浏览器本地的强制改写。所以你会看到一个很困惑的现象——DevTools 的 Network 面板里那条请求的状态是 307 Internal Redirect,Initiator 显示不出来源,因为这个跳转发生在网络栈内部,压根没有网络交互。
HSTS 的设计目的是防 SSL 剥离攻击(sslstrip):用户在地址栏输 example.com 时浏览器默认发 HTTP 请求,攻击者可以在这个明文请求上做中间人,把服务器的 HTTPS 重定向拦掉,让用户全程走 HTTP。HSTS 让浏览器记住「这个站只能用 HTTPS」,从第二次访问起就没有明文请求可劫持了。
那 localhost 怎么会被记上?几个常见来源:
1. 你在 localhost 上跑过一个带 HSTS 头的服务。开发时把生产的 Nginx 配置或者中间件配置照搬到本地,里面就带着 Strict-Transport-Security。浏览器不区分是不是本机,照样记下来。这是最常见的原因。
2. 反向代理或框架默认加了这个头。Spring Security 默认会加 HSTS 头,Helmet(Node 的安全中间件)默认也加。用了这些的项目,本地跑起来就会给 localhost 打上 HSTS 标记。
3. .dev / .app 域名的特殊情况。这两个顶级域被 Google 整体加入了 HSTS preload 列表,也就是硬编码在 Chrome 里的强制 HTTPS 名单。如果你的本地开发域名是 myapp.dev,它永远走 HTTPS,且无法通过 chrome://net-internals/#hsts 删除——preload 是内置的,不在可删除的动态列表里。本地开发域名要避开 .dev、.app、.new 这些,用 .test 或 .localhost(这两个是 RFC 保留给测试用的,不会被 preload)。
那个页面的其它功能
chrome://net-internals/#hsts 上有三个区域,都值得知道:
Query HSTS/PKP domain — 查一个域名当前的 HSTS 状态。输入域名点 Query,如果有记录会返回 static_sts_domain(preload 列表里的)或 dynamic_sts_domain(服务端下发记录的)以及过期时间。排查时先用这个确认问题真的是 HSTS,别上手就删。
如果返回 Not found,说明问题不是 HSTS,得往别的方向查(见下文)。
Add HSTS domain — 手动给某个域名加 HSTS。测试自己站点的 HSTS 行为时有用。
Delete domain security policies — 就是正文用的那个,删除动态 HSTS 记录。注意它只能删动态记录,preload 列表里的删不掉。
删完之后建议再 Query 一次确认,以及清一下 socket 池和 DNS 缓存(同一个 net-internals 页面下有 Sockets 和 DNS 标签,各有一个 flush 按钮),因为已建立的连接可能还在复用。
删了还是跳转的其它原因
删掉 HSTS 记录后如果还是跳 HTTPS,往这几个方向查:
1. Chrome 的 HTTPS-First 模式。Chrome 从某个版本开始默认尝试用 HTTPS 加载所有站点,失败才回退 HTTP。设置在 chrome://settings/security → 始终使用安全连接。如果开着,表现和 HSTS 很像。关掉它,或者在设置里给 localhost 加例外。
2. Chrome 的地址栏自动补全。输入 localhost:3000 时,Chrome 可能根据历史记录补全成 https://localhost:3000。这不是强制跳转,是补全。解决办法是显式输完整的 http://localhost:3000,或者在地址栏历史里删掉那条 HTTPS 的记录(下拉建议里选中后按 Shift + Delete)。
3. 服务端自己在跳。你的应用或者反向代理配了 HTTP → HTTPS 的重定向。用 curl 看清楚(curl 不受浏览器策略影响,能看到服务端的真实响应):
curl -I http://localhost:3000
看到 301 或 302 加 Location: https://... 就是服务端在跳,去改服务端配置。
4. Service Worker 在拦请求。注册过的 Service Worker 会一直活着,即便你改了代码。它可以在 fetch 事件里改写请求。去 chrome://serviceworker-internals 或者 DevTools 的 Application → Service Workers 里 Unregister 掉。
顺带一说,Service Worker 是本地开发里另一个高频的「改了代码没生效」的原因。DevTools 的 Application 面板里勾上 Update on reload 和 Bypass for network,开发时能省很多困惑。
5. 浏览器缓存了 301。301 是永久重定向,浏览器会把它缓存起来,之后不再请求服务端直接跳。这个缓存清起来比较麻烦:清空缓存(Command/Ctrl + Shift + Delete 选「缓存的图片和文件」),或者用无痕窗口验证。
这也是为什么开发环境永远不要用 301——一旦下发过一次,用户(包括你自己)的浏览器会记很久,改回来都不生效。开发和测试环境的重定向用 302 或 307。
更好的方向:干脆给本地开发上 HTTPS
与其和 HTTPS 强制跳转对抗,另一条路是让本地也支持 HTTPS。这在今天越来越有必要,因为不少 Web API 只在安全上下文(secure context)下可用:
navigator.mediaDevices(摄像头、麦克风)navigator.clipboard(剪贴板读写)navigator.geolocation(新版本要求)- Service Worker
- WebAuthn、Payment Request
SharedArrayBuffer(还需要额外的 COOP/COEP 头)
好消息是 localhost 被浏览器视为安全上下文,即便是 HTTP。所以在 http://localhost 上这些 API 都能用,不需要 HTTPS。
但如果你需要用局域网 IP 或自定义域名访问(比如在手机上测本机起的服务),那就不是 localhost 了,安全上下文不成立,这些 API 全部失效。这时候本地 HTTPS 是唯一的解。
最省事的方案是 mkcert,它会在系统里安装一个本地 CA,签出来的证书浏览器直接信任,不会有那个「不安全」的警告页:
# macOS 安装
brew install mkcert
# 把本地 CA 装进系统信任库(只需一次)
mkcert -install
# 给需要的域名签证书
mkcert localhost 127.0.0.1 192.168.1.100 myapp.test
会生成两个文件(.pem 和 -key.pem),配进 Vite:
// vite.config.js
import fs from 'node:fs';
export default {
server: {
https: {
key: fs.readFileSync('./localhost+3-key.pem'),
cert: fs.readFileSync('./localhost+3.pem'),
},
host: true, // 监听 0.0.0.0,局域网可访问
},
};
host: true 这一项别漏,不加的话 Vite 只监听 127.0.0.1,手机连不上。
证书文件记得加进 .gitignore——它们是本机专用的,提交进仓库没有意义,而且私钥不该进版本控制:
*.pem
要在手机上访问这个 HTTPS 地址,还得把 mkcert 的 CA 根证书装到手机上并设为信任,否则手机浏览器不认这个证书。mkcert -CAROOT 能打印出 CA 文件所在目录,把 rootCA.pem 传到手机上安装即可。iOS 上装完还要去「设置」→「通用」→「关于本机」→「证书信任设置」里手动启用,这一步很容易漏。

还没有留言,来发表第一条吧~