常见跨域方式实现原理

389 阅读3分钟

前言

前后端数据交互接口调试常见的问题就是跨域,什么是跨域,以及有哪些跨域方式呢?

什么是跨域

1. 什么是同源策略及其限制内容?

同源策略 是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指 "协议+域名+端口" 三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

image.png 同源策略限制以下几种行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM和JS对象无法获得
  • AJAX 请求不能发送

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

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

2. 常见跨域场景?

当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域

image.png

跨域解决方案

1.Jsonp

原理

利用标签没有跨域限制的漏洞,网页跨域得到从其他来源动态产生的json数据。JSONP请求一定要做到请求的服务器支持才可以。

缺点

仅支持get请求具有局限性,不安全可能会遭受XSS攻击

实现流程

  • 在服务器上定义的函数,如(show函数),通过ajax请求将服务器定义好的函数作为请求参数传递,如果服务器获取到该参数则在服务器端调用该函数并返回json串
  • 在页面创建script标签,通过拼接路径的形式?wd=b&jiajia=jiajia&cb=show获取后台返回的json串
 function jsonp({url, params, cb}) {
        return new Promise((resolve, reject) => {
          window[cb] = function (data) {
            resolve(data)
            document.body.removeChild(script)
          }
          let arrs = [];
          params = {
            ...params,
            cb
          }
          for (let key in params) {
            arrs.push(`${key}=${
              params[key]
            }`)
          }
          let script = document.createElement("script");
          script.src = `${url}?${
            arrs.join("&")
          }`;
          document.body.appendChild(script)
        })
      }

      // 封装 一个jsonp请求
      jsonp({
        url: 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su',
        params: {
          wd: "b",
          jiajia: 'jiajia'
        },
        cb: "show"
      }).then(res => {
        console.log("res", res)
        let div = document.createElement("div")
        document.body.appendChild(div)
        div.innerHTML = res.s
      })    

2.cros

cros 需要后端与浏览器同时支持,只需要在服务器端Access-Control-Allow-Origin,设置可以允许访问的

  • 3000端口的静态页面访问4000端口的接口
const xhr = new XMLHttpRequest;
xhr.open("GET",'http://localhost:4000/getData',true);
    // 如果想通过前端的ajax服务设置http请求头,需要后台服务响应的支持
    xhr.setRequestHeader('name','zfpx');
    // 强制http请求必须带上cookie
    document.cookie = 'name=jiajia';
    xhr.withCredentials = true;
    xhr.onreadystatechange = function(){
        if(xhr.readyState  == 4){           
            if(xhr.status >= 200 ){
                console.log("ajax start")
                console.log(xhr.response)                
            }
        }
    }
    xhr.send();
  • 2 3000端口托管步骤1的静态页面
const express = require("express");
const app = express();
app.use(express.static(__dirname));
app.listen(3000, () => {
    console.log("server is running at 3000")
})   
  • 3 开启4000端口的接口服务,允许端口3000的静态页面访问
const express = require("express");
const app = express();
let whiteList = ['http://localhost:3000'];
app.use(function (req, res, next)  {
    const origin = req.headers.origin;
    console.log("origin",req.headers.origin)
    if (whiteList.includes(origin)) {
        // 设置哪个源可以访问我
        res.setHeader('Access-Control-Allow-Origin', origin);
        res.setHeader("Content-type","text/html;charset=UTF8")
        // 允许携带哪个头访问我
        res.setHeader('Access-Control-Allow-Headers', "name");
        // 允许哪个方法访问我
        res.setHeader("Access-Control-Allow-Methods", 'PUT');
        // 预检存货时间(非简单请求会有预检,6m之后不会同时发送)
        res.setHeader("Access-Control-Max-Age", 6000);
        // 允许携带cookie
        res.setHeader("Access-Control-Allow-Credentials", true);
        // 允许前端获取哪个头
        res.setHeader("Access-Control-Expose-Headers",'name')
        if (res.method === 'OPTIONS') {
            res.end();//options 请求不做任何处理
        }
    }
    next()
})

app.get("/getData", (req, res) => {
    console.log("req.header",req.header)
    res.end("我不爱你")
})

// app.use(express.static(__dirname))
app.listen(4000, () => {
    console.log("server is running at 4000")
})
  • 服务端在4000端口的接口对3000端口设置了Access-Control-Allow-Origin则就可以访问了,设置的相应头都可以在浏览器中查看的到 image.png

