背景
Access to XMLHttpRequest at ‘http://a.com/api’ from origin ‘http://b.com’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: Redirect is not allowed for a preflight request.
生产上多次出现上面这个奇怪的跨域问题,但神奇的是强刷新后或者使用无痕模式打开就正常了。
到底是哪里出了问题呢?本文将一探究竟。
先说结论
排查后发现,出现这个报错的原因是:之前使用过 HTTPS 访问页面,所以也请求了 HTTPS 协议的 API,然后 API 的域名被记录在 HSTS
列表中,之后使用 HTTP 访问页面,而 API 请求却被重定向到 HTTPS,而因为预检请求(OPTIONS)不能被重定向,所以导致出现 CORS 错误。
关于 CORS 更详细的介绍可以点击查看:HTTP 访问控制(CORS)
总的来说原因在于前后端 HTTP 和 HTTPS 混用导致的,正常的情况下如果统一为 HTTP 或者 HTTPS 则不会出现这个问题。
所以如果要开启 HSTS
,请确保前后端都开启,否则就会出现与本文一样的错误。
那么前端页面要开启 HSTS 的话,需要做哪些操作呢?需要在 web 服务器添加响应头 ,以 Nginx 为例:
server {
listen 443 ssl;
server_name www.example.com;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
如果是其他 web 服务器,可以到参考这里。
这里还要注意:
- 即便关掉 HSTS 也需要等到 max-age 过期才会从 HSTS 列表清除,所以除非让用户手动清除,否则这段时间内还是会被重定向到 HTTPS。
- 如果加了 includeSubDomains ,该网站的所有子域名都会被重定向到 HTTPS ,那会有什么影响呢?假设生产环境为 http://a.com,而测试环境为 http://test.a.com,当访问 http://a.com 后,即便在测试环境没有 HTTPS 的情况下也会被重定向
好了,说完结论,那下面来讲讲我是如何使用 Chrome 自带的网络记录工具定位到此问题的。
是缓存的问题?
既然强刷新或者使用无痕模式是正常的,那么就很有可能是因为缓存导致,对比会出现跨域和正常的请求头,发现两者有很大不同:
这是正常的 👇
这是出现跨域的 👇
可以看出下面这个会出现跨域的请求少了很多内容,而猜测会不会是因为缺少 CORS 相关的部分请求头,所以导致跨域呢?
根据关键词「跨域 无痕模式」在搜索引擎找到这篇《原來 CORS 沒有我想像中的簡單》,其中说到是因为浏览器缓存的问题,使用 ,然并卵。crossorigin="anonymous"
让每次发出的请求带上 origin header, 但是这个属性只适用于 <img>
、 <script>
等,于是我去寻找在 axios 请求库对应的属性
那么有没有可能问题根本不是出在前端发送请求上呢?
使用 Chrome 排查网络问题
既然这样,那就来看看浏览器发送请求的过程中到底发生了什么,于是我通过 chrome://net-export 记录会发生跨域的页面请求日志:
导出日志文件后,发现了一个很奇怪的点:
在上图可以看到,明明请求 HTTP 协议的 API,但是因为 HSTS
,将请求重定向到了 HTTPS 协议下。
简单来说一下 HSTS
是什么:
HTTP Strict Transport Security(通常简称为 HSTS)是一个安全功能,它告诉浏览器只能通过 HTTPS 访问当前资源,而不是 HTTP —— MDN
就是说如果浏览器发现该网站开启了 HSTS
,则会自动把所有请求重定向到 HTTPS 下。
为了验证是不是因为这个原因,在无痕模式下也进行了一次记录,发现该日志下找不到 HSTS
的身影:
随后我又通过 Chrome 的 HSTS
设置页面进一步验证了两者的不同,打开 chrome://net-internals/#hsts 查询发送跨域的请求域名:
出现跨域的浏览器窗口 👇
正常请求的浏览器窗口(无痕模式)👇
都是 HSTS 惹的祸
没想到,竟然是因为 HSTS
,那为什么一个会有 HSTS
而另一个却没有呢?这时候我留意到出现跨域的页面使用的是 HTTP 访问,但其实是支持 HTTPS 的,所以我猜想会不会是:之前使用过 HTTPS 访问页面,所以也请求了 HTTPS 的 API,然后 API 的域名被记录在 HSTS
列表中,之后使用 HTTP 访问页面,而 API 请求却被重定向到 HTTPS,所以导致跨域?
为了验证上面的猜想,特地开了一个新的无痕模式,按照上面所说的步骤操作,完美复现跨域错误!
更重要的是我在原本出现跨域的页面改为 HTTPS 访问,也正常请求了。
为什么会出现这种情况?
为什么请求 API 会被自动重定向到 HTTPS,而请求页面却不会呢?
原来 HSTS
是根据响应头有没有 strict-transport-security
字段决定是否加入 HSTS
列表的,经排查发现前端页面的响应头是没有这个字段的,但 API 响应头却有。
页面响应头:
API 响应头:
总结
如果出现奇怪的网络请求问题,可以尝试使用 Chrome 自带的网络请求分析工具,说不定会有收获。