前端开发者必须掌握的跨域问题

125 阅读4分钟

什么是跨域

用简单的情况来说就是 浏览器不执行其他与自己服务端口和域名不同的脚本的安全限制
我们知道,在互联网中存在两个基本的通信模型,即CS(Client Server)和p2p (peer to peer) ,无论那种方式,要进行通信要考虑到应用层前的域名地址问题,以及到终端后的应用层的端口问题。
众所周知,在网络编程中要唯一确定一个应用要使用到socket套接字,我们就是需要设置域名和端口来让这个应用有个名分。
那么涉及到前后端应用交互时一定要考虑到的问题,当域名或进程端口抑或是ip不同时我们往往都要处理这个跨域问题。

解决方案之典中典之必备知识之jsonp之预备知识

想要使用jsonp方法解决跨域问题有一个前提条件就是在html页面中的script标签的src属性
<script src="https://cdn.bootcss.com/jquery/3.6.0/jquery.min.js"></script>
是不会被同源机制所跨域,那么客户端发送script请求,参数中带着处理返回数据的回调函数,来将前端请求植入src属性中, 再将回调函数拼接到请求末尾 服务端接收到请求并帮助执行掉这个回调函数
说人话就是既然我普通方法不行,那我就把我的请求放到script的src属性的链接里,要带的东西也用get方式将params拼接在src的后面

通过一个例子来了解jsonp

用jQuery向服务端请求时:

<script>
    $('#btn').click(function () {
      // console.log(123);
      $.ajax({
        url: 'http://127.0.0.1:3000',
        success: function (res) {
          console.log(res);
        }
      })
    })
  </script>

显然会遇到跨域的问题

image.png

后端以下面形态出击:

const http = require('http')
const querystring = require('querystring')

let server = http.createServer((req, res) => {
  const url = req.url 
  const query = querystring.parse(url.split('?')[1]) //
  const { name, age, callback } = query //

  const data = {
    name: name,
    age: age
  }
  res.end(`${callback}('${JSON.stringify(data)}')`) //
  // JSON.parse(...)翻转回去
})


server.listen(3000, () => {
  console.log('server start');
})

服务器的任务比较简单

  1. 将前端成功传来的参数构造数组
  2. 解构数据
  3. 将数据组合好形式传回给客户端

那么,主要的实现过程在客户端:

<script>
    class Jsonp {
      constructor(req) {
        this.url = req.url
        this.callbackName = req.callbackName
      }
      create() {
        const script = document.createElement('script')
        const url = `${this.url}?callback=${this.callbackName}`
        script.src = url
        document.getElementsByTagName('head')[0].appendChild(script)
      }
    }
    new Jsonp({
      url: 'http://127.0.0.1:3000',
      callbackName: 'getMsg'
    }).create()

    function getMsg(data) {
      data = JSON.parse(data)
      console.log(`My name is ${data.name}, and ${data.age} age`);
    }
  </script>

客户端需要: 将请求构造好后插入到页面的script标签的src属性中,即可成功与服务端搭上线,再将服务端传的数据处理 最后再将jsonp封装一下:

<script>
    const jsonp = ({ url, params, callbackName }) => {
      const generateUrl = () => {
        let dataStr = ''
        // 往url后面拼接参数
        for (let key in params) {
          dataStr += `${key}=${params[key]}&`
        }
        dataStr += `callback=${callbackName}`
        return `${url}?${dataStr}`
      }
      return new Promise((resolve, reject) => {
        callbackName = callbackName || 'cb' + Math.random().toString().replace('.', '')
        let script = document.createElement('script')
        script.src = generateUrl()
        document.body.appendChild(script)

        window[callbackName] = data => {
          resolve(data)
          // document.body.removeChild(script)
        }
      })
    }

    jsonp({
      url: 'http://127.0.0.1:3000',
      params: {
        name: 'zowie',
        age: 18
      },
      callbackName: 'getData'
    })
      .then(data => JSON.parse(data))
      .then(data => {
        console.log(data);
      })
  </script>

解决方案2——CORS(跨域资源共享)

浏览器通过请求做预检,判断是简单请求还是复杂请求

在浏览器的网络报文中我们可以通过查看到一些配置的参数:

  1. Access-Control-Request-Method //请求方法
  2. Access-Control-Request-Headers //请求头部
  3. Origin //请求发出的域

服务端应答:

  1. Access-Control-Allow-Origin //能发出这个请求的域名
  2. Access-Control-Allow-Methods //允许发出请求的方法
  3. Access-Control-Allow-Headers //允许发出请求的头部字段
  4. Access-Control-Allow-Age //预检请求能被缓存的最长时间,在这个时间内,同一个请求不会再次发出预检请求
  res.writeHead(200, {
    "Content-Type": "text/plain", //服务端设置请求头的类型
    "Access-Control-Allow-Origin": "*" //表示允许所有的域来请求我
  })

但是实际引用中几乎是不会允许这样的操作,为了安全各厂的前端后端都会在同一域下,只有通过自家的前端获取自家后端的数据,这是可控的,也是安全的,

  • 简单请求 如果是简单请求,该请求回携带Origin,如果Origin不在服务端的Access-Control-Allow-Origin中,浏览器就会拦截响应

  • 复杂请求 和简单请求不同的地方主要体现在 预检和响应

  res.writeHead(200, {
    "Content-Type": "text/plain", //服务端设置请求头的类型
    "Access-Control-Allow-Origin": "*",//表示允许所有的域来请求我
    "Access-Control-Allow-Methods": "PUT,POST,GET",
    "Access-Control-Allow-Headers": "X-Custom-Header", //允许请求头
    "Access-Control-Max-Age": 2000,   //预检缓存最长
    "Access-Control-Allow-Credentials": true //允许携带cookie
  })

不仅仅是设置简单的请求,也可以设置更多的可选选项来满足自身需求

Nginx Proxy

Nginx启动服务器代理请求,因为服务器是没有同源策略

websocket