CORS

Cross-Origin Resource Sharing。是浏览器的行为,出于安全原因,浏览器限制从脚本发起的跨源HTTP请求,浏览器通过HTTP的HEADER来判断是否允许加载跨域的资源。

出现跨域的根本原因:浏览器的同源策略不允许非同源的URL之间进行资源的交互

一个跨域的例子,从https://domain-a.com的前端js代码请求https://domain-b.com/data.json提供的数据。

image-20220926122703916

浏览器对跨域请求的拦截

浏览器允许发起跨域请求,但是,跨域请求回来的数据,会被浏览器拦截,无法被页面获取到!

这是针对简单请求的,如果是预检请求没有返回相应的cors请求头,那么真正的请求不会发送到服务端。

image-20220926154823739

代码验证

TIP

验证 1. cors问题的出现. 2.cors只发生在浏览器端

为了方便快速实验cors机制,采用node+express来开发一个简单的web服务器

服务端代码app.js

import express from "express";

const app = express()
app.get("/test",(req,res)=>{
    res.json({name:'Aeroxian'})
})

app.listen(9000,()=>{
    console.log("server start at 9000");
})

客户端代码:

文件说明,项目采用webpack管理,默认不配置webpack.打包的逻辑是src/index.js,打包后成立main.js

// 前端脚本
import axios from "axios";
const api = "http://localhost:9000/test"

axios.get(api).then(response => {
    console.log(response.data)
}).catch(e => console.log("cors appear"))
<body>
    <h2>Cors 实验</h2>
    <div class="person">
        Hello: <span id="name"></span>
    </div>
    <script src="main.js"></script>
</body>

浏览器中出现跨域请求http://127.0.0.1:5500/dist/index.html请求http://localhost:9000/test的资源,此时跨域问题出现了

image-20220926125820871

Access to XMLHttpRequest at 'http://localhost:9000/test' from origin 'http://127.0.0.1:5500'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

在index.js脚本当中,请求了不同源的资源.浏览器限制加载该资源.(可以看到api接口返回的是200,是可以调通的,但是浏览器限制了资源的加载)

image-20220926130314335

在node环境中执行前端的index.js脚本,可以看到有数据.

image-20220926131603859

解决跨域(Simple Request)❤️

上面的请求资源是一个简单请求(Simple Request).对应的还有预检请求(Preflight request),如自己添加了HTTP header头部信息.

根据Cross-Origin Resource Sharing (CORS) - HTTP | MDN (mozilla.org)open in new window提供的解决方案

This operation performs a simple exchange between the client and the server, using CORS headers to handle the privileges:

image-20220926133921077

从浏览器中查看请求头,发现浏览器自动为我们添加请求头的Origin.要解决cors跨域问题,只需要我们在服务端添加相应的响应头Access-Control-Allow-Origin即可

image-20220926133729516

在服务端(app.js)添加响应头Access-Control-Allow-Origin

import express from "express";

const app = express()
app.get("/test",(req,res)=>{
    console.log("get req");
    // add Access-Control-Allow-Origin header
    res.setHeader("Access-Control-Allow-Origin","*")
    res.json({name:'Aeroxian'})
})

app.listen(9000,()=>{
    console.log("server start at 9000");
})

image-20220926135217955

Preflight request⭐

TIP

与简单请求相比,浏览器自己多发送了一个请求

Preflight request - MDN Web Docs Glossary: Definitions of Web-related terms | MDN (mozilla.org)open in new window

浏览器在发现页面发出的请求非简单请求,并不会立即执行对应的请求代码,而是会触发预先请求模式。预先请求模式会先发送preflight request(预先验证请求),preflight request是一个OPTION请求,用于询问要被跨域访问的服务器,是否允许当前域名下的页面发送跨域的请求。在得到服务器的跨域授权后才能发送真正的HTTP请求。

It is an OPTIONSopen in new window request, using three HTTP request headers: Access-Control-Request-Methodopen in new window, Access-Control-Request-Headersopen in new window, and the Originopen in new window header.

  1. 如下面的案例自己添加了一个X-PINGOTHER的头部信息,在跨域的情况下会先发送预检请求
  2. 然后再发送正真的请求

img

代码验证❤️

TIP

通过自定义头部信息,来发送跨域请求,观察preflight request

import express from "express";

const app = express()
app.get("/test",(req,res)=>{
    console.log("get req");
    res.json({name:'Aeroxian'})
})

// 添加一个options方法,来观察
app.options("/test",(req,res)=>{
    console.log("Preflight Appear");
    res.send("finished:)")
})