3.Iframe- postMessage

托管在3000端口的a页面,想要内嵌托管在4000端口的b页面

  • 托管在3000端口的a页面
<body>
    a文件
    <iframe class="lazy" src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe>
    <!-- 等它加载完触发一个事件 -->
    <script>
      function load() {
        let frame = document.getElementById('frame')
        frame.contentWindow.postMessage('我爱你', 'http://localhost:4000') // 发送数据
        window.onmessage = function (e) { // 接受返回数据
          console.log("我调用的给我回的消息为", e.data) // 我不爱你
        }
      }
    </script>
  </body>
  • 托管在4000端口的b页面
<body>
    <div>b文件</div>
  </body>
  <script>
    window.onmessage = function (e) {
      console.log("调用我的人对我的说的话为", e.data) // 我爱你
      e.source.postMessage('我不爱你', e.origin)
      console.log("b-postMessage")
    }
  </script>

4.websocket

  • 在页面创建一个 WebSocket实例
 <body>
    this is WebSocket
    <script>
      let socket = new WebSocket('ws://localhost:3000');
      socket.onopen = function () {
        socket.send('我爱你'); // 向服务器发送数据
      }
      socket.onmessage = function (e) {
        console.log(e.data); // 接收服务器返回的数据
      }
    </script>
  </body>   
  • 在服务器端监听前端的WebSocket通信
const express = require('express');
const app = express();
app.use(express.static(__dirname))
const WebSocket = require('ws');//记得安装ws
const wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
  ws.on('message', function (data) {
    ws.send('我不爱你')    
  });
})
app.listen(4000, () => {
  console.log('success')
})

在浏览器可以在WS查看浏览器与服务器的通信数据 image.png

5.Node中间件代理

实现原理:同源策略是浏览器需要遵循的标准,而服务器通信就无需遵循同源策略。代理服务器,需要做到以下几个步骤

  • 接受客户端请求
  • 将请求转发给实际提供数据的服务器
  • 拿到服务器相应的数据并返回给浏览器

image.png

  • 1 在3000端口的静态页面访问4000端口的接口服务
 <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
    <script>
      $.ajax({
        url: 'http://localhost:4000',
        type: 'post',
        data: { name: '佳佳', password: '123456' },
        contentType: 'application/json;charset=utf-8',
        success: function(result) {
          console.log(result) // {"title":"fontend","password":"123456"}
        },
        error: function(msg) {
          console.log(msg)
        }
      })
     </script>
  • 2 监听3000端口服务,特定的接口服务进行转发
 const http = require("http");
// 1. step1 接受客户端请求
const server = http.createServer((request,response) => {
    // 代理服务器直接与接口服务器通信,需要设置CROS的首部字段
    response.writeHead(200,{
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': '*',
        'Access-Control-Allow-Headers': 'Content-Type'
    })
    //2 step2 将请求转发给服务器
    const proxyRequest = http
        .request({
           host: '127.0.0.1',
            port: 4000,
            url: '/',
            method: request.method,
            headers: request.headers 
        },(serverResponse)=>{
          //3 step3 收到服务的响应
            var body = ''
            serverResponse.on('data',(chunk) => {
                body += chunk;
            })
            serverResponse.on('end',() =>{
                response.end(body)
            })
        })
        .end()
})
app.use(express.static(__dirname))
server.listen(3000, () => {
  console.log('The proxyServer is running at http://localhost:3000')
})

  • 3 4000端口提供数据的接口服务
const http = require("http");
const data = { title:"fontend",password:"123456"};
const server = http.createServer((request,response) => {
    if(request.url == '/'){
        response.end(JSON.stringify(data))
    }
})
server.listen(4000,() => {
    console.log('data is support on 4000 port')
})

6.nginx反向代理

  • 通过nginx 提供的web服务修改nginx的配置即可 实现原理:通过nginx配置一个代理服务器域名与页面的静态服务器域名相同,端口号不同做跳板机,反向代理访问domain2的接口,并且可以修改cookie中domian携带的信息,方便写入cookie,实现跨域登录。
    // 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;
    }
}

总结

  • node中间件以及nginx都是利用的服务器没有同源策略的限制
  • 工作中常用的是nginx以及cors中间件