问题引出

我在 http://localhost:8080 放了一台后端服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.pushihao.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/hello")
public class HelloController {

@RequestMapping("/h1")
public String h1() {
return "Hello, world!";
}

}

通过调用 http://localhost:8080/hello/h1 可以获得一个字符串

image-20220907131446988


我又在 http://localhost:5173 放了一台前端前端服务器,通过 xhr 请求调用后端的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import axios from 'axios'
axios.defaults.timeout = 5000

axios.get("http://localhost:8080/hello/h1").then(
response => {
console.log(response)
}, error => {
console.log(error)
}
)
</script>

<template></template>
<style scoped></style>

结果这时,浏览器报错

image-20220907131920563


以上便是 xhr 请求跨域问题的一个经典例子


为什么会有请求跨域问题

什么导致的跨域请求问题?简单来说,浏览器的 同源策略 导致了跨域请求问题。

为什么要制定同源策略?官方是这样解释的:同源策略是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

什么样的 url 才算是同源呢?当两个 url 的**协议(protocol)域名(host)端口(port)**三者都相同时才算是同源。只要其中有一个不同,那么 url 就不算同源,进行的 http 请求就称为跨域请求。

跨域请求有什么限制?无法读取非同源网页的 cookie、localstorage 等、无法接触非同源网页的 DOM 和 js 对象、无法向非同源地址发送 Ajax 请求。


解决跨域请求问题

我们知道,同源策略是一个重要的安全策略,它可以减少我们被攻击的风险。但是对于正常的请求,我们希望可以接触跨域带来的一些限制。解决跨域请求问题的方法有很多,以下给出一些常用的解决方案。

后端使用 nginx 解决跨域问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
server {
listen 3500;
server_name localhost;
location / {

# 给 Nginx 配置 Access-Control-Allow-Origin * 后,表示服务器可以接受所有的请求源(Origin),即接受所有跨域的请求。也就是说,表示接受任意域名的请求。这里为 * 是最简单粗暴的方式,但是出于安全考虑,一般会设置为前端域名,如:add_header Access-Control-Allow-Origin 'www.pushihao.com';(只能指定一个域)
add_header Access-Control-Allow-Origin *;

# 如果是上述设置为 * 的话,浏览器将不会发送 cookie 数据(如果需要携带 cookie 数据,则需要进行此设置
add_header Access-Control-Allow-Credentials true;

# 防止出现 Content-Type is not allowed by Access-Control-Allow-Headers in preflight response. 错误
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';

# 防止出现 Request header field Content-Type is not allowed by Access-Control-Allow-Headers in preflight response. 错误
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

# 防止在发送 post 请求时 Nginx 依然拒绝访问的错误。发送"预检请求"时,需要用到方法OPTIONS,所以服务器需要允许该方法
if ($request_method = 'OPTIONS') {
return 204;
}

# 反向代理地址
proxy_pass http://localhost:8080;
}
}

启动 Nginx 服务后,3500 端口被代理到 8080 端口,并且开启了允许所有域进行跨域请求

此时,修改前端请求的 url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import axios from 'axios'
axios.defaults.timeout = 5000

axios.get("http://localhost:3500/hello/h1").then(
response => {
console.log(response)
}, error => {
console.log(error)
}
)
</script>

<template></template>
<style scoped></style>

启动服务后,请求响应正常

image-20220907144118916


使用前端框架提供的代理服务器

注意:这种方法仅在调试阶段有效,部署到服务器上之后就没用了

使用原理:

只有前端在发送 Ajax 请求时会触发跨域请求,而两台后端服务器进行通信时是不会存在跨域的问题的,因此我们可以在前端与后端之间设置一台代理服务器,这台代理服务器与我们的前端项目保持同源,即:localhost:5173 ,我们的前端项目只负责请求这台代理服务器,由代理服务器向后端接口请求数据。

image-20220907145652664

  1. 对于 webpack + vue 的前端项目,可以使用 devServer 配置代理解决跨域问题

vue.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,

//开启代理服务器(方式一)
// devServer: {
// proxy: "http://localhost:8080"
// }
//只能由一个代理服务器,而且会优先请求本地资源,无法决定请求资源的位置


//开启代理服务器(方式二)
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
//重写请求路径,把/api去掉
pathRewrite: {
'^/api': ''
},
ws: true, //用于支持websocket

//等于true时会有跨域伪造,即:如果请求的目标服务器为8080,则请求时也假装自己是8080端口的
//等于false时面对目标服务器就如实回答
changeOrigin: true
},

//proxy可配置多个
'/teacher': {
target: 'http://localhost:8082',
pathRewrite: {
'^/teacher': ''
},
ws: true,
changeOrigin: true
}
},
}
})

AxiosTest.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import axios from 'axios'
axios.defaults.timeout = 5000

// 由于我们制定了重写规则,所以此处的 /api 会在发送请求时被略去
axios.get("/api/hello/h1").then(
response => {
console.log(response)
}, error => {
console.log(error)
}
)
</script>

<template></template>
<style scoped></style>

  1. 对于 vite + vue 的前端项目

vite.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
})

AxiosTest.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import axios from 'axios'
axios.defaults.timeout = 5000

axios.get("/api/hello/h1").then(
response => {
console.log(response)
}, error => {
console.log(error)
}
)
</script>

<template></template>
<style scoped></style>

至此,问题解决!