# 跨域

# 什么是跨域

跨域是浏览器的同源策略里所衍生出来的词汇,其意为两个origin源的域名、协议、端口号之中有一个不相同,则会造成跨域。

# 同源策略是什么?

同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS(跨站代码注入)、CSRF(跨站请求伪造)等攻击。

# 同源规则

  1. 协议相同 (httphttps等)
  2. 域名相同 (wwwcomcnzhorg等)
  3. 端口号相同(8080803000等)

# 同源示例

以url http://store.company.com/dir/page.html作参照。

URL 结果 原因
http://store.company.com/dir2/other.html 同源 只有路径不同
http://store.company.com/dir/inner/another.html 同源 只有路径不同
https://store.company.com/secure.html 跨域 协议不同
http://store.company.com:81/dir/etc.html 跨域 端口不同 ( http:// 默认端口是80)
http://news.company.com/dir/other.html 跨域 主机不同

# 同源策略限制的内容有哪些?

# · 同源策略限制内容有:

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

# · 但是有三个标签是允许跨域加载资源:

  • <img src=XXX>
  • <link href=XXX>
  • <script src=xxx>

# 为什么需要对跨域做限制?

跨域问题是因同源策略产生的,而同源策略是浏览器最核心最基本的安全手段。即如果不对跨域做限制,将会受到黑客攻击。拿DOM同源策略XMLHttpRequest同源策略来举例说明。

# 如果DOM可跨域

  • 网页中用iframe嵌套一个其他用户需要提交自己信息的页面(例如关系到个人财产的登录页等)
  • 因为没有对跨域做限制,便可以获取iframe嵌套页面的DOM节点,即可以获取节点值(例如账号密码个人隐私)。

# 如果XMLHttpRequest可跨域

  • 黑客可以进行CSRF(跨站请求伪造) 攻击。
  • 假如用户登陆了 http://mybank.com 页面,浏览器中的cookie保存了银行页返回来的用户标识。
  • 同时用户又浏览了黑客做好的假页面,假页面中自执行了AJAX请求,默认将mybank页对应cookie一同发送过去。
  • 银行页验证通过,返回带有用户信息的响应报文。这样,用户的个人数据就泄露了。
  • 由于ajax是后台执行,用户是无法感知的。

# 关于跨域的特别说明

  1. 如果是协议和端口造成的跨域问题“前台”是无能为力的。
  2. 在跨域问题上,仅仅是通过“URL的首部”来识别而不会根据域名对应的IP地址是否相同来判断。“URL的首部”可以理解为“协议, 域名和端口必须匹配”。
  3. 跨域限制并不会限制请求,但会限制响应的返回。
    • 归根结底,跨域是为了阻止用户读取到另一个域名下的内容。
    • Ajax会获取响应内容,浏览器认为不安全,所以会受到跨域限制。
    • form表单提交时不获取新的内容,因此不受限制。
    • 跨域并不能完全阻止 CSRF(跨站请求伪造

# 常见的跨域有哪些?

# JSONP

# · 原理

<script>标签没有跨域限制,因此可以在该标签上得到其他来源动态产生的JSON数据。但JSONP请求一定需要对方的服务器做支持

# · 与AJAX的区别

同样都是客户端给服务器端发送请求,从服务器获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求).

# · 优缺点

  • 兼容性好,可用于解决主流浏览器的跨域数据访问问题。
  • 仅支持get请求。
  • 不安全,可能会遭受XSS攻击。

# · 实现

  • 声明一个回调,形参为( {url,params,callback} ),形参类型为Object, 放置 请求url, 请求体,及函数名callback
  • 创建一个<script>标签,src传入跨域的接口请求地址url。向服务器传递函数名(通过字符串拼接)
  • 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数,对返回的数据进行操作。
/**
 * @param url string
 * @param params object
 * @param callback function
 */
