关于 CORS、跨域、案例演示

关于跨域 和 CORS

CORS 主要用于解决跨域问题。

跨域就是浏览器的浏览器的同源策略(协议+域名+端口)。

网上有太多的总结介绍文章,例如:

跨域解决方案

现在常用的跨域解决方案主要有两个:

  • 代理 Proxy
    • 开发环境需要前端自己配置 web 服务器
    • 生产环境如果不同源,需要运维人员配置
  • CORS
    • 缺点:低版本浏览器不支持
    • 优点:前端几乎不用做任何配置(后端负责配置)

下面通过案例演示

实现跨行

初始化客户端和服务端

创建文件夹 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-Credentialstrue,表示允许发送 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-Typeapplication/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')

非简单请求大致过程

  1. 先发送 OPTIONS 预检请求(preflight),将正式请求的请求方法、请求头字段发送到服务端询问
  2. 服务端收到预检请求,首先检查 Origin,如果允许跨域,则返回一个正常的 HTTP 回应
  3. 服务端还会检查 Access-Control-Request-Methods Access-Control-Request-Headers
    1. 如果检查通过,则会将 Access-Control-Allow-Methods Access-Control-Allow-Headers 包含在响应头中返回
    2. 如果检查不通过,则响应信息中不会包含这两个字段
  4. 浏览器根据预检请求的响应结果,决定是否发送正式请求。
  5. 如果发送正式请求,服务端就会像简单请求一样检查 CORS 相关设置。

开箱即用的 CORS 工具

有一个开箱即用的 cors 工具,可以直接使用:

npm install cors
const express = require('express')
const cors = require('cors')

const app = express()
app.use(cors())
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值