# 同源与跨域

  如果两个 URL 的协议、域名、端口都一致,则他们同源,反之是跨域。例如:

  • 本地项目 http://localhost:8080 去访问本地的后端接口 http://localhost:6060 —— 端口不一致
  • 本地项目 http://localhost:8080 去访问部署的后端接口 https://10.143.88.222:8080 —— 域名、协议不一致
  • ……

# 同源策略

  出现跨域的根本原因:

浏览器的同源策略不允许非同源的 URL 之间进行资源的交互。

# 一、JONSP 方式

# 1、原理

  由于浏览器的同源策略,网页无法通过 ajax 请求非同源的接口数据。但是:

script 标签的 src 属性、img 标签的 src 属性、link 标签的 href 属性等是没有跨域的限制的。

# 2、优缺点

优点:

  实现方式非常巧妙,通过前端写一个回调函数,并在发送请求的时候将回调函数名告诉后端,后端返回数据是通过触发回调函数的方式向前端发送数据。

缺点:

  只能解决 get 请求的跨域问题。并且使用 JONSP 方法解决跨域的话,需要前后端相互配合的写一些东西,这一点就 😒 非常难受。

# 3、具体实现

  动态添加一个<script>标签,通过 src 属性请求跨域的数据接口,并通过函数调用的形式接受跨域接口响应回来的数据。

# ① 前端

  利用 src 属性解决跨域,前端除了使用原生 js 手动实现,还可以使用 jQuery,因为 jQuery 是支持发起 jsonp 请求的。

前端发起请求的时候,在请求地址上追加 ?callback='回调函数名'

代码演示
  • index.html
<body>
  <h4>1、请求方式</h4>
  <button class="btn-origin">发起跨域请求-原生js封装方式</button>
  <br /><br />
  <button class="btn-jquery">发起跨域请求-jQuery请求方式</button>

  <h4>2、服务器地址</h4>
  <p>原生node服务器:http://localhost:6060/data?callback=requestData</p>
  <p>express服务器:http://localhost:6061/data?callback=requestData</p>

  <script>
    document.querySelector('.btn-origin').onclick = function () {
      // JSONP请求封装
      function jsonp(url, callback) {
        const script = document.createElement('script')
        script.src = url + '?callback=' + callback
        document.body.appendChild(script)
      }

      // 1、发起请求
      jsonp('http://localhost:6060/data', 'requestData')
    }

    // 2、接收数据(回调 -- 供后端调用)
    // 问题: 如何将这里的回调🤔🤔🤔放到onclick里面
    function requestData(data) {
      console.log(data)
    }

    // ----------------------------

    document.querySelector('.btn-jquery').onclick = function () {
      $.ajax({
        dataType: 'jsonp',
        url: 'http://localhost:6061/data',

        // 设置指定的回调函数名
        // 【等效于上面的:'?callback=' + callback】
        jsonp: 'callback',
        jsonpCallback: 'requestData',

        success: function (res) {
          console.log(res)
        }
      })
    }
  </script>
</body>

# ② 后端

  后端的话,可以使用原生 node.js 或者 后面的 express 快速搭建一个接口环境。

后端返回数据时,返回的是一个回调函数 res.send(`${callback}(${JSON.stringify(data)})`)

代码演示
  • 原生 node 服务器 —— 搭建
const http = require('http')
const url = require('url')

// 创建web服务器
http
  .createServer((req, res) => {
    const data = { name: 'lencamo', age: 18 }
    const callback = url.parse(req.url, true).query.callback

    if (url.parse(req.url, true).pathname === '/data') {
      res.end(`${callback}(${JSON.stringify(data)})`)
    }
  })
  .listen(6060, () => {
    console.log('原生node服务器启动:http://localhost:6060')
  })
  • express 服务器 —— 搭建
const express = require('express')
const app = express()

app.get('/data', (req, res) => {
  const data = { name: 'lencamo', age: 18 }
  const callback = req.query.callback

  res.send(`${callback}(${JSON.stringify(data)})`)
})

app.listen(6061, () => {
  console.log('express服务器启动:http://localhost:6061')
})

# 二、CORS 方式(后端)