function jsonp({url, params, callback}) {
  return new Promise((resolve, reject) => {
    let script = document.createElement("script");
    window[callback] = function (data) {
      resolve(data);
      document.body.removeChild(script);
    };
    let strConcatArr = []; // 拼接字符串数组

    params = [...params, callback];

    for (let key in params) {
      strConcatArr.push(`${key}=${params[key]}`); // http://localhost:3000/find?name=李华&callback=show
    }

    // 字符串拼接,?之后为查找
    url = `${url}?${strConcatArr.join("&")}`;

    // 将url放入src中
    script.setAttribute("src", url);
    document.body.appendChild(script);
  });
}


// jsonp放入数据,当调用show时,打印出响应值,并返回响应值。
jsonp({
    url:"http://localhost:3000/find",
    params:{name:"李华"},
    callback: "show"
}).then(res=>{console.log(res); return res;})

# CORS

跨源资源共享 (CORS (opens new window))(cross origin sharing stantard)(或通俗地译为跨域资源共享)是一种基于 HTTP (opens new window) 头的机制,该机制通过允许服务器标示除了它自己以外的其它 origin (opens new window)(域,协议和端口),使得浏览器允许这些 origin 访问加载自己的资源。

# · 如何开启CORS

服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

# · CORS在业务中的使用

因为现在node环境下有了js语法的后端框架,比如express、koa等。因此如果前端做得full-stack工作,也可能会用到CORS。(若非node,则CORS都是交给后端处理的)。这里以express为例。

  1. 前端发送请求

    // index.html
    let xhr = new XMLHttpRequest()
    document.cookie = 'name=xiamen' // cookie不能跨域
    xhr.withCredentials = true // 前端设置是否带cookie
    xhr.open('PUT', 'http://localhost:4000/getData', true)
    xhr.setRequestHeader('name', 'xiamen')
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
          console.log(xhr.response)
          //得到响应头,后台需设置Access-Control-Expose-Headers
          console.log(xhr.getResponseHeader('name'))
        }
      }
    }
    xhr.send()
    
    
  2. express后端得到请求后,做响应处理,其中包含设置CORS跨域

    //server1.js
    let express = require('express');
    let app = express();
    app.use(express.static(__dirname));
    app.listen(3000);
    
    
    //server2.js
    let express = require('express')
    let app = express()
    let whitList = ['http://localhost:3000'] //设置白名单
    app.use(function(req, res, next) {
      let origin = req.headers.origin
      if (whitList.includes(origin)) {
        // 设置哪个源可以访问我
        res.setHeader('Access-Control-Allow-Origin', origin)
        // 允许携带哪个头访问我
        res.setHeader('Access-Control-Allow-Headers', 'name')
        // 允许哪个方法访问我
        res.setHeader('Access-Control-Allow-Methods', 'PUT')
        // 允许携带cookie
        res.setHeader('Access-Control-Allow-Credentials', true)
        // 预检的存活时间
        res.setHeader('Access-Control-Max-Age', 6)
        // 允许返回的头
        res.setHeader('Access-Control-Expose-Headers', 'name')
        if (req.method === 'OPTIONS') {
          res.end() // OPTIONS请求不做任何处理
        }
      }
      next()
    })
    app.put('/getData', function(req, res) {
      console.log(req.headers)
      res.setHeader('name', 'jw') //返回一个响应头,后台需设置
      res.end('put返回数据')
    })
    app.get('/getData', function(req, res) {
      console.log(req.headers)
      res.end('get返回数据')
    })
    app.use(express.static(__dirname))
    app.listen(4000)
    
    

    上述代码由http://localhost:3000/index.htmlhttp://localhost:4000/跨域请求.可知CORS跨域主要由后端添加白名单,做一些逻辑操作实现的。

# postMessage

postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

TIP

这个方法类似vue中的emit()传值。

otherWindow.postMessage(message, targetOrigin, [transfer]);
  • message:将要发送其他window的数据
  • 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
  • 是一串和message 同时传递的 Transferable (opens new window) 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