app.listen(9000,()=>{
    console.log("server start at 9000");
})
// 前端脚本
import axios from "axios";
const api = "http://localhost:9000/test"
const nameEl = document.getElementById("name")
// 自定义了一个header
const config = {
    headers: {
        'auth-token': 'aeroxian-token'
    }
}

axios.get(api,config).then(response => {
    console.log(response.data)
    nameEl.textContent = response.data.name
    nameEl.className = "success"
}).catch(e => {
    console.log("cors appear")
    nameEl.className = "errors"
    nameEl.textContent = "Cors Appear"
})

浏览器在发送真正的请求之前(http://localhost:9000/test)先发送了一个请求方法为options的请求。预检请求没有返回相应的cors策略,导致跨域问题出现,真正的请求并没有发送。

Access to XMLHttpRequest at 'http://localhost:9000/test' from origin 'http://127.0.0.1:5500' 
has been blocked by CORS policy: Response to preflight request doesn\'t pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

image-20220926171001344

image-20220926171706077

预检查请求是一个options方法

image-20220926171834319

解决跨域

只需要更改服务端的预检请求即可。app.js

import express from "express";

const app = express()
app.get("/test",(req,res)=>{
    console.log("get req");
    res.json({name:'Aeroxian'})
})

// 添加一个options方法,来观察
app.options("/test",(req,res)=>{
    console.log("Preflight Appear");
    // 添加允许的跨域的策略
    res.setHeader("Access-Control-Allow-Origin","*") // 允许所有来源
    // 由于自定义了头部,需要指定允许跨域指定的头部
    res.setHeader("Access-Control-Allow-Headers","auth-token")
    res.send("finished:)")
})

app.listen(9000,()=>{
    console.log("server start at 9000");
})

image-20220926173206563

express提供cors中间件

TIP

Express cors middleware (expressjs.com)open in new window

express提供了cors中间件,专门解决express中跨域的问题,使得我们专注业务开发,不再关注处理跨域的问题。

npm install cors
import express from "express";
import cors from 'cors'

const app = express()

// 使用cors中间件
app.use(cors())

app.get("/test",(req,res)=>{
    console.log("get req");
    res.json({name:'Aeroxian'})
})

app.listen(9000,()=>{
    console.log("server start at 9000");
})
import express from "express";

const app = express()
app.get("/test",(req,res)=>{
    console.log("get req");
    res.json({name:'Aeroxian'})
})

// 添加一个options方法,来观察
app.options("/test",(req,res)=>{
    console.log("Preflight Appear");
    // 添加允许的跨域的策略
    res.setHeader("Access-Control-Allow-Origin","*") // 允许所有来源
    // 由于自定义了头部,需要指定允许跨域指定的头部
    res.setHeader("Access-Control-Allow-Headers","auth-token")
    res.send("finished:)")
})

app.listen(9000,()=>{
    console.log("server start at 9000");
})

cors相关header说明

响应首部字段

跨源资源共享(CORS)- 响应首部字段open in new window

Access-Control-Allow-Origin

顾名思义,访问控制允许的源,主要在server端设置响应的header

// means that the resource can be accessed by any origin
Access-Control-Allow-Origin: *

If the resource owners at https://bar.other wished to restrict access to the resource to requests only from https://foo.example (i.e., no domain other than https://foo.example can access the resource in a cross-origin manner), they would send

// 严格限制
Access-Control-Allow-Origin: https://foo.example

Access-Control-Allow-Methods

首部字段用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。

Access-Control-Allow-Headers

首部字段用于预检请求的响应。其指明了实际请求中允许携带的首部字段

请求首部字段

跨源资源共享(CORS) - 请求首部字段open in new window

Origin

首部字段表明预检请求或实际请求的源站。

Access-Control-Request-Headers

首部字段用于预检请求。其作用是,将实际请求所携带的首部字段告诉服务器。

跨域问题出现的领域

跨域问题只出现在浏览器端发送的请求。如果是后端发送的请求(如node运行js,或者使用postman测试,则不会出现跨域)

但是如果是在浏览器,开发插件的话并不会出现跨域的问题。

使用插件自动添加

image-20220926175702008

该插件会自动添加响应的信息,方便开发测试。

image-20220926180551648

值得注意的是,如果预检请求,尽管返回了响应的cors头部信息,但是如果返回的是不是2xx状态,比如401,那么还是会有跨域的问题。所以该插件尽管添加了这些信息,问题仍然出现。