# 1、原理

  通过最前面的图片介绍,我们知道了,产生跨域的根源是浏览器的同源策略。

  使用 CORS 时,它可以通过向浏览器添加 HTTP 头,达到允许源站点设置哪些外部域可以访问其资源的目的。相关的 HTTP 字段有:

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • ……

# 2、优缺点

  CORS 解决跨域可以说是真正意义上的解决跨域问题,但是压力全给到了后端,并且响应头不是可以随便配置的。

# 3、具体实现

# ① 前端

  在上面 JONSP 实现请求跨域中,我们使用了 jQuery 发起 ajax 请求,下面我们采用其他的几种方式发起数据请求:XHR、 fetch 和 axios 。

采用 CORS 方法解决跨域问题,就没前端什么事了,安心写请求就可以了。

代码演示
<body>
  <h4>1、请求方式</h4>
  <button class="btn-XHR">发起跨域请求-XHR请求</button>
  <br /><br />
  <button class="btn-fetch">发起跨域请求-fetch请求</button>
  <br /><br />
  <button class="btn-axios">发起跨域请求-axios请求</button>

  <h4>2、服务器地址</h4>
  <p>原生node服务器:http://localhost:6060/data</p>
  <p>express服务器:http://localhost:6061/data</p>

  <script>
    document.querySelector('.btn-XHR').onclick = function () {
      var xhr = new XMLHttpRequest()
      xhr.open('GET', 'http://localhost:6060/data')
      xhr.send()
      xhr.onload = function () {
        if (xhr.status === 200) {
          console.log(JSON.parse(xhr.responseText))
        }
      }
    }

    document.querySelector('.btn-fetch').onclick = function () {
      fetch('http://localhost:6060/data')
        .then((res) => res.json())
        .then((res) => {
          console.log(res)
        })
    }

    document.querySelector('.btn-axios').onclick = function () {
      axios.get('http://localhost:6061/data').then((res) => {
        console.log(res.data)
      })
    }
  </script>
</body>

# ② 后端

  后端利用 CORS 解决跨域问题时呢,也有两种方式:

