跨域详解

308 阅读9分钟

跨域一直是个经久不衰的问题,无论在初级或者中高级前端面试中,貌似回答不出几个跨域的解决方案就能判定你经验不足或者工作只是混混经验。前几年的面试中遇到此类问题我都会简单答答 CORS JSONP之类,对于具体细节并没有去充分准备,心里也挺没底的,很显然对于面试官而言,我不是个合格或者良好的候选人。

既然面试官喜欢问,何不花点时间学习学习?在繁琐却重复的前端工作中,抽出时间来巩固基础知识,以后再遇到此类问题,才能淡定从容。

什么是跨域?

说到跨域都会先提及浏览器的同源策略

同源策略

同源策略(SOP: same-origin policy)是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。--MDN

用大白话来说,同源策略是浏览器中实现的一个重要安全策略,其目的是为了保护浏览器中的用户安全,减少恶意攻击。

同源定义

同源指的是三个相同,如果其中任意一个不满足就可称为非同源

  • 协议相同

  • 域名相同

  • 端口相同(其中http(s):// 默认端口是80)

以链接 http://store.company.com/dir/page.html 作为源示例

URL结果原因
store.company.com/dir2/other.…同源只有路径不同
store.company.com/dir/inner/a…同源只有路径不同
store.company.com/secure.html非同源协议不同
store.company.com:81/dir/etc.htm…非同源端口不同
news.company.com/dir/other.h…非同源域名(二级域名)不同

同源策略的限制

同源策略是个安全策略,它主要限制了浏览器下以下几点

  • 数据存储限制:Cookie, LocalStorage, IndexDB 无法读取(如访问非同源的网页cookie)

  • 脚本 API 限制:DOM 无法操作(如访问非同源iframe DOM,JS变量)

  • 网络请求限制:XHR 请求无法接收响应(如请求域名和网页域名非同源)

特别注意,在跨域请求中,请求实际上已经发出,并且无论是否跨域都会携带cookie,但是浏览器拦截了响应

但是以下几个虽然属于非同源操作,但是没有被同源策略禁止

  • 跨域资源(link script img video audio)(font字体文件有跨域问题)

  • 表单跨域(跨域的form可正常提交)

  • 重定向,a标签链接

  • ajax可以正常发送请求,可以携带cookie(withCredentials),不能接收到响应

跨域

前面我们学习了同源策略同源的概念,现在可以引申出跨域的概念:跨域指的是一个origin的文档或者它加载的脚本与另一个源的资源进行交互。有些跨域是被禁止的,因为同源策略的限制,但有些跨域是被允许的,并未被同源策略所禁止。

跨域的方法

我们所说的跨域方法就是突破被同源策略禁止的跨域限制,让跨域交互正常进行。

针对不同的跨域场景,我们跨域方法分为两类

  1. 数据存储限制与脚本API交互

  2. ajax请求

数据存储限制与脚本API交互(cookie,JS变量访问)

正常父页面可以通过iframeName(标签上的name属性)访问iframe页面的window变量,iframe可以通过parent访问父页面的window变量,其cookie和localStorage通用(实际操作同一个对象)。但是在跨域情况下,不再可以访问window变量,cookie和localStorage也不通用

document.domin + iframe跨域

适用场景:iframe下的二级域名跨域(主域相同)

解决问题:跨域下的父页面和iframe页面的资源交互限制

  • DOM操作

  • 变量读写

  • cookie,localstorage读写(父子页面实际操作的是同一个对象,也就是说在iframe中设置的cookie会覆盖父页面cookie,同理父页面设置也会覆盖iframe)

实现方法:使用 document.domain 将两个页面的 document.domain 设置为同一个主域即可实现跨域

a页面(parent.example.com)

<iframe src="http://child.example.com/a.html" id="child">

<script>
  document.domain = 'example.com';

  var page = 'parent';

  document.cookie = 'user=xxx';

  // 确保iframe已加载
  window.onload = () => {
    console.log('child', document.getElementById('child').contentWindow.page); // parent
    console.log('child', document.getElementById('child').contentWindow.document.cookie); // user=yyy
  }
</script>

b页面(child.example.com)

<script>
  document.domin = 'example.com';

  var page = 'child';

  // 实际覆盖了a页面设置的cookie
  document.cookie = 'user=yyy';

  console.log('parent', window.parent.page, window.parent.document.cookie); // parent user=yyy
</script>

location.hash + ifrmae跨域

使用场景:iframe下的跨域

解决问题:跨域下页面之间的单向通信

实现方法:

  1. 在父页面a通过设置b页面的hash链接来传递消息

  2. 在b页面通过onhashchange函数来获取a页面信息,再通过设置子页面c的hash向c页面传递消息

  3. 在页面c也通过onhashchange函数来获取来自b页面的信息,再传递给同域的a页面

  4. 至此完成 a->b->c->a 的单向数据传递

a页面(new.example.com/a.html)

<iframe src="http://b.company.com/b.html"></iframe>

<script>
  function callBack(msg) {
    console.log('receiving:' + msg);
  }

  const iframe = document.querySelector('iframe');

  setTimeout(() => {
    const msg = JSON.stringify({
      msg: 'msgFromA'
    });

    iframe.src += '#' + msg
  }, 2000);
</script>

b页面(b.company.com/b.html)

<iframe src="http://new.example.com/c.html"></iframe>

<script>
  const iframe = document.querySelector('iframe');

  window.onhashchange = () => {
    iframe.src += '#' + location.hash.slice(1);
  }
</script>

c页面(new.example.com/c.html)

<script>
  window.onhashchange = () => {
    let msg = location.hash.slice(1);

    msg = JSON.parse(decodeURIComponent(msg));

    window.parent.parent.callBack(msg);
  }
</script>

window.name + iframe跨域

适用场景:iframe下的跨域

解决问题:跨域下页面之间的单向单次数据通信

实现方法:

主要利用浏览器窗口(包括iframe)加载不同页面时window.name的值保留

  1. 父页面a加载跨域页面b

  2. 在b中设置window.name为通信数据

  3. 切换iframe的链接指向与a同源的页面c,此时页面c的window.name为跨域页面b中设置的值

  4. 通过监听页面c加载获取数据,至此完成b->c->a的单向单次数据通信

a页面(a.example.com/a.html)

<iframe src="http://new.example/b.html"></iframe>

<script>
  const iframe = document.querySelector('iframe');
  let state = 0;

  function callBack() {
    console.log(iframe.contentWindow.name);
  }

  iframe.onload = () => {
    if (state === 0) {
      iframe.src = 'http://a.example.com/c.html';
      state++;
    } else if (state === 1) {
      callBack();

      // 销毁iframe,防止内存泄漏
      iframe.contentWindow.document.write('');
      iframe.contentWindow.close();
      document.body.removeChild(iframe);
    }
  }
  </script>

b页面(new.example.com/b.html)

<script>
  window.name = 'b';
</script>

c页面(a.example.com/c.html)

c页面作为信息传递的介质,可以为空页面

postMessage跨域

使用场景:iframe下的跨域,多窗口跨域通信

解决问题:iframe(多窗口)下的双向通信

实现方法:

  1. 多窗口跨域通信

如果打开窗口和源窗口同源,源窗口可以通过open方法返回的对象访问打开窗口的window变量,打开窗口可以通过opener对象访问源窗口的window变量。跨域下不再可以进行访问引用。(不能访问window里的变量,但是能调用window.postMessage)

父页面(parent.example.com)

<script>
  const openWin = window.open('http://child.example.com/a.html');

  // 发送消息到打开的窗口
  setInterval(() => {
    // 发送过程中会将data进行stringify拷贝 
    openWin.postMessage({name: 1}, '*');
  }, 2000);

  // 监听打开窗口发送的消息
  window.addEventListener('message', (e) => {
    console.log(e.type, e.data, e.origin)
  }, false);
</script>

子页面(child.example.com)

<script>
  setInterval(() => {
    opener.postMessage({name: 2}, '*');
  }, 2000);
  
  window.addEventListener('message', (e) => {
    console.log(e.type, e.data, e.origin)
  }, false);
</script>
  1. ifrmae跨域通信

ifrmae下跨域使用postMessage方法通信的原理其实和多窗口下是一样的,只是将对应的方法引用改为parentiframeName,在此就不加以详述了

ajax请求

jsonp跨域

jsonp应该是个大家都比较熟悉且常用的跨域方法了,在面试中也被大家常常提及

适用场景:GET请求跨域(无法解决POST跨域)

解决问题:跨域下的AJXAX请求

实现方法:利用script等资源标签的加载,如果跨域也不会被同源策略限制的“漏洞”。将请求参数放在资源链接上,同时后端配合资源请求,返回立即执行脚本并携带对应的响应值。

前端JS

// JSONP回调函数
function done(data) {
  console.log(data)
}

// JSONP函数
function JSONP(callBack) {
  const script = document.createElement('script');

  // callBack为响应函数名,也可以添加其它参数在链接上
  script.src = 'http://new.example.com/index.js?callBack=' + callBack;

  script.onload = () => {
      document.body.remove(script);
  }

  document.body.appendChild(script);
}

// 使用JSONP请求
JSONP('done');

后端实现(node)

// ...
const msg = {
  method: 'jsonp'
}

// 获取回调函数名
const callBack = url.match(/(?<=.*callBack=).*/g);

// 返回script资源,内容为立即执行函数,其中msg为响应值
res.write(`${callBack}(${JSON.stringify(msg)})`);
res.end();
// ...

CORS跨域

CORS是当前前后端跨域的一种流行解决方案,也是我面试最常回答的,那么它究竟是如何去实现跨域的呢?

CORS(cross-origin resource sharing)(跨源资源共享)是一种机制,该机制使用附加的 HTTP 头来告诉浏览器,准许运行在一个源上的Web应用访问位于另一不同源选定的资源。--MDN

简单地说,CORS是一种利用 HTTP 头达成的跨源资源共享。

适用场景:ajax跨域请求

解决问题:跨域下的AJAX请求

实现方法:设置http响应头来达到CORS

前端其实是正常的跨域请求

const invocation = new XMLHttpRequest();

// 本域 http://xxx.com:8888
const url = 'http://xxx.com:8080/api/getname';

function callOtherDomain() {
    if(invocation) {    
        invocation.open('GET', url, true);
        // 可选:跨域cookie携带
        invocation.withCredentials = true;
        invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
        invocation.onreadystatechange = (state) => {
            // handler
        };
        invocation.send(); 
    }
}

后端设置(node)(xxx.com:8080)

// 设置可接受域,可以设置为*表示接受所有域名
res.setHeader('Access-Control-Allow-Origin', 'http://xxx.com:8888');
// 可接受的请求方法(用于预检请求)
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
// 可接受的请求首部字段(用于预检请求)
res.setHeader('Access-Control-Allow-Headers', 'X-PINGOTHER');

// 可选:接受跨域cookie
res.setHeader('Access-Control-Allow-Credentials', 'true');

这样就可以跨域了,其实就是服务器设置可接受的域名,浏览器根据服务器的响应首部来放开跨域限制,不再屏蔽响应

CORS是个内容比较多的知识点,其中涉及简单请求复杂请求预检请求等多个知识点,大家可参考跨源资源共享(CORS),加深学习。

nginx代理跨域

nginx 是 http 服务器,用于处理静态资源(动态资源会交给 tomcat 处理)。我们常常使用其实现反向代理,负载均衡。现在我们用其代理转发来实现跨域。

适用场景:ajax跨域请求

解决问题:跨域下的AJXAX请求

实现方法:nginx代理转发

将原本访问跨域的接口都改为请求同源服务器,同源服务器帮我们去请求跨域服务器,这样就没有跨域限制了。

nginx代理配置

{
  server {
    listen       xxx.com:8888;

    location / {
      # 配置静态资源如(字体文件)的CORS
      add_header Access-Control-Allow-Origin *;
    }
    
    location ~ /api/ {
        # 将api路径的接口都转发
        proxy_pass http://xxx.com:8080;
    }
  }
}

如果接口有设置 cookie 的话,记得要配置proxy_cookie_domain,为cookie 设置正确的domain 和 path

node中间件跨域(vue项目)

node 中间件的跨域和 nginx 的跨域原理一致,都是使用服务器代理转发

适用场景:本地起的vue项目跨域

解决问题:跨域下的AJAX请求

实现方法:node 服务器代理转发

其中vue配置如下

// ···
devServer: {
  proxy: {
    '/api': {
        target: 'https://xxx.com',
        changeOrigin: true,
        pathRewrite: {
            // 重写请求连接,去除api
            '^/api': ''
        }
    }
  }
}
// ···

websocket跨域

同源限制是针对 http 请求的跨域,对于同处于应用层的 webSocket 协议来说,是没有同源限制问题的,所以我们可以使用其实现跨域请求。

适用场景:跨域通信

实现方法:webSocket双工通信

演示代码

前端

// 创建WebSocket
let socket = new WebSocket('ws://xxx.xxx.xxx.xxx:3000');

// 连接
socket.addEventListener('open',()=>{
  console.log('连接成功')

  // 发送消息
  socket.send('客户端消息');

});

// 监听消息
socket.addEventListener('message', (msg)=>{
  console.log('message:', msg.data)
});

// 断开
socket.addEventListener('close',()=>{
  console.log('服务断开');
});

服务端(node)

const ws = require('nodejs-websocket');

// 创建server
const server = ws.createServer(connect => {
  console.log('用户连接成功');

  // 监听消息
  connect.on('text', data => {
    console.log('message:' + data);
    // 发送数据
    connect.sendText('服务端消息');
  });

  // 断开
  connect.on('close',()=>{
    console.log('连接断开');
  });

  connect.on('error',()=>{
    console.log('用户连接异常');
  });
});

server.listen(3000, 'xxx.xxx.xxx.xxx', () => {
  console.log('监听3000');
});

总结

对于不同的跨域场景有不同的跨域方法,掌握它们可以巩固我们的前端基础。以前我都是在面试前看看文章准备,面试后就忘记了。这次通过实践并博客记录,希望能加深理解与记忆。同时希望可以帮助到大家加深对于跨域的掌握与理解。对于文章中有错误的地方,希望大家可以加以指出提醒。

参考


欢迎来前端学习打卡群一起学习~516913974