如何解决跨域

216 阅读3分钟

一、什么是跨域?

在了解什么是跨域的时候,我们首先要了解一个概念,叫同源策略,什么是同源策略呢,就是我们的浏览器出于安全考虑,只允许与本域下的接口交互。不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源。

是什么意思呢,就比如你刚刚登录了淘宝买了东西,但是你现在又点进去了另外一个网站,那么你现在的淘宝账户是属于登录状态,而并没有登出,所以你现在点进去的这个网站可以看到你的账户信息,并操作你的账户信息,这样子就很危险。

我们在来了解一个概念,就是本域,什么是本域呢?就是同协议,同主机,同端口就叫本域。 常见的跨域场景

http://www/a.com/a.js

http://www/a.com/b.js 同一域名。允许通信

http://www/a.com/a.js

https://www/a.com/b.js 协议不同,跨域

http://www/a.com/a.js

script.a.com/a.js 主域相同,子域不同,跨域

那什么时候跨域呢,如下浏览器请求的三种报错:

1、请求未发送

2、请求发送后,服务器发现不一样,服务器未反应。

3、请求发送,服务器有反应,数据返回的时候,浏览器发现不对,被拦截。

同源策略限制了ajax请求,但有三个标签没有被同源策略所影响

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

特别说明

  1. 如果是端口和协议造成的跨域,前端是没有办法解决的
  2. 跨域仅仅是根据URL的首部来识别,不会根据这个首部对应的ip地址来判断
  3. 跨域并不是请求没有发出去,请求是能发出去,服务期也能响应,只是响应结果被浏览器拦截了

二、跨域的解决方案

1. jsonp

原理: 利用 script 标签中的 src 属性不会被同源策略所拦截的这一机制,将我们要请求的URL 地址,添加到script的src属性中,并且携带上前端的一个函数名作为参数给到后端,后端获取到前端传递的函数名,返回该函数的调用语法,将要返回的数据放在该函数中作为参数,当浏览器接收到全局下的函数被调用的后端响应时,会自动执行该函数,从而从参数中获取到后端返回的数据

//前端
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.0/jquery.min.js"></script>
    <title>Document</title>
</head>

<body>
    <button id="btn">发请求</button>

    <script>
        let btn = document.getElementById('btn')
        btn.addEventListener('click', () => {
            // $.ajax({
            //   url: 'http://localhost:3000/home',
            //   method: 'get',
            //   scuccess(res) {
            //     console.log(res);
            //   }
            // })

            // jsonp
            function jsonp({ url, callback }) {
                return new Promise((resolve, reject) => {
                    let script = document.createElement('script')
                    window[callback] = function (data) {  // 还没执行
                        resolve(data) // show函数的参数
                        document.body.removeChild(script)
                    }
                    script.src = `${url}?callback=${callback}`  // <script src="http://localhost:3000/home?callback='show'" />
                    document.body.appendChild(script) // script标签只要出现在了body里,浏览器就会自动加载其src中的资源
                })
            }

            jsonp({
                url: 'http://localhost:3000/home',
                callback: 'show'
            }) // 后端会返回一个 show(),导致了window上的show函数执行了
                .then(data => {
                    console.log(data);

                    // 拿到了数据之后的操作...
                })
        })
    </script>
</body>

</html>
//后端
const express=require('express')
const app=express()

app.get('/home',(req,res)=>{
    // console.log(req.query);  ///show
    let {callback}=req.query
    res.end(`${callback}('hello word')`) //'show()'
})
app.listen(3000,()=>{
    console.log('3000端口已启动');
})

缺点:

  1. 需要对方的服务期做支持才可以

  2. 只支持 get 请求,有局限性,可能会遇到xss攻击

2. cors

cors是W3C的标准,它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求。也就是说浏览器发请求是不会被跨域的,跨域的核心是后端响应不了。

要让后端响应内容能够不被浏览器拦截,关键在于后端。如果后端也能遵从 cors标准的话,后端的响应也可以跨源

  • 简单请求 · 使用get、post、head

    · Content-Type的值仅限于 text/plain || multipart/form-data || appplication/x-www-form-urlencoded

  • 复杂请求 · 不满足简单请求的条件的就是复杂请求 · 复杂请求的cors请求,会在正式通信之前,增加一次http查询请求,称为“预检”,预检是用来知道服务端是否允许跨域请求,预检请求发的是options方法

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.0/jquery.min.js"></script>
  <title>简单请求</title>
</head>

<body>
  <button id="btn">发请求</button>

  <script>
    let btn = document.getElementById('btn')
    btn.addEventListener('click', () => {
      $.ajax({
        url: 'http://localhost:3000/home',
        method: 'get',
        headers: {
          'Content-Type': "text/plain"
        },
        scuccess(res) {
          console.log(res);
        }
      })
    })
  </script>
</body>

</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>复杂请求</title>
</head>
<body>
  <button id="btn">发请求</button>

  <script>
    let btn = document.getElementById('btn')
    btn.addEventListener('click', () => {
      let xhr = new XMLHttpRequest()
      document.cookie = 'name=wn'
      xhr.withCredentials = true  //xhr这个请求必将去浏览器的cookie里找cookie并携带上
      xhr.open('PUT', 'http://localhost:3000/getData', true)
      xhr.setRequestHeader('name', 'wn')
      xhr.onreadystatechange = function() {
        if (xhr.readyState === 4 && xhr.status === 200) {
          console.log(xhr.response);
        }
      }
      xhr.send() // 请求发送
    })
  </script>
</body>
</html>

后端

//app.js
const express = require('express')
const app = express()

let whitList = ['http://127.0.0.1:5500'] // 设置白名单
const cors = (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() // 预检请求不做任何处理
    }

  }
  // console.log(origin);
  next()
}

app.use(cors)

app.get('/home', (req, res) => { // 简单请求
  console.log(req.headers);
  res.end('hello world')
})

app.put('/getData', (req, res) => { // 复杂请求
  console.log(req.headers);
  res.end('hello world 复杂请求')
})

app.listen(3000, () => {
  console.log('3000端口已启动');
})
//app2.js
const express = require('express')
const app = express()
const cors = require('cors')

app.use(cors())

app.get('/home', (req, res) => { // 简单请求
  // console.log(req.headers);
  res.end('hello world')
})

app.put('/getData', (req, res) => { // 复杂请求
  // console.log(req.headers);
  res.end('hello world 复杂请求')
})

app.listen(3000, () => {
  console.log('3000端口已启动');
})

3. postMessage

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

可以解决这么几个问题:

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

otherWindow.postMessage(message, targetOrigin, [transfer]) - message: 要发送给其他window的数据 - targrtOrigin:目标窗口 - transfer(可选)和message一起传递的一个对象,这个对象的所有权将移交给消息接收方

a.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  这是 A 页面  -- 哆啦A梦的大朋友
  <iframe src="http://127.0.0.1:5500/postMessage/b.html" frameborder="0" id="frame" onload="load()"></iframe>

  <script>
    function load() {
      let frame = document.getElementById('frame')
      frame.contentWindow.postMessage('海绵宝宝的朋友是派大星', 'http://127.0.0.1:5500') // 给b页面传数据
      window.onmessage = function(e) { // 接收b页面返回的数据
        console.log(e.data);
      }
    }
  </script>
</body>
</html>
b.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  page B  --- 海绵宝宝

  <script>
    window.onmessage = function(e) {
      console.log(e.data);
      e.source.postMessage('哆啦A梦的朋友是大雄', e.origin)
    }
  </script>
</body>
</html>