// a.html下的内容

  // 内嵌不同源的b页面
  <iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件

    <script>
      function load() {
        // 获取b页面Dom节点
        let frame = document.getElementById('frame')

        // 给b页面发送消息,若第二个形参 协议域名端口号有一处与b页面url不一样,则发送失败。
        frame.contentWindow.postMessage('来自a的msg', 'http://localhost:4000') //发送数据


        // 接收b的消息
        window.onmessage = function(e) { //接受返回数据
          console.log(e.data) // 来自b的msg
        }
      }
    </script>


// b.html下的内容
window.onmessage = function(e) {
  console.log(e.data) // 来自a的msg
  e.source.postMessage('来自b的msg', e.origin)
}

# webSocket

webSocket本身不存在跨域问题,所以我们可以利用webSocket来进行非同源之间的通信。(client与sever的全双工通信,用法有点类似于postmessage)

# · 实现

利用webSocket的API,可以直接new一个socket实例,然后通过open方法内send要传输到后台的值,也可以利用message方法接收后台传来的数据。后台是通过new WebSocket.Server({port:3000})实例,利用message接收数据,利用send向客户端发送数据。

 <!DOCTYPE html>
 <html>
 <head>
  <title></title>
 </head>
 <body>
  <!-- 
   高级api  不兼容  但是有一个socket.io这个库,是兼容的(一般用这个)
   -->

   <script type="text/javascript">
    let socket = new WebSocket("ws://localhost:3000");//ws协议是webSocket自己创造的
    socket.onopen = function(){
     socket.send("我叫俞华");
    }
    socket.onmessage = function(e){
     console.log(e.data);//你好,我叫俞华!
    }
   </script>
 </body>
 </html>

以node中间件express,起一个服务端.

 /*
  要使用ws协议,需要装一个ws的包
 */
 let express = require("express");
 let app = express();
 let WebSocket = require("ws");
 let wss = new WebSocket.Server({port:3000});
 wss.on("connection",function(ws){//先连接
  ws.on("message",function(data){//用message来监听客户端发来的消息
   console.log(data);// 我叫俞华
   ws.send("你好,"+data+"!");
  })
 })

# nginx反向代理

其原理是通过代理服务器来发送请求。当本地域名端口为 www.domain.com:81时,通过反向代理,就会被 www.domain2.com:8080代理服务器转发出去。从而解决跨域问题

// proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

随后nginx -s reload,重启nginx

// index.html
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();

服务端中监听8080端口

// server.js
var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));
    // 向前台写cookie
    res.writeHead(200, {
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
    });
    res.write(JSON.stringify(params));
    res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');

# nodejs中间件

nginx反向代理是原理相似的。都是你把请求发给一个媒介,再由媒介发送给目标。他可以直接代理或者其他的手段得到想到的数据,然后返回。

const Koa = require('koa');
// 代理
const Proxy = require('koa-proxy');
// 对以前的异步函数进行转换
const Convert = require('koa-convert');

const app = new Koa();
const server = require('koa-static');
app.use(server(__dirname+"/www/",{ extensions: ['html']}));

app.use(Convert(Proxy({
  // 需要代理的接口地址
  host: 'http://127.0.0.1:8888',
  // 只代理/api/开头的url
  match: /^\/api\//
})));

console.log('服务运行在:http://127.0.0.1:7777');
app.listen(7777);

# window.name+iframe

# · 原理

window.name属性在于加载不同的页面(包括域名不同的情况下),如果name值没有修改,那么它将不会变化。并且这个值可以非常的长(2MB)。

# · 步骤

利用window.name的特性,我们可以做以下几个步骤:

  • 在A页面通过iframe加载B页面。
  • B页面获取完数据后,把数据赋值给window.name。
  • 因为Onload会触发2次,在第二次时将window.name赋值给anotherOriginData,完成跨域数据获取。
