同源策略
域名地址由
协议(protocol)
、
域名(host)
、
端口(port)
、
请求资源地址
等部分组成。
如果两个 URL 的
协议
、
域名
和
端口
都相同,我们就称这两个 URL 同源
。
同源策略(same-origin policy)是一种出于浏览器安全方面的考虑而出台的一种策略,它可以保护用户信息的安全,防止恶意的网站窃取、身份伪造等。同源策略会阻止一个域的JS脚本和另外一个域的内容进行交互(只允许与
本域
下的接口交互)。不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源。
注意:很多人以为同源策略是浏览器不让请求发出去、或者后端拒绝返回数据。实际情况是,请求能正常发出,后端接口正常响应,只是
数据到了浏览器后被丢弃了
。
受同源策略限制的方面:
1)DOM节点,限制不同源JS脚本对当前DOM对象的读写操作;
2)数据层面,限制不同源站点读取当前站点 Cookie、IndexDB、LocalStorage等数据。
3)网络层面,限制通过 XMLHttpRequest 等方式将站点的数据发送给不同源的站点(即AJAX的跨域问题)。
浏览器遵守同源策略,同时有三个标签又允许跨域加载资源:
<img src=’’>
<link href=‘’>
<script src=‘’>
所以 HTML 中引入外站的图片、样式、脚本等不会因跨域报错。这些标签一般是加载静态资源的,和后端关系不大,我们应该关心如何解决 AJAX 跨域问题。
跨域
当一个请求 URL 的协议、域名、端口三者之间任意一个与当前页面 URL 不同即为跨域
。由于同源策略的存在,当从一个域的网页去请求另一个域的资源时(如:XMLHttpRequest 和 Fetch),就无法成功获取到资源。
提示:以上在协议不同、主域名不同、子域名不同和端口号不同的情况下,向不同源的 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应用就可以跨域了。
什么情况需要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 代码层面是无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。
意思是说:从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', '*')
控制台输出:正常输出。
响应头信息:
不设置 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 预检请求。
看下预检请求示意图
预检请求示例
修改上例客户端代码:
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
头信息,允许自定义头信息。
控制台输出:
后端服务输出:
提示:这次连接一共发起两次请求,分别是 OPTIONS 和 POST;
Network 查看请求信息:
开发者工具中的 Network 中也可以看到这两次请求:
其中一次是 preflight 即 OPTIONS预检请求。
查看请求头信息:
提示1:OPTIONS 是 HTTP/1.1 协议定义的方法,用以从服务器获取更多信息,该方法不会对服务器资源产生影响。
提示2:请求行中显示本次请求方法是 OPTIONS;(红框)
提示3:预检请求中携带了两个 CORS 请求头字段;(黄框)
Access-Control-Request-Method: POST
告知服务器,实际请求将使用 POST方法;
Access-Control-Request-Headers: name
告知服务器,实际请求携带自定义请求头字段:name
查看响应头信息:
提示1:响应行显示OK;
提示2:CORS 头信息响应;
Access-Control-Allow-Origin: *
不限制请求域,也可以设置具体请求的域 http://127.0.0.1:5500;
Access-Control-Allow-Headers: *
不限制自定义请求头,也可以设置具体的头信息字段,如:name
预检请求优化
触发预检请求时,跨域请求便会发送 2 次请求,既增加了请求数,也延迟了实际请求发起的时间,影响性能。
添加一行头信息,把预检请求的响应缓存起来,在有效期内只需一次预检;
response.setHeader('Access-Control-Max-Age', '600')
提示:设置600秒缓存时间,有效期内,不会再次触发预检;