简介
跨域是前端开发中常见的一个问题,其本身跟http协议没有关系,而是浏览器的一种安全机制,称之为同源策略;
同源策略指的是当前网站和需要访问的目标网站的协议
、域名
、端口
都要相同,其中主要有一个不相同,就会产生跨域问题;
当前url | 目标url | 是否跨域 | 原因 |
---|---|---|---|
http://www.test.com | http://www.test.com/index.html | 否 | 同源(协议、域名、端口相同) |
http://www.test.com | https://www.test.com/index.html | 是 | 协议不同(http/https),不只是这两种协议、如本地的file协议,websocket的ws协议等都算 |
http://www.test.com | http://www.test1.com/index.html | 是 | 主域名不同(test/test1) |
http://www.test.com | http://mail.test.com/index.html | 是 | 子域名不同(www/mail) |
http://www.test.com:8000 | http://www.test.com:8080/index.html | 是 | 端口不同(8000/8080) |
跨域会造成的问题
跨域会造成很多问题,比如
- 无法读取非同源网页的
Cookie
、LocalStorage
和IndexedDB
- 无法接触非同源网页的
DOM
- 无法向非同源地址发送
AJAX
请求
对于现在前后端分离的开发模式,我们最常见的跨域问题就是当请求后端接口时候发生的。如果遇到以下错误,就是发生了跨域问题,我们通过fetch
,去请求后端接口(http://localhost.com/api/user
),由于当前url是本地的,协议是file
协议,就会发生跨域问题
如何解决跨域问题
jsonp
在之前最常见的解决跨域问题是通过jsonp
来进行的,jsonp
的原理是在标签中的请求不会发生跨域问题,可以将请求放到标签上,然后再后台进行数据返回,前端定义一个获取数据的函数,后端将执行函数的代码当做字符串返回,前端会自动执行该代码,从而获取到了数据,但是jsonp
有个最大的问题就是jsonp
跨域只能处理get
请求。
const koa = require('koa');
const Router = require('koa-router');
let server = new koa();
let router = new Router();
server.listen(8080);
server.use(router.routes());
router.get('/api/user', async (ctx, next) => {
ctx.body = {username: 'daryl', age: 18};
});
router.get('/api/jsonp', async (ctx, next) => {
const user = JSON.stringify({username: 'daryl', age: 18});
// jsonp就是这样处理,拿到客户端定义的callback函数,返回函数调用的字符串,将数据包在里面
const { callback } = ctx.query
ctx.body = `${callback}(${user})`;
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<button id="btn_get">get</button>
<button id="btn_jsonp">jsonp</button>
<script>
// 直接fetch请求
let btn_get = document.getElementById('btn_get');
btn_get.addEventListener("click", () => {
fetch('http://localhost:8080/api/user',
)
.then(response => {
return response.json();
})
// 请求的数据
.then(data => {
console.log(data);
}).catch(err => console.log(err));
});
// jsonp
let btn_jsonp = document.getElementById('btn_jsonp');
btn_jsonp.addEventListener('click', () => {
// 删除上个请求的script标签,避免html中充斥大量的无用script标签
let prevScript = document.querySelector(`script[src='http://localhost:8080/api/jsonp?callback=getUser']`);
if (prevScript) {
document.body.removeChild(prevScript);
}
let script = document.createElement('script');
script.setAttribute('src', 'http://localhost:8080/api/jsonp?callback=getUser');
document.body.appendChild(script);
})
// 这里的函数名就是上面的callback参数指定的名字
function getUser(data) {
console.log(data);
}
</script>
</body>
</html>
我们可以看到,当我们直接去fetch
的时候,发生了跨域,但是当我们通过jsonp
处理后,可以拿到后端返回的数据
后端设置
浏览器将CORS
请求分成两类:简单请求(simple request
)和非简单请求(not-so-simple request
)。
只要同时满足以下两大条件,就属于简单请求。
- 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
- HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同时满足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不一样的
简单请求
对于简单请求,浏览器直接发出CORS
请求。具体来说,就是在头信息之中,增加一个Origin
字段。Origin
字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
上面的Origin
字段是null
,是因为我们是使用浏览器直接打开的本地文件,当我们使用live server
模拟真实环境时,就会变成真实的Origin
如果Origin
指定的源,不在许可范围内,服务器会返回一个正常的HTTP
回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin
字段,就知道出错了,从而抛出一个跨域错误;
我们可以在后端设置Access-Control-Allow-Origin
,表示允许那些源进行跨域请求
const koa = require('koa');
const Router = require('koa-router');
let server = new koa();
let router = new Router();
server.listen(8080);
server.use(router.routes());
router.get('/api/user', async (ctx, next) => {
// * 表示允许所有源进行跨域请求
ctx.set("Access-Control-Allow-Origin", "*");
ctx.body = {username: 'daryl', age: 18};
});
后端还可以设置其他响应头,有其他作用,比如:
Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送
Cookie
。默认情况下,Cookie
不包括在CORS
请求之中。设为true
,即表示服务器明确许可,Cookie
可以包含在请求中,一起发给服务器。这个值也只能设为true
,如果服务器不要浏览器发送Cookie
,删除该字段即可。注意:如果要把
Cookie
发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials
为true
,还要将客户端的ajax
请求指定withCredentials
,例如:var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
或者
fetch(url, { credentials: 'include' })
这篇文章跨域设置cookie详细讲解了这个属性和客户端设置
cookie
相关的内容Access-Control-Expose-Headers
该字段可选。
CORS
请求时,XMLHttpRequest
对象的getResponseHeader()
方法只能拿到7个基本字段(简单响应首部):Cache-Control
、Content-Length
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必须在Access-Control-Expose-Headers
里面指定。
例如:
我们设置了自定义头Other-Header
,值为other-header
const koa = require('koa'); const Router = require('koa-router'); let server = new koa(); let router = new Router(); server.listen(8080); server.use(router.routes()); router.get('/api/user', async (ctx, next) => { ctx.set("Access-Control-Allow-Origin", "*"); // 我们设置了自定义头Other-Header,值为other-header ctx.set("Other-Header", "other-header"); ctx.body = {username: 'daryl', age: 18}; });
此时通过
fetch
的api
去获取响应头,是拿不到Other-Header
的<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> </head> <body> <button id="btn_get">get</button> <script> // 直接fetch请求 let btn_get = document.getElementById('btn_get'); btn_get.addEventListener("click", () => { fetch('http://localhost:8080/api/user', ) .then(response => { for (let [key, value] of response.headers) { console.log(`${key} : ${value}`); } return response.json(); }) // 请求的数据 .then(data => { console.log(data); }).catch(err => console.log(err)); }); </script> </body> </html>
但如果我们加上
ctx.set("Access-Control-Expose-Headers", "Other-Header");
此时就可以拿到了router.get('/api/user', async (ctx, next) => { ctx.set("Access-Control-Allow-Origin", "*"); ctx.set("Other-Header", "other-header"); ctx.set("Access-Control-Expose-Headers", "Other-Header"); ctx.body = {username: 'daryl', age: 18}; });
非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT
或DELETE
,或者Content-Type
字段的类型是application/json
。
非简单请求的CORS
请求,会在正式通信之前,增加一次HTTP
查询请求,称为”预检”请求(preflight
)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP
动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。
我们来模拟一个非简单请求,在请求头中加入一个自定义字段Authorization
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<button id="btn_get">get</button>
<script>
// 直接fetch请求
let btn_get = document.getElementById('btn_get');
btn_get.addEventListener("click", () => {
fetch('http://localhost:8080/api/user',
{
headers: {
'Authorization': 'some token'
}
}
)
.then(response => {
return response.json();
})
// 请求的数据
.then(data => {
console.log(data);
}).catch(err => console.log(err));
});
</script>
</body>
</html>
预检请求用的请求方法是OPTIONS
,表示这个请求是用来询问的。头信息里面,关键字段是Origin
,表示请求来自哪个源。
除了Origin
字段,”预检”请求的头信息包括两个特殊字段。
Access-Control-Request-Method
该字段是必须的,用来列出浏览器的
CORS
请求会用到哪些HTTP
方法,上例是GET
。Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器
CORS
请求会额外发送的头信息字段,上例是authorization
。
预检请求也是一个真实的请求,我们后端需要对其进行处理,否则就会出现404
错误(如上图),对于预检请求的响应,关键的是Access-Control-Allow-Origin
字段,表示哪些源可以请求数据,如果在预检请求里面没有指定这个资源,而仅在需要的请求中指定,则也会发生跨域问题;例如:
const koa = require('koa');
const Router = require('koa-router');
let server = new koa();
let router = new Router();
server.listen(8080);
server.use(router.routes());
router.get('/api/user', async (ctx, next) => {
ctx.set("Access-Control-Allow-Origin", "*");
ctx.body = {username: 'daryl', age: 18};
});
router.options('*', async (ctx, next) => {
// 如果预检请求这里没有指定Access-Control-Allow-Origin,而仅在上面设置了,也会发生跨域问题
ctx.set("Access-Control-Allow-Origin", "*");
ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE");
ctx.set("Access-Control-Allow-Headers", "authorization");
ctx.set("Access-Control-Max-Age", "1728000");
ctx.body = 'ok';
})
还有一些其他的相关字段:
Access-Control-Allow-Methods
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。
Access-Control-Allow-Headers
如果浏览器请求包括
Access-Control-Request-Headers
字段,则Access-Control-Allow-Headers
字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
Access-Control-Allow-Credentials
该字段与简单请求时的含义相同
vue跨域配置
vue3.0
首先声明我们真实的接口地址是
http://localhost:8080/api/user
vite.config.js
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'path' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], server:{ port: 3000, proxy: { // /api 属性声明axios中url以/api开头的请求都适用于该规则,例如axios.get('/api/xxx'),对应下面按钮1的请求 '/api' : { target: 'http://localhost:8080', changeOrigin: true, }, // /local_api 属性声明axios中url以/local_api开头的请求都适用于该规则,例如axios.get('/local_api/api/xxx'),对应下面按钮2的请求 // 但是由于我们真实的接口地址是http://localhost:8080/api/user,而此时请求的是http://localhost:8080/local_api/api/user,因此需要将多余的路径进行重写,需要rewrite一下,将local_api/去掉,使其成为http://localhost:8080/api/user '/local_api' : { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => {console.log(path) ;return path.replace(/^\/local_api/, '')} } } } })
<template> <div> <button @click="getData">按钮1</button> <button @click="getData2">按钮2</button> </div> </template> <script> import axios from 'axios' export default { methods: { getData() { axios.get('/api/user').then(res => { console.log(res.data) }) }, getData2() { axios.get('/local_api/api/user').then(res => { console.log(res.data) }) }, } } </script>
两个请求都成功了,请求中的地址是本地的地址,正是因为我们做了代理
vue2.0
vue2.0的配置原理是相同的,只不过写法有一点差异而已,下面是相关配置
vue.config.js
const path = require('path') module.exports = { devServer: { proxy: { '^/api': { // 注意是以/api开头,即:axios.get('/api/xxx'),不能带接口的域名及端口 target: 'http://localhost:8080', // 代理目标 ws: true, // 启用websocket changeOrigin: true }, '^/local_api': { // 注意是以/local_api开头,即:axios.get('/local_api/api/xxx'),不能带接口的域名及端口 target: 'http://localhost:8080', // 代理目标 changeOrigin: true, ws: true, // 启用websocket // 匹配到的地址重写, /local_api/api/xxx => https://www.baihuzi.com/api/xxx pathRewrite: { '^/local_api': '' } } } }, }
注意和
vite
的配置的写法不一样
react跨域配置
react跨域配置需要使用第三方库http-proxy-middleware
来解决,之前在package.json
文件中配置proxy
的方式已经不适用了,可能是希望将跨域配置放到后端来做,这样既省事也能保证安全。
首先需要安装http-proxy-middleware
yarn add http-proxy-middleware
然后在src
目录下新建文件setupProxy.js
文件,项目启动的时候,会自动去找这个文件,我们不需要我们在其他地方引入
const {createProxyMiddleware} = require('http-proxy-middleware')
module.exports = function (app) {
// proxy第一个参数为要代理的路由
// 第二参数中target为代理后的请求网址,changeOrigin是否改变请求头,其他参数请看官网
app.use(createProxyMiddleware('/api', {
target: 'http://localhost:8080',
changeOrigin: true,
}))
app.use(createProxyMiddleware('/local_api', {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/local_api': '' }
}))
}
测试
import React from 'react'
import axios from 'axios'
const GetData = () => {
const getData = () => {
axios.get('/api/user').then(res => {
console.log(res.data);
})
}
const getData2 = () => {
axios.get('/local_api/api/user').then(res => {
console.log(res.data);
})
}
return <>
<button onClick={getData}>按钮1</button>
<button onClick={getData2}>按钮2</button>
</>
}
export default GetData
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com