同源策略
由于浏览器的同源策略, 如果两个页面的协议, 端口 (如果有指定) 和域名都相同, 则两个页面具有相同的源, 只要有一者不同, 就会造成跨域
JSONP
JSONP 由两部分组成: 回调函数和数据. 回调函数是当响应到来时应该在页面中调用的函数. 回调函数的名字一般是在请求中指定. 而数据就是传入回调函数中的 JSON 数据. 形式如下:
- <script>
- var localHandle = function(data){
- alert("返回的数据:" + data);
- }
- </script>
- <script src="http://remoteserver.com/remote.js?name=value&callback=localHandle"></script>
remote.JS 代码如下:
localHandler({"result":"我是远程 js 带来的数据"});
服务器根据请求里的 callback=localHandle 知道了本地的回调函数名称 localHandle, 查询字符串为 name=value, 然后服务器执行相对应的函数并返回数据给本地的 localHandle 回调函数.
建议动态的创建 script 来查询
- <script>
- var localHandle = function(data){
- alert("返回的数据:" + data);
- }
- // 提供 JSONP 服务的 url 地址(不管是什么类型的地址, 最终生成的返回值都是一段 JavaScript 代码)
- var url = "http://remoteserver.com/remote.js?name=value&callback=localHandle";
- // 创建 script 标签, 设置其属性
- var script = document.createElement('script');
- script.setAttribute('src', url);
- // 把 script 标签加入 head, 此时调用开始
- document.getElementsByTagName('head')[0].appendChild(script);
- </script>
JSONP 优点
简单易用, 能够直接访问响应文本, 支持浏览器和服务器之间的双向通信
JSONP 缺点
安全性不足. 借助 JSONP 有可能进行跨站请求伪造 (CSRF) 攻击, 当一个恶意网站使用访问者的浏览器向服务器发送请求并进行数据变更时, 被称为 CSRF 攻击. 由于请求会携带 cookie 信息, 服务器会认为是用户自己想要提交表单或者发送请求, 而得到用户的一些隐私数据.
错误原因不易找. JSONP 缺乏错误处理机制, 如果脚本注入成功后, 就会调用回调函数, 但是注入失败后, 没有任何提示. 这就意味着, 当 JSONP 遇到 404,505 或者其他服务器错误时, 你是无法检测出错原因的. 我们能够做的也只有超时, 没有收到响应, 便认为请求失败, 执行对应的错误回调.
只能适用于 get 请求. 只能使用 GET 请求就意味着很多限制, 提交到服务器的数据量将受限于浏览器的最大 URL 长度.
JSONP 封装
- /**
- * JSONP 请求工具
- * @param url 请求的地址
- * @param data 请求的参数
- * @returns {Promise<any>}
- */
- const request = ({url, data}) => {
- return new Promise((resolve, reject) => {
- // 处理传参成 xx=yy&aa=bb 的形式
- const handleData = (data) => {
- const keys = Object.keys(data)
- const keysLen = keys.length
- return keys.reduce((pre, cur, index) => {
- const value = data[cur]
- const flag = index !== keysLen - 1 ? '&' : ''
- return `${pre}${cur}=${value}${flag}`
- }, '')
- }
- // 动态创建 script 标签
- const script = document.createElement('script')
- // 接口返回的数据获取
- Windows.jsonpCb = (res) => {
- document.body.removeChild(script)
- delete Windows.jsonpCb
- resolve(res)
- }
- script.src = `${url}?${handleData(data)}&cb=jsonpCb`
- document.body.appendChild(script)
- })
- }
- // 使用方式
- request({
- url: 'http://localhost:9871/api/jsonp',
- data: {
- // 传参
- msg: 'helloJsonp'
- }
- }).then(res => {
- console.log(res)
- })
空 iframe 加 form
- const requestPost = ({url, data}) => {
- // 首先创建一个用来发送数据的 iframe.
- const iframe = document.createElement('iframe')
- iframe.name = 'iframePost'
- iframe.style.display = 'none'
- document.body.appendChild(iframe)
- const form = document.createElement('form')
- const node = document.createElement('input')
- // 注册 iframe 的 load 事件处理程序, 如果你需要在响应返回时执行一些操作的话.
- iframe.addEventListener('load', function () {
- console.log('post success')
- })
- form.action = url
- // 在指定的 iframe 中执行 form
- form.target = iframe.name; //target 规定在何处打开 action URL. 这里可以通过 iframe.name 来指定 iframe
- form.method = 'post'
- for (let name in data) {
- node.name = name
- node.value = data[name].toString()
- form.appendChild(node.cloneNode())
- }
- // 表单元素需要添加到主文档中.
- form.style.display = 'none'
- document.body.appendChild(form)
- form.submit()
- // 表单提交后, 就可以删除这个表单, 不影响下次的数据发送.
- document.body.removeChild(form)
- }
- // 使用方式
- requestPost({
- url: 'http://localhost:9871/api/iframePost',
- data: {
- msg: 'helloIframePost'
- }
- })
- document.domain + iframe
通过 document.domain 将两个页面的域名都设置成相同域名, 也可以实现跨域. 不过这个方法只适用于不同子域的框架间的交互
- // a.html
- <iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
- <script>
- document.domain = 'domain.com';
- var user = 'admin';
- </script>
- // b.HTML
- <script>
- document.domain = 'domain.com';
- // 获取父窗口中变量
- alert('get js data from parent --->' + Windows.parent.user);
- </script>
- location.hash
通过 location.hash 来跨域, 原理是改变 url 的 hash 部分来进行双向通信, 父页面向 iframe 子页面通信只需监听自身的 url 变化来发送信息, 而子页面向父页面通信就麻烦一些, 由于两个页面不在同一个域, IE 和 Chrome 都不允许修改 parent.location.hash 的值, 所以需要创建一个和父页面同域的中间页, 中间页可利用 parent.parent 访问父页面的所有对象.
该方法的缺点是会造成不必要的浏览器历史记录, 并且有些浏览器不支持 onhashchange 事件, 数据直接暴露在 url 中, 数据容量和类型都有限等.
- // a.HTML
- <iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
- <script>
- var iframe = document.getElementById('iframe');
- // 向 b.HTML 传 hash 值
- setTimeout(function() {
- iframe.src = iframe.src + '#user=admin';
- }, 1000);
- // 开放给同域 c.HTML 的回调方法
- function onCallback(res) {
- alert('data from c.html --->' + res);
- }
- </script>
- // b.HTML
- <iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
- <script>
- var iframe = document.getElementById('iframe');
- // 监听 a.HTML 传来的 hash 值, 再传给 c.HTML
- Windows.onhashchange = function () {
- iframe.src = iframe.src + location.hash;
- };
- </script>
- // c.HTML
- <script>
- // 监听 b.HTML 传来的 hash 值
- Windows.onhashchange = function () {
- // 再通过操作同域 a.HTML 的 JS 回调, 将结果传回
- Windows.parent.parent.onCallback('hello:' + location.hash.replace('#user=', ''));
- };
- </script>
- Windows.name + iframe
Windows.name + iframe, 利用 Windows.name 在不同页面 (甚至不同域) 加载后依旧存在, 并且支持大小达到了 2MB. 步骤: 首先在 a 页面中创建 iframe, 将 src 指向外域保存数据到 Windows.name, 再将 iframe 的 src 指向和 a 页面同域的 b 代理页面, 借此让 a 页面以 iframe.contentWindow.name 的形式成功读取数据.
- //a.HTML
- var proxy = function(url, callback) {
- var state = 0;
- var iframe = document.createElement('iframe');
- // 加载跨域页面
- iframe.src = url;
- // onload 事件会触发 2 次, 第 1 次加载跨域页, 并留存数据于 Windows.name
- iframe.onload = function() {
- if (state === 1) {
- // 第 2 次 onload(同域 proxy 页)成功后, 读取同域 Windows.name 中数据
- callback(iframe.contentWindow.name);
- destoryFrame();
- } else if (state === 0) {
- // 第 1 次 onload(跨域页)成功后, 切换到同域代理页面
- iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
- state = 1;
- }
- };
- document.body.appendChild(iframe);
- // 获取数据以后销毁这个 iframe, 释放内存; 这也保证了安全(不被其他域 frame JS 访问)
- function destoryFrame() {
- iframe.contentWindow.document.write('');
- iframe.contentWindow.close();
- document.body.removeChild(iframe);
- }
- };
- // 请求跨域 b 页面数据
- proxy('http://www.domain2.com/b.html', function(data){
- alert(data);
- });
- //b.HTML
- <script>
- Windows.name = 'This is domain2 data!';
- </script>
postMessage 跨域
postMessage 是 HTML5 新增的 Windows 属性
用法: postMessage(data,origin)方法接受两个参数
- //a.HTML
- <iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
- <script>
- var iframe = document.getElementById('iframe');
- iframe.onload = function() {
- var data = {
- name: 'aym'
- };
- // 向 domain2 传送跨域数据
- iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
- };
- // 接受 domain2 返回数据
- Windows.addEventListener('message', function(e) {
- alert('data from domain2 --->' + e.data);
- }, false);
- </script>
- //b.HTML
- <script>
- // 接收 domain1 的数据
- Windows.addEventListener('message', function(e) {
- alert('data from domain1 --->' + e.data);
- var data = JSON.parse(e.data);
- if (data) {
- data.number = 16;
- // 处理后再发回 domain1
- Windows.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
- }
- }, false);
- </script>
- CORS
CORS 需要浏览器和服务器同时支持. 目前, 所有浏览器都支持该功能, IE 浏览器不能低于 IE10.
整个 CORS 通信过程中, 都是浏览器自动完成, 对于开发者来说, CORS 和同源的 Ajax 通信没有差别, 浏览器一旦发现 Ajax 跨域, 就会自动添加一些附加的头部信息, 根据请求的不同还会多出一次附加的请求.
实现 CORS 通信的关键是服务器, 只要服务器实现了 CORS 接口, 就实现了跨域
服务器实现 CORS 通信的关键是设置以下请求头
Access-Control-Allow-Origin,
该字段时必选的, 用于设置允许响应资源的 origin
Access-Control-Allow-Origin: *// 允许所有域
Access-Control-Allow-Origin: <origin> 允许指定的 origin
Access-Control-Allow-Credentials 该字段是可选的, 用于设置是否允许发送 Cookie, 该字段的值只能设为布尔值 ==true== , 删除该字段即可不发送. 如果需要发送 Cookie, 这里还需要前端的 Ajax 请求设置 ==withCredentials== 属性, 此外, Access-Control-Allow-Origin 不能设为星号, 必须明确指定与请求网页一致的域名, 并且, Cookie 依旧遵循同源策略, 只有用服务器域名设置的 Cookie 才会上传, 其他域名的 Cookie 并不会上传, 且 (跨源) 原网页代码中的 document.cookie 也无法读取服务器域名下的 Cookie.
- var xhr = new XMLHttpRequest();
- xhr.withCredentials = true;
- Access-Control-Expose-Header
该字段可选, XMLHttpRequest 对象的 getResponseHeader()方法只能拿到 6 个基本字段: Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma. 如果想拿到其他字段, 就必须在 Access-Control-Expose-Headers 里面指定.
CORS 与 JSONP 比较
CORS 与 JSONP 的使用目的相同, 但是比 JSONP 更强大.
JSONP 只支持 GET 请求, CORS 支持所有类型的 HTTP 请求. JSONP 的优势在于支持老式浏览器, 以及可以向不支持 CORS 的网站请求数据.
来源: https://www.cnblogs.com/y-dt/p/10458176.html