# 同源与跨域
如果两个 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 的时候再来补充 😂