2021-09-13 一文解析前端请求307导致CORS跨域失败

现象

公司的系统偶尔会出现跨域错误,同事们的解决方法一般是清空浏览器缓存即可解决。大家一直都觉得很奇怪,但因为是偶发事件都不太关注。最近,经过一些资料搜索和努力排查,终于明白了是怎么回事


image.png

根据现象可以发现:

  • 先是一个预检(preflight)请求
    请求头:
OPTIONS /user/getxxxxxxxxxxxxxxxUser?tokenId=tokenId_590fddfe_eb95_46d1_a208_659e3a8d0a57 HTTP/1.1
Accept: */*
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Origin: http://xxx.yyy.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36
Sec-Fetch-Mode: cors

返回头:

HTTP/1.1 307 Internal Redirect
Location: https://xxx.yyy.com/user/getxxxxxxxxxxxxxxxUser?tokenId=tokenId_590fddfe_eb95_46d1_a208_659e3a8d0a57
Non-Authoritative-Reason: HSTS
Access-Control-Allow-Origin: http://origin.zzz.com
Access-Control-Allow-Credentials: true
  • 然后报了一个CORS error


    image.png

结论

先说结论:
-- 原因是浏览器在发送http请求时自动进行307内部跳转,导致了跨域预检失败。
-- 为什么会307内部跳转呢? 是因为浏览器采用HSTS(HTTP Strcit-Transport-Securit)策略,将所有非http的请求内部跳转成https。
-- 为什么浏览器认为这个域名需要采用HSTS策略呢?因为服务端/nginx返回的http请求header里配置了Strict-Transport-Security(严格传输安全)选项。

原理

跨域(CORS)问题

  • 什么是跨域问题?
    -- 出于安全性,浏览器限制脚本内发起的跨源HTTP请求。浏览器限定了不能任意访问非同源的资源,这意味着使用这些API(例如:XMLHttpRequest和Fetch API)的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确CORS响应头。
    -- CORS(Cross-origin resource sharing) -- 跨源资源共享

  • 简单请求和复杂请求
    -- 浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。如果是简单请求直接访问,如果是非简单请求需要先发预检请求preflight。
    -- 如何区分简单请求和负责请求?
    满足下面条件的即为简单请求,否则为复杂请求
    (这里插一句:曾经有一段代码,虽然是get请求,但是因为Content-Type 写了json 导致了也发出了预检请求,我们排查了半天才明白)

1. 请求头方法必须是 HEAD/GET/POST
2. HTTP的头信息不超出以下几种字段:
   Accept
   Accept-Language
   Content-Language
   Last-Event-ID
   Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、 
   text/plain
  • 简单请求
    如果是简单请求,request请求header里需要有一个origin 字段,response请求返回的header里需要标识Access-Control-Allow-Origin 表示允许此源的网站请求。如果没有返回对应的,浏览器知道出错了,会抛出一个错误。

  • 复杂请求
    -- 预检:复杂请求需要先发起一个预检请求(preflight),如果服务器返回的response中有对应的Access-Control-Allow-Origin头,表示预检通过。然后才能进入下一步
    -- 正式请求:一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

307跳转问题

现代浏览器和服务器都开始支持 HSTS(HTTP Strict Transport Security) 功能,即自动将不安全的 HTTP 请求使用 307 Internal Redirect 跳转到 HTTPS 请求。这是由Chrome内部HSTS缓存导致的。Chrome 会自动记住每个域的 HSTS 设置,也就是说HSTS只要在理论上的第一次暴露后,后来就不经网页服务器返回,浏览器会查询本地数据,直接伪造 HSTS 307 跳转到安全的 HTTPS,以此来加强网络访问的安全性。

服务器设置问题

问题是,是谁告诉浏览器这么做的呢?
原因是在nginx配置中有这么一段:

add_header Strict-Transport-Security max-age=31622400;

浏览器接到这样的回复头:

HTTP/1.1 200 OK
Server: 5.62
Date: Tue, 14 Sep 2021 08:10:07 GMT
Content-Length: 0
Connection: keep-alive
Strict-Transport-Security: max-age=31622400

这里意思是通知浏览器,访问的这个域名需要采用HSTS策略,并且过期时间是31622400秒(一年),在这一年中都需要使用HSTS策略,意味着发起http请求时,浏览器都会强行进行307内部跳转。

然后因为307跳转被cors预检请求认为是不合法的,故此预检失败:

has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request. 

解决方案

  1. 临时解决(以chrome为例) — 打开:chrome://net-internals/#hsts
    找到: "Query HSTS/PKP domain" 找到报错的域名 浏览器有没有记录

    image.png

    然后找到:"Delete domain security policies" 删掉记录。


    image.png

    立刻就能访问,但是这种方法当用户再次访问了https之后,仍然会报错

  2. nginx端解决:

将: add_header Strict-Transport-Security max-age=31622400
改为: add_header Strict-Transport-Security max-age=0;

  1. 前端解决:
    所有配置了https的域名都采用 https链接访问保持一致性

FAQ:

  1. 为什么清空缓存就能暂时解决?
    -- 因为清除了浏览器中关于此域名的HSTS的记录,在用户访问https之前不会再次出现问题
  2. 为什么是偶发现象?
    -- 因为大部分请求是简单请求,例如get请求。所以直接跨域的问题就通过了
  3. 为什么有的域名nginx也配置了add_header Strict-Transport-Security max-age=31622400 但从来没有跨域问题?
    -- 可能是域名的证书并没有生效,导致浏览器的没有留下HSTS记录。

参考

  • 强制HTTPS (HSTS) 导致 CORS preflight 请求失败的问题
  • 跨域资源共享 CORS 详解
  • HTTP Strict Transport Security

你可能感兴趣的:(2021-09-13 一文解析前端请求307导致CORS跨域失败)