springboot 应用中
@Configuration
public class GlobalCorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        // 设置你要允许的网站域名
        config.addAllowedOrigin("http://localhost:8080");
        //允许跨域发送cookie
        config.setAllowCredentials(true);
        //放行全部原始头信息
        config.addAllowedHeader("*");
        //允许所有请求方法跨域调用
        config.addAllowedMethod("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}
  • 方式 1:设置响应头(需要知道 😒 前端服务运行地址
const http = require('http')
const url = require('url')

http
  .createServer((req, res) => {
    // 设置响应头解决跨域
    res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8081')
    res.setHeader('Access-Control-Allow-Methods', 'GET')
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
    res.setHeader('Content-Type', 'application/json')

    const data = { name: 'lencamo', age: 18 }

    if (url.parse(req.url, true).pathname === '/data') {
      res.end(JSON.stringify(data))
    }
  })
  .listen(6060, () => {
    console.log('原生node服务器启动:http://localhost:6060')
  })
  • 方式 2:使用第三方中间件

  在真正的开发中,我们往往是采用第三方中间件来简化跨域处理,例如 cors (opens new window)(HTTP 服务器)、http-proxy-middleware (opens new window)(代理服务器)等。

这些中间件会自动设置响应头和处理预检请求(OPTIONS),让我们更方便地处理跨域问题。

  • cors 包
const express = require('express')
const app = express()

// cors包解决跨域
const cors = require('cors')
app.use(cors())

app.get('/data', (req, res) => {
  const data = { name: 'lencamo', age: 18 }

  res.send(JSON.stringify(data))
})

app.listen(6061, () => {
  console.log('express服务器启动:http://localhost:6061')
})

# 三、代理服务器

  浏览器没办法和 目标服务器沟通 解决跨域的问题,那就找一个代理人帮我们做。

作用

  自己单独创建一个 node 代理服务器有如下功能:

  • 作为服务端:它解决跨域问题,并接受数据请求地址
  • 作为客户端:它向目标服务器发出数据请求

# 1、原生 Node 代理

略(详见 - Node 服务器 (opens new window)

# 2、第三方中间件 代理

  -http-proxy-middleware (opens new window) 是一个 Node.js 中间件,它允许将 HTTP 请求代理到另一个服务器,从而避免浏览器的同源策略限制

完整实现 ✍
  • 前端
<body>
  <h4>1、请求方式</h4>
  <button class="btn-axios">发起跨域请求</button>

  <h4>2、服务器地址</h4>
  <p>express-代理服务器:http://localhost:5050/ren/data</p>
  <p>express-http服务器:http://localhost:6061/data</p>

  <script>
    document.querySelector('.btn-axios').onclick = function () {
      // 向代理商发起数据请求
      axios.get('http://localhost:5050/ren/data').then((res) => {
        console.log(res.data)
      })
    }
  </script>
</body>
  • 代理服务器 5050 ✍
const express = require('express')
const cors = require('cors')
const { createProxyMiddleware } = require('http-proxy-middleware')
const app = express()

// 1、解决请求跨域问题
app.use(cors())

// 注意使用时机🤔
app.use((req, res, next) => {
  // console.log(req)
  console.log(
    '5050代理服务器收到' + req.get('origin') + '发来的数据请求!该请求将发往' + req.get('host')
  )

  next()
})

// 2、将请求进行代理转发
// 将以/ren(ren这一类型的😄请求)开头的请求,代理到 http://localhost:6061
app.use(
  '/ren',
  createProxyMiddleware({
    target: 'http://localhost:6061',
    // 请求头的 Host 字段将由http://localhost:5050改为http://localhost:6061(伪装)
    changeOrigin: true,
    // 将 http://localhost:6061/ren/data/ 代理到 http://localhost:6061/data/
    // 【去掉/ren这个接口标识】
    pathRewrite: {
      '^/ren': ''
    }
  })
)

app.listen(5050, () => {
  console.log('代理服务器启动:http://localhost:5050')
})
  • 目标服务器 6061
const express = require('express')
const app = express()

app.use((req, res, next) => {
  // console.log(req)
  console.log(
    '6061-http服务器收到' + req.get('origin') + '发来的数据请求!该请求将发往' + req.get('host')
  )

  next()
})

app.get('/data', (req, res) => {
  const data = { name: 'lencamo', age: 18 }

  res.send(JSON.stringify(data))
})

app.listen(6061, () => {
  console.log('HTTP服务器启动:http://localhost:6061')
})

  在上面这个完整实现案例中,changeOrigin: true,这个配置是什么意思呢?

答案

  首先,要先了解两个东西origin(请求从哪里发起的) 和 host(请求将被发送的目的地) 。

  其次,changeOrigin 这个单词是意思要理解好:是将 Host 字段将更改为 Origin 字段

  要想深刻的理解一下,可以通过打印:

console.log(
  '5050代理服务器收到' + req.get('origin') + '发来的数据请求!该请求将发往' + req.get('host')
)

console.log(
  '6061-http服务器收到' + req.get('origin') + '发来的数据请求!该请求将发往' + req.get('host')
)
  • changeOrigin: false,
5050代理服务器收到http://localhost:8081发来的数据请求!该请求将发往localhost:5050

6061-http服务器收到http://localhost:8081发来的数据请求!该请求将发往localhost:5050
  • changeOrigin: true,
5050代理服务器收到http://localhost:8081发来的数据请求!该请求将发往localhost:5050

# 这就是改为true时👍的作用
6061-http服务器收到http://localhost:8081发来的数据请求!该请求将发往localhost:6061

# 3、Nginx 代理

略(详见 - 项目部署 (opens new window)

# 四、vue-cli 代理(前端)

  关于 vue-cli 项目内部的这个 8081 代理服务器,它是怎么搞的我暂时不太清楚(虚拟内存?),但目标服务器 6061 的打印信息为:

6061-http服务器收到🤔undefined发来的数据请求!该请求将发往localhost:6061
完整实现 ✍
  • App.vue
<template>
  <div id="app">
    <h4>1、请求方式</h4>
    <button @click="btn_axios">发起跨域请求</button>

    <h4>2、服务器地址</h4>
    <p>express-代理服务器:http://localhost:8081/ren/data</p>
    <p>express-http服务器:http://localhost:6061/data</p>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  name: 'App',
  methods: {
    btn_axios() {
      // 向代理商✍8081发起数据请求
      axios.get('http://localhost:8081/ren/data').then((res) => {
        console.log(res.data)
      })
    }
  }
}
</script>
  • vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  devServer: {
    // 一、项目启动配置
    open: true,
    host: 'localhost',
    port: 8081, // 默认为8080
    https: false,

    // 二、请求代理配置
    proxy: {
      '/ren': {
        target: 'http://localhost:6061',
        ws: true, // 支持webSocket
        changeOrigin: true,
        pathRewrite: {
          '^/ren': ''
        }
      }
    }
  }
})
  • 目标服务器 6061
const express = require('express')
const app = express()

app.use((req, res, next) => {
  // console.log(req)
  console.log(
    '6061-http服务器收到' + req.get('origin') + '发来的数据请求!该请求将发往' + req.get('host')
  )

  next()
})

app.get('/data', (req, res) => {
  const data = { name: 'lencamo', age: 18 }

  res.send(JSON.stringify(data))
})

app.listen(6061, () => {
  console.log('HTTP服务器启动:http://localhost:6061')
})

# 1、官方解释:

  如果你的前端应用和后端 API 服务器没有运行在同一个主机上,你需要在 开发环境下 将 API 请求代理到 API 服务器。

  这个问题可以通过 vue.config.js 中的 devServer.proxy 选项来配置。

# 2、本质

  通过官方可以发现,它是使用了http-proxy-middleware 中间件 (opens new window),说白了还是使用了 CORS 方式解决跨域的。

  知道了 vue-cli 是怎么实现代理后,后面看到它的 devServer.proxy (opens new window) 配置和 http-proxy-middleware 类似就没什么奇怪的了。

# 3、vue-cli 的使用

  既然都使用 vue-cli 了,devServer.proxy 配置该写在那个文件里面就不用说了吧?

答案

  在 vue.config.js 文件里面进行配置

  在 vue-cli 中进行代理时,最大的体验莫过于:不用自己单独创建一台代理服务器,vue-cli 已经在项目中为我们启动了一台代理服务器,而是直接在配置文件中一键配置代理

(就有那么一瞬间感觉配置 devServer.proxy 就是代理服务器中配置 createProxyMiddleware)。

① 代理所用请求

module.exports = {
  devServer: {
    proxy: 'http://localhost:6061'
  }
}

② 代理指定类型的请求

module.exports = {
  devServer: {
    // 一、项目启动配置
    open: true,
    host: 'localhost',
    port: 8081, // 默认为8080
    https: false,

    // 二、请求代理配置
    proxy: {
      '/ren': {
        target: 'http://localhost:6061',
        ws: true, // 支持webSocket
        changeOrigin: true,
        pathRewrite: {
          '^/ren': ''
        }
      }
      // '/foo': {
      //   target: '<other_url>'
      // }
    }
  }
}
拓展
// [process.env.VUE_APP_BASE_API]: {
//   target: `http://example.com`, //要代理的域名地址
//   ws: true,
//   changeOrigin: true, // 支持跨域请求
//   pathRewrite: {
//     // 不保留[process.env.VUE_APP_BASE_API]
//     ['^' + process.env.VUE_APP_BASE_API]: ''
//     // 保留或重写为其他
//     '^/api': '/api/oj'
//   }
// },

# 4、生产环境跨域

  生产环境表示我们已经开发完成项目,将项目部署到了服务器上,这时已经没有了 vue-cli 脚手架的辅助了。

  我们只是把打包好的 html+js+css 交付运维人员,放到 Nginx 服务器而已,所以此时需要借助 Nginx 的反向代理来进行。

# 五、webpack / vite 代理

  无论是 webpack-dev-server 中的 proxy,还是 vite 中的 proxy,内部都是使用了同一个库 node-http-proxy (opens new window)

改天 回顾 webpack 和 学完 vite 的时候再来补充 😂

更新于 : 8/7/2024, 2:16:31 PM