同源策略
浏览器的重要功能
-
定义
- 协议、端口、主机名(域名或IP)都相同称为 同源
-
用途
- 不同源的网页,不能共享数据/偷数据
-
为什么只有浏览器有同源策略(插件,本地控制台等等都可以跨域)
- 为了保证用户的数据安全
-
不同源的两个网址如何互相请求数据(俗称 跨域)的方法:
- CORS
- JSONP
- 反向代理
CORS
步骤:
- 假设b.com:7777 想要访问 a.com:8888 的 /data接口(POST/GET)
- 方法: 后端在 /data接口 设置 响应头: Access-Control-Allow-Origin: b.com:7777
这样数据返回回来的响应头里就有:Access-Control-Allow-Origin: b.com:7777 这句,浏览器就会认为你们两个网站达成了某种协议,就不会阻止了
但是:在某些复杂的跨域请求上,只设置上面的响应头依旧会被浏览器阻止
浏览器把跨源资源共享机制(跨域)分成了简单请求和复杂请求
简单请求
若请求满足所有下述条件,则该请求可视为简单请求:
-
只允许使用下列方法之一:
-
发送的请求只允许设置以下请求头:
Accept
Accept-Language
Content-Language
Content-Type
(需要注意额外的限制)Range
(只允许简单的范围标头值 如bytes=256-
或bytes=127-255
)
备注: Firefox 还没有将 Range
实现为安全的请求标头。参见 bug 1733981。
-
Content-Type
的值仅限于下列三者之一:text/plain
multipart/form-data
application/x-www-form-urlencoded
-
如果请求是使用
XMLHttpRequest
对象发出的,在返回的XMLHttpRequest.upload
对象属性上没有注册任何事件监听器;也就是说,给定一个XMLHttpRequest
实例xhr
,没有调用xhr.upload.addEventListener()
,以监听该上传请求。 -
请求中没有使用
ReadableStream
对象。 -
以上为简单请求的条件,简单请求跨域只要后端在响应头设置 Access-Control-Allow-Origin 即可
复杂请求
与简单请求不同,“复杂请求”浏览器会先使用
OPTIONS
方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响,如果预检请求不通过,那么该复杂请求不会发出去,直接被浏览器毙掉
如下是一个复杂请求
const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://b.com:7777/data');
xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');
上面的代码是一个POST请求,该请求包含了一个非标准的请求头:X-PINGOTHER,另外该请求的Content-Type为application/xml,这不符合简单请求的要求,所以这是一个复杂请求,浏览器会先发一个预检请求,如果不通过,请求则不会发出
-
那么跨域复杂请求怎么处理呢
-
后端处理设置响应头如下:
- Access-Control-Allow-Origin: b.com:7777
- Access-Control-Allow-Methods: POST,GET,OPTIONS
- Access-control-Allow-Headers: X-PINGOTHER, Content-Type
-
了解更多 : MDN-跨源资源共享(CORS)
总结:CORS解决跨域问题:需要后端去做
JSONP
利用了script标签不受同源策略约束,可以任意请求JS的原理(script标签可以执行一段JS,但是得不到JS的内容)
步骤
- 假设b.com:7777 想要访问a.com:8888 的 /data接口
- b.com:7777 前端写的
// 创建一个script标签
const script = document.createElement('script')
// 创建一个随机名字,并成为window的属性
let callName = 'JSONP' + Date.now()
window[callName] = (res) => {
console.log('从a.com得到的数据', res)
}
// script的地址为http://a.com:8888/data加上callback参数为刚刚创建的随机名字,会下载这个文件并执行
script.src = "http://a.com:8888/data?callback="+callName
// 把script标签放在head下面
document.head.appendChild(script)
// 当script文件下载完毕,删除window[callName]函数
script.onload = () => {
delete window[callName]
}
- a.com:8888 后端写的
if (path === '/data') {
response.statusCode = 200
// 设置Content-Type为script
response.setHeader('Content-Type', 'text/javascript;charset=utf-8')
// 返回下面的字符串,前端js下载完后就会直接执行,那就相当于执行了window.${query.callback}这个函数,参数为....
response.write(`window.${query.callback}({"name": "Jack"})`)
response.end
}
- 前端创建一个script标签
- 创建一个随机名字, 如name1314
- 定义window.name1314这个函数
- 请求a.com:8888/data?callba…
- 把script标签放在head下面,这样页面一执行就能下载这个script,并执行里面的js
- 等script下载完毕后删除window.name1314这个方法
- 后端设置响应头的Content-Type为text/script
- 获取到传过来的callback参数(name1314),返回window.name1314({"name": "Jack"}), 这个函数里面的对象就是后端要传的值
- 这样页面一打开,就会去下载a.com:8888/data这个路径里的代…
- 相当于执行了 window.name1314({"name": "Jack"})
- 而上面第3步的时候就定义了这个函数,所以在这个函数的形参就能拿到后端返回的值,拿到返回后做什么,可以在函数里面写
总结:JSONP现在已经不用了,因为缺点也很明显,它只能GET请求,而且也是需要后端支持,前端无法独立完成
反向代理
- 一般通过nginx配置实现
- 后端实现
总结
跨域三大解决方法
- CORS
- JSONP
- 反向代理
都需要后端支持或者全由后端实现,前端没有办法依靠自己实现跨域操作