对象存储COS跨域CORS问题小结
跨源资源共享(CORS)
CORS(Cross-origin resource sharing) 中文名称"跨域资源共享",由于安全原因,Web 应用程序默认情况只能在同源(协议、域名和端口)的情况下向服务器获取数据。
主要有以下三种行为会受到限制:
Cookie、LocalStorage 和 IndexDB 无法读取。
DOM 无法获得。禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
AJAX 请求不能发送(XMLHttpRequest)。
但是在日常的业务开发中,我们是需要经常访问跨域资源的。CORS 机制允许 Web 应用进行跨源访问,需要浏览器和服务器同时支持。整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。
1. 请求分类
CORS 将请求分成了两类:简单请求(Simple Request)和非简单请求。其中非简单请求会触发预检请求(Preflight Request)。满足以下两大条件的请求就属于简单请求。
请求方法为下列方法之一
GET
HEAD
POST
请求的 HTTP 的头部信息不超出以下几种字段
Accept
Accept-Language
Content-Language
Content-Type -> { text/plain, multipart/form-data, application/x-www-form-urlencoded }undefinedDPR
Downlink
Save-Data
Viewport-Width
Width
这是为了兼容表单(form),因为历史上表单一直可以发出跨域请求。
凡是不满足上面两个条件,就属于非简单请求。例如 COS V5 版本的 XML 接口中,当 Content-Type 为 application/xml 时就会触发 CORS 预检请求。
2. 简单请求
对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个 Origin 字段。下面我们先看一下 COS 服务器端对于跨域访问 CORS 设置中的各参数的配置作用,并给出结果图。
2.1 浏览器端
浏览器在发起跨域请求时会自动向 HTTP Header 添加一个额外的请求头字段:Origin。Origin 字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。
另外还有三个 Sec-Fetch-*
开头的字段,这是一个新的草案 Fetch Metadata Request Headers。
Sec-Fetch-Mode
代表请求的模式,主要有 cors、navigate、nested-navigate、no-cors 等等。
Sec-Fetch-Site
代表请求的来源是同源还是跨域。
2.2 COS 服务器端
Access-Control-Allow-Origin -> 来源 Origin
作用:COS 服务端允许跨域的源。
如果 Origin 指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin
字段,就知道出错了,从而抛出一个错误,被 XMLHttpRequest 的 onerror 回调函数捕获。
Access-Control-Allow-Methods -> 操作 Methods
作用:HTTP 请求方法的限制
这个参数应该还是比较容易理解的,允许浏览器的 CORS 请求会用到哪些 HTTP 方法。
Access-Control-Expose-Headers -> Expose-Headers
作用:允许浏览器端能够获取相应的 header 值
CORS 请求时,如果服务器端没有设置对应的Access-Control-Expose-Headers
字段,浏览器通过请求响应后的 Header 如下,比如我们非常熟悉的 x-cos-request-id
、ETag
等头部无法在浏览器中无法获取到。
在 COS CORS 设置中把Expose-Headers
置为*
再来看一下结果
就可以拿到 COS 服务器端返回的全部 Header 字段了
Access-Control-Allow-Credentials
作用:是否允许发送 Cookie
这个头部在 COS CORS 设置中并没有对应的选项,如果要发送 Cookie,Access-Control-Allow-Origin
就不能设为星号,必须指定明确的、与请求网页一致的域名。当 Access-Control-Allow-Origin
指定源后,COS 服务器端会自动设置该字段为 true。
当然,如果需要使用该特性,开发者也必须在请求时打开withCredentials
属性。
3. 非简单请求
预检请求是在发送实际请求前,客户端先发送一次 OPTIONS
方法请求到服务器端来确认请求是否通过,可以避免跨域请求对服务器的用户数据造成影响。
如何判断是否会发送预检请求可以参考第一部分的请求分类。
3.1 浏览器端
预检请求用的请求方法是 OPTIONS
,表示这个请求是用来询问的。
当然也需要带上 Origin
字段。
除了 Origin 字段,预检请求的头信息还包括两个特殊字段 Access-Control-Request-Method
和 Access-Control-Request-Headers
。
Access-Control-Request-Method
该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法。如 PUT、POST、GET 等。
Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段。
如上图在请求的时候加上了自定义头部 X-Custom-Header = shuoweiwu
,所以触发了预检请求。
Provisional headers are shown
字面意思是"显示了临时报文头",代表请求被阻塞,未收到响应,说明 请求并没有发出去。
看下控制台报错和 Network 里的请求详情,是由于预检请求报错 403,被浏览器拦截了。
Access to XMLHttpRequest at 'https://xxxxxxxxx-xxxxxxxxxx.cos.ap-guangzhou.myqcloud.com/test/3.jpg' from origin 'http://127.0.0.1:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
3.2 COS 服务器端
Access-Control-Allow-Origin
和 Access-Control-Allow-Methods
同简单请求一样,分别用于判定请求源和请求方法,需要注意的是以下两个配置项。
Access-Control-Allow-Headers -> Allow-Headers
作用:表示服务器允许请求中携带的请求头部字段。
比如上面预检请求中的 X-Custom-Header
头部。
Access-Control-Max-Age -> 超时 Max-Age
作用:指定本次预检请求的有效期,单位为秒。
如果设置 超时 Max-Age
为 0,则浏览器发送请求的时候始终都会先发送 OPTIONS 预检请求。
COS 中的 CORS 配置:
预检请求:
实际请求:
超时 Max-Age
设置为 600 时,只有在第一次请求时发送了 OPTIONS 预检请求。
4. 跨域重定向
当跨域请求被重定向时,中间服务器返回的 CORS 相关的响应头应当与最终服务器保持一致。 任何一级的 CORS 失败都会导致 CORS 失败。即需要满足每一级的 CORS 都能够通过验证。
浏览器会直接访问重定向后的地址,可以跟随多次重定向。但是需要注意的一点就是:
重定向后请求头 Origin 字段会被设为 null
重定向请求:
重定向后 Origin 字段被置为 null,导致重定向后的跨域请求失败:
解决方法有以下两种:
- 设置每一级的
Access-Control-Allow-Origin
字段为*
- 设置重定向后的
Access-Control-Allow-Origin
字段为null
5. 跨域的 src 属性
具有 src 属性的 HTML 标签都可以跨域
<link>,<script>,<img>
等标签是可以直接进行跨域访问的,但是不会产生跨域头。
6. 常见问题总结
当然这里最常见的问题就是已经配置好了跨域头,用 curl 测试生效,但是在前端页面访问的时候没有生效,看 Network 的请求头里确实是没有 CORS 的相关字段。
由于img
标签是可以直接进行跨域访问的,在请求 COS 前,img
标签加载了同样的图片,因为img
加载在前,等到访问 COS 中的资源的时候,浏览器直接使用了缓存,缓存中是没有跨域头的,导致了跨域失败。
如何判定有可能是命中了浏览器缓存?
- 请求的时候存在
Provisional headers are shown
字段,如上所述,代表请求没有发出来,有可能是命中了浏览器缓存。 - AJAX 请求的 COS 资源的 request id 与 img、script 标签等一致。
- Network 栏里的 Remote Address 为空或者 size 中的值为(disk cache)。
- 打开调试器的 disable cache 选项后观察看下能否复现。
使用场景以及命中浏览器缓存后的解决方案:
- 直接访问COS源站
- 使用
Cache-Control
头部关闭缓存。如在 COS 上传的时候加上该头部Cache-Control:no-cache
,或者复制该资源的时候加上该头部。如果对象数量不是很多,可以直接在COS控制台点开该对象详情,设置自定义Headers。 - 设置
<img>
标签的 crossorigin 属性的值为 anonymous,强制图片每次请求都使用 XHR 的 CORS 请求。 - AJAX 请求图片的时候加上随机参数。
- 使用
- 访问CDN域名,CDN回源到COS
如果只在COS侧配置了跨域,但是没有在CDN配置的话,由于CDN会缓存住第一次访问的请求,第一次请求没有跨域的话CDN会缓存住这个头部,可能会导致后面的跨域请求失败了,所以这种场景下建议在CDN侧下发跨域配置。还有一种场景是一个COS域名对应多个CDN域名时,也是由于CDN的缓存问题,可能会导致各个CDN域名表现不一致,这种场景也建议在CDN配置跨域头部。CDN 自定义响应头配置- 仍然可以使用COS的
Cache-Control
头部关闭缓存,并且刷新对应的CDN的URL。 - 设置
<img>
标签的 crossorigin 属性的值为 anonymous,强制图片每次请求都使用 XHR 的 CORS 请求。 - AJAX 请求图片的时候加上随机参数。
- 仍然可以使用COS的
ps: 其中设置 <img>
标签的 crossorigin 属性的方式是可以使用本地缓存的,但是可能有些浏览器是不支持 crossOrigin 的。
Vary头部 -> COS对跨域的进一步支持
Vary头部的使用场景是本地浏览器通过多个域名访问同一个URL,带上Vary头部后浏览器会缓存住不同Origin的请求,这个头部COS侧会尽快安排上,丰富产品的特性。
其他常见问题:
- 重定向后跨域失败 -> 判断是否满足每一级的 CORS 验证
- 浏览器无法获取到如
ETag
等字段 -> 参考上面 CORS 的 Expose Header 的配置
Reference: