关于跨域 和 CORS
CORS 主要用于解决跨域问题。
跨域就是浏览器的浏览器的同源策略(协议+域名+端口)。
网上有太多的总结介绍文章,例如:
-
阮一峰老师的 跨域资源共享 CORS 详解
-
MDN 的 浏览器的同源策略
-
MDN 的 跨源资源共享(CORS)
跨域解决方案
现在常用的跨域解决方案主要有两个:
- 代理 Proxy
- 开发环境需要前端自己配置 web 服务器
- 生产环境如果不同源,需要运维人员配置
- CORS
- 缺点:低版本浏览器不支持
- 跨源资源共享(CORS) 最下面有介绍,例如 IE 最低支持 IE10
- 优点:前端几乎不用做任何配置(后端负责配置)
- 缺点:低版本浏览器不支持
下面通过案例演示
实现跨行
初始化客户端和服务端
创建文件夹 cors-demo,内部结构如下:
- client - 客户端页面
- server - 开启一个web服务器
编写 client/index.html
,使用 axios
进行跨域 HTTP 请求:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="https://unpkg.com/axios"></script>
<script>
axios({
method: 'GET',
url: 'http://localhost:3000/posts'
}).then(res => {
console.log(res)
})
</script>
</body>
</html>
在 server 目录下使用 express 开启一个web服务器:
cd ./server
npm init -y
npm install express
编写 server/app.js
:
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello world')
})
app.get('/posts', (req, res) => {
res.send('get posts')
})
app.listen(3000, () => {
console.log('Local: http://localhost:3000')
})
分别运行
-
server:使用 node 运行
server/app.js
开启 web 服务。- 建议使用
nodemon server/app.js
运行,减少手动重启的操作
- 建议使用
-
client:开启一个 web 服务,访问 html 页面
-
方法1:使用 npm 工具
serve
开启npm install -g serve cd ../client serve .
-
方法2:使用 vscode 插件 Live Server 开启,右键
Open with Live Server
-
访问 index.html
,页面报错 No 'Access-Control-Allow-Origin' header is present on the requested resource.
。
一般报错提示 Access-Control-Allow-Origin
就肯定和跨域有关。
开启 CORS
CORS 其实就是规定的一个 客户端和服务端共同遵守 的HTTP 协议。
只需服务端设置 Access-Control-Allow-Origin
:
// server/app.js
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello world')
})
app.get('/posts', (req, res) => {
// 设置 CORS
// 允许任意主机名的客户端跨域请求
// res.setHeader('Access-Control-Allow-Origin', '*')
// 与 * 效果差不多
// res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
// 允许指定主机名的客户端跨域请求
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5000')
// 发送响应数据
res.send('get posts')
})
app.listen(3000, () => {
console.log('Local: http://localhost:3000')
})
重启服务端(nodemon会自动重启)。
客户端在请求时发送的请求头包含一个字段 Origin
,表示请求的源,一般是客户端主机名。
如果 Access-Control-Allow-Origin
包含这个主机名,则请求成功,否则请求失败。
服务端发送的响应头中也会包含 Access-Control-Allow-Origin
。
Access-Control-Allow-Origin
的值只能设置为 *
或 一个主机名,不能设置多个。
发送 Cookie
CORS 请求默认不发送 Cookie,需要客户端和服务端进行设置:
- 客户端:在 Ajax 请求时打开
withCredentials
- 服务端:设置响应头
Access-Control-Allow-Credentials
为true
,表示允许发送 Cookie。- 如果服务端不允许发送,而客户端发送了 Cookie,请求就会报错
修改 client/index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="https://unpkg.com/axios"></script>
<script src="https://unpkg.com/js-cookie"></script>
<script>
// 设置
Cookies.set('name', 'jack')
Cookies.set('age', '18')
axios({
method: 'GET',
url: 'http://localhost:3000/posts',
// 打开withCredentials,请求携带 Cookie
withCredentials: true
}).then(res => {
console.log(res)
})
</script>
</body>
</html>
修改 server/app.js
:
// server/app.js
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello world')
})
app.get('/posts', (req, res) => {
// 设置 CORS
// 允许任意主机名的客户端跨域请求
// res.setHeader('Access-Control-Allow-Origin', '*')
// 允许指定主机名的客户端跨域请求
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5000')
// 允许发送 Cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 发送响应数据
res.send('get posts')
})
app.listen(3000, () => {
console.log('Local: http://localhost:3000')
})
Access-Control-Allow-Credentials
只能设置为 true
,如果不要浏览器发送 Cookie,不设置该字段即可。
客户端获取自定义响应头
CORS请求时,默认 Ajax 请求只能获取响应头的 6 个基本字段。如果想获取其他字段,需要服务端指定。
添加自定义响应头:
// server/app.js
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello world')
})
app.get('/posts', (req, res) => {
// 设置 CORS
// 允许任意主机名的客户端跨域请求
// res.setHeader('Access-Control-Allow-Origin', '*')
// 允许指定主机名的客户端跨域请求
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5000')
// 允许发送 Cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 设置自定义响应头
res.setHeader('token', `t_${new Date().getTime()}`)
// 发送响应数据
res.send('get posts')
})
app.listen(3000, () => {
console.log('Local: http://localhost:3000')
})
查看请求结果:
响应头确认已添加
但是请求结果未获取到该字段:
在服务端设置 Access-Control-Expose-Headers
指定客户端可以获取的其他字段:
// server/app.js
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello world')
})
app.get('/posts', (req, res) => {
// 设置 CORS
// 允许任意主机名的客户端跨域请求
// res.setHeader('Access-Control-Allow-Origin', '*')
// 允许指定主机名的客户端跨域请求
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5000')
// 允许发送 Cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 设置自定义响应头
res.setHeader('token', `t_${new Date().getTime()}`)
// 添加客户端可以获取的其他字段,多个用逗号 `,` 隔开
res.setHeader('Access-Control-Expose-Headers', 'token')
// 发送响应数据
res.send('get posts')
})
app.listen(3000, () => {
console.log('Local: http://localhost:3000')
})
响应头增加返回了 Access-Control-Expose-Headers
:
Ajax 请求成功获取该字段:
添加的响应头会正常展示在响应信息中,但是如果前后端没有进行相应的设置,前端也获取不到,了解这一点,可以避免项目中后端把锅甩给前端。
CORS 响应头总结
- Access-Control-Allow-Origin:开启 CORS
- Access-Control-Allow-Credentials: 允许浏览器发送 Cookie
- Access-Control-Expose-Headers: 允许Ajax请求获取其他的响应头字段
简单请求和非简单请求
非简单请求就是被认为对服务器有特殊要求的请求。
就过程而言,两者的主要区别就是,非简单请求会比简单请求多发送一次预检请求(preflight)。
浏览器先发送预检请求,询问服务器这个请求是否可以通过。得到答复后才会发送正式请求。
满足条件
只要同时满足以下两大条件,就属于简单请求。
(1) 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
(2)HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded
(表单提交)、multipart/form-data
(提交文件)、text/plain
(普通文本)
凡是不同时满足上面两个条件,就属于非简单请求。
项目中常见的非简单请求就是 Content-Type
为 application/json
的请求。
可以简单理解为凡是会产生副作用的请求,都是复杂请求。
POST 修改资源理论上每次请求的结果都应该是一样的,不过这只是一种规范并没有强制开发者去实现,很多 POST 请求实现都会产生不同的结果
预检请求(preflight)
预检请求的目的主要是为了避免资源浪费。
它是一个 HTTP 查询请求(请求方法是 OPTIONS
),只会发送询问所需的信息,不会发送请求体。
当服务端确认允许请求后,发送正式请求的时候,才会发送请求体。
发送一个非简单请求
修改 client/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="https://unpkg.com/axios"></script>
<script src="https://unpkg.com/js-cookie"></script>
<script>
// 设置
Cookies.set('name', 'jack')
Cookies.set('age', '18')
// axios({
// method: 'GET',
// url: 'http://localhost:3000/posts',
// // 打开withCredentials,请求携带 Cookie
// // withCredentials: true
// }).then(res => {
// console.log(res)
// })
axios({
method: 'POST',
url: 'http://localhost:3000/posts',
// 请求体,axios 默认 Content-Type: application/json
data: {
foo: 'bar'
}
}).then(res => {
console.log(res)
})
</script>
</body>
</html>
服务端添加 POST 接口:
// server/app.js
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello world')
})
app.get('/posts', (req, res) => {
// 设置 CORS
// 允许任意主机名的客户端跨域请求
// res.setHeader('Access-Control-Allow-Origin', '*')
// 允许指定主机名的客户端跨域请求
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5000')
// 允许发送 Cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 设置自定义响应头
res.setHeader('token', `t_${new Date().getTime()}`)
// 添加客户端可以获取的其他字段,多个用逗号 `,` 隔开
res.setHeader('Access-Control-Expose-Headers', 'token')
// 发送响应数据
res.send('get posts')
})
// 正式请求
app.post('/posts', (req, res) => {
// 开启 CORS
res.setHeader('Access-Control-Allow-Origin', '*')
// 判断客户端请求是否发送过来
console.log('接收到了客户端请求')
res.send('post posts')
})
app.listen(3000, () => {
console.log('Local: http://localhost:3000')
})
刷新页面查看请求记录:
- 一共发送了两次请求
- 先发送了预检请求 OPTIONS
- 然后是 POST 请求,但是失败了,根本并没有发送到服务端
- 控制台报错:预检请求(preflight)没有未允许跨域请求
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
给预检请求开启 CORS
配置预检请求,并开启 CORS
// server/app.js
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello world')
})
app.get('/posts', (req, res) => {
// 设置 CORS
// 允许任意主机名的客户端跨域请求
// res.setHeader('Access-Control-Allow-Origin', '*')
// 允许指定主机名的客户端跨域请求
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5000')
// 允许发送 Cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 设置自定义响应头
res.setHeader('token', `t_${new Date().getTime()}`)
// 添加客户端可以获取的其他字段,多个用逗号 `,` 隔开
res.setHeader('Access-Control-Expose-Headers', 'token')
// 发送响应数据
res.send('get posts')
})
// 预检请求
app.options('/posts', (req, res) => {
// 开启 CORS
res.setHeader('Access-Control-Allow-Origin', '*')
res.send('options posts')
})
// 正式请求
app.post('/posts', (req, res) => {
// 开启 CORS
res.setHeader('Access-Control-Allow-Origin', '*')
// 判断客户端请求是否发送过来
console.log('接收到了客户端请求')
// 结束响应,否则会一致挂起
res.send('post posts')
})
app.listen(3000, () => {
console.log('Local: http://localhost:3000')
})
刷新页面:
- 预检请求成功,正式请求仍然未能发送
- 控制台报错:请求头字段
content-type
不在预检请求(preflight)响应字段Access-Control-Allow-Headers
的允许范围内。Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
预检请求的请求和响应
预检请求的头信息会包含两个特殊字段:
- Access-Control-Request-Method:当前CORS请求的请求方法
- Access-Control-Request-Headers:当前CORS请求额外发送的请求头字段
服务端收到预检请求后会检查以下3个内容:
- Origin 主机名是否允许
- Access-Control-Request-Method 请求方法是否允许
- Access-Control-Request-Headers 请求头字段是否允许
它们分别对应服务端设置的以下字段,并包含在响应信息中发送给客户端:
- Access-Control-Allow-Origin
- Access-Control-Allow-Method
- Access-Control-Allow-Headers
设置预检请求的响应
// server/app.js
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello world')
})
app.get('/posts', (req, res) => {
// 设置 CORS
// 允许任意主机名的客户端跨域请求
// res.setHeader('Access-Control-Allow-Origin', '*')
// 允许指定主机名的客户端跨域请求
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5000')
// 允许发送 Cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 设置自定义响应头
res.setHeader('token', `t_${new Date().getTime()}`)
// 添加客户端可以获取的其他字段,多个用逗号 `,` 隔开
res.setHeader('Access-Control-Expose-Headers', 'token')
// 发送响应数据
res.send('get posts')
})
// 预检请求
app.options('/posts', (req, res) => {
// 开启 CORS
res.setHeader('Access-Control-Allow-Origin', '*')
// 设置允许跨域的请求方法,多个以逗号`,`隔开
res.setHeader('Access-Control-Allow-Method', 'POST, DELETE, PUT')
// 设置允许跨域发送的请求头字段,多个以逗号`,`隔开
res.setHeader('Access-Control-Allow-Headers', 'content-type')
// 结束响应,否则会一致挂起
res.send('options posts')
})
// 正式请求
app.post('/posts', (req, res) => {
// 开启 CORS
res.setHeader('Access-Control-Allow-Origin', '*')
// 判断客户端请求是否发送过来
console.log('接收到了客户端请求')
res.send('post posts')
})
app.listen(3000, () => {
console.log('Local: http://localhost:3000')
})
正式请求发送成功。
添加请求头
// client/index.html
axios({
method: 'POST',
url: 'http://localhost:3000/posts',
// 请求体,axios 默认 Content-Type: application/json
data: {
foo: 'bar'
},
headers: {
Authorization: 'xxxxx'
}
}).then(res => {
console.log(res)
})
// server/app.js
res.setHeader('Access-Control-Allow-Headers', 'content-type, Authorization')
非简单请求大致过程
- 先发送 OPTIONS 预检请求(preflight),将正式请求的请求方法、请求头字段发送到服务端询问
- 服务端收到预检请求,首先检查
Origin
,如果允许跨域,则返回一个正常的 HTTP 回应 - 服务端还会检查
Access-Control-Request-Methods
Access-Control-Request-Headers
- 如果检查通过,则会将
Access-Control-Allow-Methods
Access-Control-Allow-Headers
包含在响应头中返回 - 如果检查不通过,则响应信息中不会包含这两个字段
- 如果检查通过,则会将
- 浏览器根据预检请求的响应结果,决定是否发送正式请求。
- 如果发送正式请求,服务端就会像简单请求一样检查 CORS 相关设置。
开箱即用的 CORS 工具
有一个开箱即用的 cors 工具,可以直接使用:
npm install cors
const express = require('express')
const cors = require('cors')
const app = express()
app.use(cors())