HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

时间:2022-12-19 07:58:57

同源策略

域名地址由协议(protocol)域名(host)端口(port)请求资源地址等部分组成。

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

如果两个 URL 的 协议域名 和 端口 都相同,我们就称这两个 URL 同源

同源策略(same-origin policy)是一种出于浏览器安全方面的考虑而出台的一种策略,它可以保护用户信息的安全,防止恶意的网站窃取、身份伪造等。同源策略会阻止一个域的JS脚本和另外一个域的内容进行交互(只允许与本域下的接口交互)。不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源。

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

注意:很多人以为同源策略是浏览器不让请求发出去、或者后端拒绝返回数据。实际情况是,请求能正常发出,后端接口正常响应,只是数据到了浏览器后被丢弃了

受同源策略限制的方面:

1)DOM节点,限制不同源JS脚本对当前DOM对象的读写操作;

2)数据层面,限制不同源站点读取当前站点 Cookie、IndexDB、LocalStorage等数据。

3)网络层面,限制通过 XMLHttpRequest 等方式将站点的数据发送给不同源的站点(即AJAX的跨域问题)。

浏览器遵守同源策略,同时有三个标签又允许跨域加载资源:

<img src=’’>

<link href=‘’>

<script src=‘’>

所以 HTML 中引入外站的图片、样式、脚本等不会因跨域报错。这些标签一般是加载静态资源的,和后端关系不大,我们应该关心如何解决 AJAX 跨域问题。


跨域

当一个请求 URL 的协议、域名、端口三者之间任意一个与当前页面 URL 不同即为跨域。由于同源策略的存在,当从一个域的网页去请求另一个域的资源时(如:XMLHttpRequest 和 Fetch),就无法成功获取到资源。

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

提示:以上在协议不同、主域名不同、子域名不同和端口号不同的情况下,向不同源的 URL 发送请求就是跨域。

要解决 AJAX 请求跨域的问题,网上有不少的总结:

CORS、postMessage、JSONP、WebSocket

扩展阅读:​​跨域解决方法​

本文主要谈谈 CORS。


CORS介绍

CORS 是跨域资源共享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,属于跨域 AJAX 请求的根本解决方法。

CORS 由一系列传输的 HTTP 头组成,这些 HTTP 头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应。

同源安全策略默认阻止“跨域”获取资源。但是 CORS 给了web服务器这样的权限,即服务器可以选择,允许跨域请求访问到它们的资源。


CORS头信息

Access-Control-Allow-Origin

指示请求的资源能共享给哪些域。

Access-Control-Allow-Credentials

指示当请求的凭证标记为 true 时,是否响应该请求。

Access-Control-Allow-Headers

用在对预请求的响应中,指示实际的请求中可以使用哪些 HTTP 头。

Access-Control-Allow-Methods

指定对预请求的响应中,哪些 HTTP 方法允许访问请求的资源。

Access-Control-Expose-Headers

指示哪些 HTTP 头的名称能在响应中列出。

Access-Control-Max-Age

指示预请求的结果能被缓存多久。

Access-Control-Request-Headers

用于发起一个预请求,告知服务器正式请求会使用那些 HTTP 头。

Access-Control-Request-Method

用于发起一个预请求,告知服务器正式请求会使用哪一种 HTTP 请求方法。

Origin

指示获取资源的请求是从什么域发起的。

出于安全原因,浏览器限制从脚本内发起的跨域HTTP请求。 例如,XMLHttpRequest和Fetch API遵循同源策略。

CORS 机制,使用额外的 HTTP 头来告诉浏览器让运行在一个 Origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定资源。这意味着正确设置 CORS 头信息,使用这些API的Web应用就可以跨域了。

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

什么情况需要CORS

跨域资源共享标准允许在下列场景中使用跨域HTTP请求:

1)由 XMLHttpRequest 或 Fetch 发起的跨域 HTTP 请求。

2)Web 字体 (CSS 中通过 @font-face 使用跨域字体资源)。

网站可以发布 TrueType 字体资源,并只允许已授权网站进行跨站调用。

3)WebGL 贴图。

4)使用 drawImage 将 Images/video 画面绘制到 canvas。


CORS机制

跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。

另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法,浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

CORS请求失败会产生错误,但是为了安全,在 JavaScript 代码层面是无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

意思是说:从A到B的AJAX请求已被 CORS策略阻止:出现了不允许(预定义之外)的请求头字段;