let mark = false;
let ifr = document.createElement('iframe');
ifr.src = "http://127.0.0.1:8888/demo4";
ifr.style.display = 'none';
var anotherOriginData = null; // 用于存储不同源的数据
document.body.appendChild(ifr);
   // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
ifr.onload = () => {
    // iframe 中数据加载完成,触发onload事件
    if (mark) {
        anotherOriginData = ifr.contentWindow.name;// 这就是数据
    } else {
        mark = true;
        // 修改src指向本域的一个页面(这个页面什么都没有)
        ifr.contentWindow.location = "http://127.0.0.1:7777/demo4/proxy.html";
    }
}

# location.hash + iframe

# · 实现背景

  • 有三个html页面a,b,c
  • ab同源,c不同源
  • c嵌套在iframe中
  • b为c页面中的嵌套iframe

# · 具体实现步骤

  • a.html给c.html传一个hash值
  • c.html收到hash值后,再把hash值传递给b.html
  • b.html将结果放到a.html的hash值中。
 // a.html
 <iframe src="http://localhost:4000/c.html"></iframe>
 <script>
   // a页面开启监听
   window.onhashchange = function () { //检测hash的变化
     console.log(location.hash); // http://localhost:3000/b.html#{"name": "张三"}
   }
 </script>
 // b.html
  <script>
    //b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
    window.parent.parent.location.hash = location.hash;
  </script>
 // c.html
 console.log(location.hash);
  let iframe = document.createElement('iframe');
  let data = {name:"张三"};
  let strData = JSON.stringify(data);
  iframe.src = 'http://localhost:3000/b.html#'+strData;
  document.body.appendChild(iframe);

# document.domain + iframe

这个跨域方法要求2个域之间的主域名相同,子域不同,比如a.xxx.com和b.xxx.com。如果不同的话是不行的。

# · 实现

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

// a.html
<body>
 helloa
  <iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
  <script>
    // 设置主域名 zf1.cn
    document.domain = 'zf1.cn'
    function load() {
      console.log(frame.contentWindow.a);
    }
  </script>
</body>

// b.html
<body>
   hellob
   <script>
     // 设置主域名 zf1.cn
     document.domain = 'zf1.cn'
     var a = 100;
   </script>
</body>

# 开发期前端该如何解决跨域?

# cors中间件

如果后端开发采用的是node中的服务器开发框架,则可以使用cors中间件来允许不同源跨域。

const express = require("express");
const cors = require("cors"); // cors中间件
const app = express();

app.use(
    // 添加允许被跨域的源
	cors({
		origin:["http://localhost:3000","http://localhost:8081"]
	})
)

# 在配置文件中设置代理中转请求

前端只需要请求中转服务器(以vite为例)

// vite.config.js
export default defineConfig({
	plugins: [vue()],
	server: {
	    // /api代理
		proxy: {
		  "/api":{
		        
				target:"http://localhost:5000", // 转到目标url
				changeOrigin: true, // 允许跨域
				rewrite: (path) => path.replace(/^\/api/,"") // 将/api重写为"",只留下url
			}
		}
	}
})

# 总结

  1. 跨域这一现象是由浏览器同源策略所产生的。当两个源的 协议域名端口号,其中之一有所不同,则会引起跨域问题
  2. 同源策略只出现在浏览器中
  3. 跨域限制并非限制请求,而是拦截响应返回的数据报文。
  4. 一般来说nginx反向代理、CORS解决方案会常用一些。

# 参考文章

  1. 浏览器的同源策略 - Web 安全 | MDN (mozilla.org) (opens new window)
  2. 九种跨域方式实现原理(完整版) - 掘金 (juejin.cn) (opens new window)
  3. window.postMessage - Web API 接口参考 | MDN (mozilla.org) (opens new window)
  4. (18条消息) 【9大跨域解决方案】websocket解决跨域的原理_俞华的博客-CSDN博客_websocket跨域 (opens new window)