扩展阅读:​​HTTP 的 OPTIONS 预检请求简介、特点、触发和优化​


跨域请求场景

客户端文件 ajax_post.html,其中原生的AJAX代码如下:

const xhr = new XMLHttpRequest
xhr.open('POST', 'http://127.0.0.1:8000/server')
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.send()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
console.log(xhr.response);
}
}
}

提示:客户端文件域 http://127.0.0.1:5500/ 向另一个域 http://127.0.0.1:8000 发送 POST请求,是典型的跨域;

后端代码:使用express框架搭建的Web服务;

const express = require('express')
const app = express()
app.post('/server', (request, response) => {
response.setHeader('Access-Control-Allow-Origin', '*')
console.log(request.method);
response.send('Hello Ajax post!')
})
app.listen('8000', () => {
console.log('Web服务已经启动,端口8000监听中... ...');
})

提示:只需在服务端添加共享域为 “*”,意思是不限来源,就可以实现跨域。

response.setHeader('Access-Control-Allow-Origin', '*')

控制台输出:正常输出。

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

响应头信息:

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

不设置 CORS 头信息的话就会报错:

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

请求被 CORS 策略阻止;


预检请求场景

预检请求是首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求,正确响应后才可以发起真正的数据请求。

什么情况会触发OPTIONS

1)跨域请求,非跨域请求不会出现options请求;

2)自定义请求头,如客户端:

xhr.setRequestHeader('name', 'Jacky')

3)请求头中的 Content-Type 出现了以下三种之外的格式;

Content-Type: application/x-www-form-urlencoded   表单提交

Content-Type: multipart/form-data   文件上传

Content-Type: text/plain  文本

扩展阅读:​​HTTP Content-Type​​ 

如客户端定义:

xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8')

当满足条件12或者13的时候,AJAX 请求就会出现 OPTIONS 预检请求。


看下预检请求示意图

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题



预检请求示例

修改上例客户端代码:

const xhr = new XMLHttpRequest
xhr.open('POST', 'http://127.0.0.1:8000/server')
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.setRequestHeader('name', 'Jacky')
xhr.send()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
console.log(xhr.response);
}
}
}

提示1:同样是跨域;

提示2:自定义头信息字段;

即跨域了又使用了自定义头信息字段,所以会触发OPTIONS预检请求;

修改上例服务端代码:

const express = require('express')
const app = express()
app.all('/server', (request, response) => {
response.setHeader('Access-Control-Allow-Origin', '*')
response.setHeader('Access-Control-Allow-Headers', '*')
console.log(request.method);
response.send('Hello Ajax post!')
})
app.listen('8000', () => {
console.log('Web服务已经启动,端口8000监听中... ...');
})

提示:后端又添加了 ​Access-Control-Allow-Headers​ 头信息,允许自定义头信息。

控制台输出:

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

后端服务输出:

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

提示:这次连接一共发起两次请求,分别是 OPTIONS 和 POST;

Network 查看请求信息:

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

开发者工具中的 Network 中也可以看到这两次请求:

其中一次是 preflight 即 OPTIONS预检请求。

查看请求头信息:

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

提示1:OPTIONS 是 HTTP/1.1 协议定义的方法,用以从服务器获取更多信息,该方法不会对服务器资源产生影响。

提示2:请求行中显示本次请求方法是 OPTIONS;(红框)

提示3:预检请求中携带了两个 CORS 请求头字段;(黄框)

Access-Control-Request-Method: POST

告知服务器,实际请求将使用 POST方法;

Access-Control-Request-Headers: name

告知服务器,实际请求携带自定义请求头字段:name

查看响应头信息:

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

提示1:响应行显示OK;

提示2:CORS 头信息响应;

Access-Control-Allow-Origin: *

不限制请求域,也可以设置具体请求的域 http://127.0.0.1:5500;

Access-Control-Allow-Headers: *

不限制自定义请求头,也可以设置具体的头信息字段,如:name


预检请求优化

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题

触发预检请求时,跨域请求便会发送 2 次请求,既增加了请求数,也延迟了实际请求发起的时间,影响性能。

添加一行头信息,把预检请求的响应缓存起来,在有效期内只需一次预检;

response.setHeader('Access-Control-Max-Age', '600')

提示:设置600秒缓存时间,有效期内,不会再次触发预检;

HTTP访问控制(CORS)、同源策略、跨域和预检请求等问题