了解跨域资源共享(CORS)

2025-05-25

了解跨域资源共享(CORS)

跨域资源共享( CORS ) 是一种从一个地方向另一个地方发出 HTTP 请求的方式。历史上,浏览器只允许使用 JavaScript 从同一个域发出请求,并且同源策略会强制执行,从而阻止跨域类型的请求。

CORS 授权服务器控制谁可以发出请求以及允许哪些类型的请求。浏览器是执行 CORS 策略的客户端。

服务器可以配置:

  • 哪些域名可以发出 HTTP 请求
  • 允许哪些 HTTP 方法(GET、POST、PUT、DELETE 等)。
  • 请求中允许使用哪些标头。
  • 请求是否可以包含 cookie 信息。
  • 客户端可以读取哪些响应标头。

简介 TLDR;

CORS 允许不同主机上的客户端访问服务器上的受限资源。

本教程将涵盖您需要了解的有关 CORS 的所有内容Access-Control-Allow-Origin: *。是的,有很多东西要学!

同源 vs 跨源

为了简单起见,同源请求就像房子里的两个人进行交流。

同源通信示例

由于 Alice 和 Bob 住在同一所房子里(同一个起源),因此他们之间没有沟通障碍。

对比

跨域请求就像有两栋不同的房子,一栋房子里的人与另一栋房子里的人进行交流

跨域通信示例

由于Bob想与住在不同房子里的Charlie通信,因此这被视为跨域通信,因为这两个家庭位于不同的源。由于是Bob发起的通话,因此Charlie所在的房子需要批准Bob的通话。

TLDR;

同源请求是指从一个主机向同一主机发出请求,而跨源请求是指从一个主机向不同主机发出请求。

同源请求

举个例子,我们先发起一个同源请求。我们将创建一个简单的服务器,为客户端提供一个端点,并提供一个 HTML 页面。为了简单起见,本教程将使用 Node.js 和Express服务器。

文件server.js



const express = require('express')
const app = express()
const port = 8000

app.use(express.static(__dirname))
app.get('/api/posts', (req, res) => {
  res.json([
    {id: 1, content: 'foo'},
    {id: 1, content: 'bar'},
  ])
})
app.listen(port, () => {
  console.log(`listening on port ${port}`)
})


Enter fullscreen mode Exit fullscreen mode

文件index.html




<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
  <script>
    (async () => {
      const res = await fetch('http://localhost:8000/api/posts')

      console.log(await res.json())
    })()
  </script>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode


$ node server.js
listening on port 8000


Enter fullscreen mode Exit fullscreen mode

如果你访问http://localhost:8000/ ,你会看到请求成功了。没有什么奇怪的,也很正常,因为是同源的。

无 CORS 错误

跨域请求

现在,为了显示跨域请求,让我们启动一个新服务器来监听不同端口上的 HTML 文件。

文件server2.js



const express = require('express')
const app = express()
const port = 9000

app.use(express.static(__dirname))
app.listen(port, () => {
  console.log(`listening on port ${port}`)
})


Enter fullscreen mode Exit fullscreen mode


$ node server.js
listening on port 8000


Enter fullscreen mode Exit fullscreen mode


$ node server2.js
listening on port 9000


Enter fullscreen mode Exit fullscreen mode

让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么

CORS 错误

浏览器抛出错误:



Access to fetch at 'http://localhost:8000/api/posts' from origin 'http://localhost:9000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.


Enter fullscreen mode Exit fullscreen mode

为了说明为什么会发生这种情况,我们通过一个简单的聊天示例来看看浏览器和服务器如何通信。

浏览器: “你好,服务器http://127.0.0.1:8000,请提供 /api/posts 的数据给我,并告诉我http://localhost:9000上的客户端是否可以访问。以下是 HTTP 消息:”



Get /api/posts HTTTP1.1
User-Agent: Chrome
Host: 127.0.0.1:8000
Accept: */*
Origin: http://localhost:9000


Enter fullscreen mode Exit fullscreen mode

服务器: “这是数据,客户端无法访问,因为我没有将源http://localhost:9000作为允许访问数据的源。”



HTTP/1.1 200 OK


Enter fullscreen mode Exit fullscreen mode

服务器会响应数据,但浏览器不会将其提供给 JavaScript 客户端,除非服务器根据其来源表示允许该客户端。

为了允许跨域请求,服务器必须设置Access-Control-Allow-Origin响应标头。

现在,我们再尝试在服务器上设置 CORS,为每个请求添加一个新的 header。我们将添加Access-Control-Allow-Origin一个通配符*值,告诉浏览器任何来源都可以访问该资源。

文件server.js



const express = require('express')
const app = express()
const port = 8000

app.use(express.static(__dirname))
app.use((req, res, next) => {
  res.set('Access-Control-Allow-Origin', '*')
  next()
})
app.get('/api/posts', (req, res) => {
  res.json([
    {id: 1, content: 'foo'},
    {id: 1, content: 'bar'},
  ])
})
app.listen(port, () => {
  console.log(`listening on port ${port}`)
})


Enter fullscreen mode Exit fullscreen mode

重启服务器:



$ node server.js
listening on port 8000


Enter fullscreen mode Exit fullscreen mode


$ node server2.js
listening on port 9000


Enter fullscreen mode Exit fullscreen mode

附注:server2.js本教程中不会更改,因此您可以继续运行。我们只会server.js从现在开始进行更改。

好的,现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么

使用允许来源标头时没有 CORS 错误

为了说明它现在为何有效,让我们再次通过一个简单的聊天示例来看一下浏览器和服务器如何进行通信。

浏览器: “你好,服务器http://127.0.0.1:8000,请提供 /api/posts 的数据给我,并告诉我http://localhost:9000上的客户端是否可以访问。以下是 HTTP 消息:”



Get /api/posts HTTTP1.1
User-Agent: Chrome
Host: 127.0.0.1:8000
Accept: */*
Origin: http://localhost:9000


Enter fullscreen mode Exit fullscreen mode

服务器: “这是数据,任何客户端都可以访问:”



HTTP/1.1 200 OK
Access-Control-Allow-Origin: *


Enter fullscreen mode Exit fullscreen mode

访问控制允许来源

最重要的 CORS 标头是:

  • Origin请求标
  • 响应Access-Control-Allow-Origin

源是主机,由协议、主机名和端口组成:



Origin = protocol + hostname + port


Enter fullscreen mode Exit fullscreen mode

示例协议:

  • http
  • https

主机名示例:

  • 本地主机
  • example.com
  • foo.example.com

示例端口:

  • 80
  • 443
  • 8000

起源示例:

当无法确定浏览器来源时(例如使用绝对文件路径时),该值null可以用作有效值Originfile:///Users/alice/wwww/index.html

浏览器不允许更改原始标头,否则客户端可以假装是其他人。

标头的值Access-Control-Allow-Origin可以是通配符,也可以是原始值。

有效标头的示例:



Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: http://localhost:8000
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Origin: null


Enter fullscreen mode Exit fullscreen mode

您只能有一个值,因此像这样具有多个值是无效的:



# Not valid values, can't have multiple!
Access-Control-Allow-Origin: http://localhost:8000, http://example.com


Enter fullscreen mode Exit fullscreen mode

为了支持多个来源,您必须使用发出请求的当前来源动态替换标头值。

例如,您首先要检查来源是否已列入白名单,然后设置标头:



if (whitelist.contains(req.get('origin')) {
  res.set('Access-Control-Allow-Origin', req.get('origin'))
}


Enter fullscreen mode Exit fullscreen mode

对于 localhost 条目,您可以使用正则表达式来匹配所有端口号,例如

正则表达式/^http://\/\/localhost(:\d+)?$/i匹配



http://localhost
http://localhost:1
http://localhost:10
http://localhost:100
http://localhost:1000 
http://localhost:2000 etc


Enter fullscreen mode Exit fullscreen mode

您可以使用硬编码白名单、正则表达式或数据库查询。黑名单总比没有好,但不建议使用。

原产地验证步骤:

  • Origin从标题读取值
  • 使用白名单验证来源
  • 将原点设置为Access-Control-Allow-Origin

null如果您不介意未设置来源,则可以将其添加到白名单中。



Access-Control-Allow-Origin: null


Enter fullscreen mode Exit fullscreen mode

TLDR;

使用Access-Control-Allow-Origin标头告诉浏览器哪些来源被允许访问数据。

预检请求

对于某些 HTTP 方法,浏览器会通过所谓的预检请求来请求权限。如果浏览器批准预检请求的响应,则会发出实际请求。如果浏览器不批准预检请求的响应,则永远不会发出实际请求。

触发预检请求的 HTTP 方法有:

  • PUT
  • PATCH
  • DELETE
  • TRACE

不触发预检请求的 HTTP 方法称为简单请求,它们包括:

  • HEAD
  • GET
  • POST

预检请求是在主请求之前由浏览器向服务器发出的一个小请求,其中包含诸如所使用的 HTTP 方法以及是否存在任何 HTTP 标头等信息。服务器会通过返回 2xx HTTP 状态代码来判断浏览器是否应该发送实际请求,或者返回一个指示客户端不应发送实际请求的错误。

预检请求可防止服务器接收不必要的跨域请求。如果服务器启用了 CORS,它将知道如何处理预检请求并做出相应的响应。如果服务器不理解或不关心 CORS,则不会发送正确的预检响应。

预检请求使用OPTIONSHTTP 方法。

在以下情况下会触发预检:

  • GET客户端请求正在使用除、POST之外的方法HEAD(称为简单方法

  • Content-Type客户端使用除以下值以外的值设置请求标头:

    • application/x-www-form-urencoded
    • multipart/form-data
    • text/plain
  • 客户端设置了以下附加请求标头:

    • Accept
    • Accept-Language
    • Content-Language

预检响应应在200范围之内(HTTP 代码 204 为标准),并且不包含正文。包含正文也会让开发人员感到困惑。

让我们发出一个DELETE请求来演示预检请求。首先,我们将检查该请求是否为预检请求。如果是,则返回 HTTP204 No Content状态码。

文件server.js



const express = require('express')
const app = express()
const port = 8000

const isPreflight = (req) => {
  return (
    req.method === 'OPTIONS' &&
    req.headers['origin'] &&
    req.headers['access-control-request-method']
  )
}

app.use(express.static(__dirname))
app.use((req, res, next) => {
  res.set('Access-Control-Allow-Origin', '*')

  if (isPreflight(req)) {
    res.status(204).end()
    return
  }

  next()
})
app.get('/api/posts', (req, res) => {
  res.json([
    {id: 1, content: 'foo'},
    {id: 1, content: 'bar'},
  ])
})
app.delete('/api/posts', (req, res) => {
  res.json({success: true})
})
app.listen(port, () => {
  console.log(`listening on port ${port}`)
})


Enter fullscreen mode Exit fullscreen mode

文件index.html



<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
  <script>
    (async () => {
      const res = await fetch('http://localhost:8000/api/posts', {
        method: 'DELETE'
      })

      console.log(await res.json())
    })()
  </script>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

重启服务器:



$ node server.js
listening on port 8000


Enter fullscreen mode Exit fullscreen mode


$ node server2.js
listening on port 9000


Enter fullscreen mode Exit fullscreen mode

现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么

缺少标头时出现 CORS 错误

我们可以看到错误提示,该方法DELETE不被 CORS 允许:



Access to fetch at 'http://localhost:8000/api/posts' from origin 'http://localhost:9000' has been blocked by CORS policy: Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.


Enter fullscreen mode Exit fullscreen mode

请记住,诸如、或之类的简单请求GET不会POST启动HEAD预检请求,并且它们会被 CORS 自动允许,但诸如DELETE或之类的方法PUT需要服务器明确允许。

为了允许DELETE客户端执行该方法,我们需要Access-Control-Allow-Methods在预检请求中添加响应标头。

文件server.js



app.use((req, res, next) => {
  res.set('Access-Control-Allow-Origin', '*')

  if (isPreflight(req)) {
    res.set('Access-Control-Allow-Methods', 'DELETE') // Add this!
    res.status(204).end()
    return
  }

  next()
})


Enter fullscreen mode Exit fullscreen mode

现在请求DELETE可以按预期工作了:

允许方法标头没有 CORS 错误

请注意两个屏幕截图中请求的数量不同。

  • 在第一个屏幕截图中,预检请求失败,因此实际DELETE请求从未发出,仅创建了 1 个请求。

飞行前检查失败

  • 在第二个屏幕截图中,预检请求成功,因此DELETE发出了实际请求,创建了 2 个请求。

飞行前成功

要拒绝预检请求:

  • 省略Access-Control-Allow-Origin标题。
  • Access-Control-Allow-Methods返回与标题不匹配的Access-Control-Request-Method
  • 如果预检请求有Access-Control-Request-Header
    • 省略Access-Control-Allow-Headers标题。
    • 返回一个值Access-Control-Allow-Headers返回与标头不匹配的标头Access-Control-Request-Headers

预检响应和实际响应都需要Access-Control-Allow-Origin标头。

浏览器可能会缓存对该来源的资源的第一个请求的预检响应,以避免一直发送额外的查询。

预检是无状态的,这意味着实际请求不包含任何将其与预检请求联系起来的信息。

预检请求永远不会遵循重定向。如果您尝试发出预检请求,但服务器尝试重定向,则预检请求将失败。您可以手动检查Location标头,以了解服务器尝试将您重定向到的位置。只有简单的 CORS 请求(GET、POST、HEAD)才会遵循重定向。

如果重定向是同一个服务器,那么Origin标头将保持不变,否则将设置为null

TLDR;

预检请求根据服务器允许的内容确定客户端是否有权发出实际请求。该请求必须是一个OPTIONS方法,具有Access-Control-Request-Method标头(例如DELETEPUT),并且包含Origin标头才会被视为预检请求。

访问控制请求方法

Access-Control-Request-Method是单个请求标头值,用于请求使用特定 HTTP 方法的权限。当客户端发送非简单方法请求时,浏览器会设置此标头。

Access-Control-Request-Method仅在预检请求中发送

下面的屏幕截图显示了Access-Control-Request-Method预检请求中的请求标头,并显示了Access-Control-Allow-Methods我们之前执行请求时的预检响应中的响应标头DELETE

CORS 预检访问控制请求方法

使用准确的预检标头来保护您的服务器免受意外请求的影响。如果您的服务器只允许GET请求,则不要将其他标头放在Access-Control-Allow-Methods标头中添加其他标头。

TLDR;

浏览器会Access-Control-Request-Method在预检请求中发送请求头,告知服务器它打算在实际请求中使用请求的方法。如果服务器允许该方法,浏览器就会发出实际请求。

访问控制允许方法

Access-Control-Allow-Methods预检请求,以告知客户端哪些方法允许使用 CORS。

例如,Access-Control-Allow-Methods: DELETE标头指示服务器允许客户端进行DELETE向 URL 发出请求。

请求头Access-Control-Allow-Methods可以有多个值,例如:



Access-Control-Allow-Methods: HEAD, GET, POST, PUT, PATCH, DELETE


Enter fullscreen mode Exit fullscreen mode

请记住GET,、、POSTHEAD是简单的方法并且始终允许,因此将它们放在标题中是多余且不必要的,但有些人喜欢将它们放在标题中,因为这样对他们来说更清楚并避免混淆。

示例显示允许使用 PUT 和 DELETE(除了简单方法):

允许的方法

TLDR;

服务器应该通过Access-Control-Allow-Methods响应头进行响应,让浏览器知道客户端可以执行哪些 HTTP 方法。

访问控制请求标头

请求Access-Control-Request-Headers标头在预检请求中发送,以便让服务器知道客户端将在实际响应中发送哪些标头。

让我们尝试向服务器发送自定义标头。

文件index.html



<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
  <script>
    (async () => {
      const res = await fetch('http://localhost:8000/api/posts', {
        headers: new Headers({
          'My-Custom-Header': 'hello world'
        })
      })

      console.log(await res.json())
    })()
  </script>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么

使用自定义标头时出现 CORS 错误

浏览器响应错误:



Access to fetch at 'http://localhost:8000/api/posts' from origin 'http://localhost:9000' has been blocked by CORS policy: Request header field my-custom-header is not allowed by Access-Control-Allow-Headers in preflight response.


Enter fullscreen mode Exit fullscreen mode

由于浏览器强制执行 CORS 策略,因此 JavaScript 无法设置的标头包括:

  • Accept-Charset
  • Accept-Encoding
  • Access-Control-Request-Headers
  • Access-Control-Request-Method
  • Connection
  • Content-Length
  • Cookie
  • Cookie2
  • Date
  • DNT
  • Expect
  • Host
  • Keep-Alive
  • Origin
  • Referer
  • TE
  • Trailer
  • Transfer-Encoding
  • Upgrade
  • User-Agent
  • Via
  • Proxy-以或开头的标题Sec-

这些标头只能由浏览器设置,因为它们具有特殊含义。浏览器会忽略您为这些标头设置的值。

对于其他标头,服务器必须允许客户端在跨源请求中包含自定义请求标头。

我们可以看到,浏览器在预检请求中通过以下标头请求访问自定义标头的权限Access-Control-Request-Headers

预检请求标头

服务器需要将批准的自定义请求标头列入白名单,如果没有将请求标头列入白名单,则请求将失败。

如果执行同源请求,则请求可以包含任何自定义请求标头,因为来源是受信任的。

默认情况下,CORS 仅允许客户端读取这些响应标头:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

如果服务器设置了额外的响应头,客户端将无法看到它们。为了让客户端能够看到额外的响应头,服务器需要将这些响应头暴露出来,我们稍后会讲到。

因此,为了让服务器能够接受客户端的自定义标头,它需要通过Access-Control-Allow-Headers在预检响应中设置响应标头来明确允许该标头。

文件server.js



const express = require('express')
const app = express()
const port = 8000

const isPreflight = (req) => {
  return (
    req.method === 'OPTIONS' &&
    req.headers['origin'] &&
    req.headers['access-control-request-method']
  )
}

app.use(express.static(__dirname))
app.use((req, res, next) => {
  res.set('Access-Control-Allow-Origin', '*')

  if (isPreflight(req)) {
    res.set('Access-Control-Allow-Methods', 'DELETE')
    res.set('Access-Control-Allow-Headers', 'My-Custom-Header') // Add this!
    res.status(204).end()
    return
  }

  next()
})
app.get('/api/posts', (req, res) => {
  res.json([
    {id: 1, content: 'foo'},
    {id: 1, content: 'bar'},
  ])
})
app.delete('/api/posts', (req, res) => {
  res.json({success: true})
})
app.listen(port, () => {
  console.log(`listening on port ${port}`)
})


Enter fullscreen mode Exit fullscreen mode

重启服务器:



$ node server.js
listening on port 8000


Enter fullscreen mode Exit fullscreen mode


$ node server2.js
listening on port 9000


Enter fullscreen mode Exit fullscreen mode

现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么

允许的标头没有 CORS 错误

预检使用允许的标头进行响应,因此浏览器继续发送自定义请求标头进行实际请求。

允许标题

请记住,以 开头的标头Access-Control-Request-是浏览器向服务器请求权限的请求标头,以 开头的标头Access-Control-Allow-是服务器向浏览器授予权限的响应标头。

Access-Control-Allow-Headers仅需出现在飞行前响应中。

TLDR;

浏览器Access-Control-Request-Headers在预检请求中发送一个请求标头,让服务器知道实际请求将请求指定的标头。如果服务器拒绝请求这些标头,则不会发送实际请求。

访问控制公开标头

Access-Control-Allow-Headers头由预检使用来指示请求中允许哪些标头,而Access-Control-Expose-Headers标头由实际响应使用来指示哪些响应标头对客户端可见。

如果服务器未设置公开标头,则客户端将无法读取响应标头。

始终向客户端公开的标头是简单的标头,它们是:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

作为示例,让我们尝试读取客户端上的所有响应头:

文件index.html



<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
  <script>
    (async () => {
      const res = await fetch('http://localhost:8000/api/posts')

      console.log(Array.from(await res.headers.entries()))
    })()
  </script>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么

客户端标头

没有什么令人惊讶的,我们得到了标准标题。

让我们在服务器端设置一个自定义响应头。



const express = require('express')
const app = express()
const port = 8000

const isPreflight = (req) => {
  return (
    req.method === 'OPTIONS' &&
    req.headers['origin'] &&
    req.headers['access-control-request-method']
  )
}

app.use(express.static(__dirname))
app.use((req, res, next) => {
  res.set('Access-Control-Allow-Origin', '*')

  if (isPreflight(req)) {
    res.set('Access-Control-Allow-Methods', 'DELETE')
    res.set('Access-Control-Allow-Headers', 'My-Custom-Header')
    res.status(204).end()
    return
  } else {
    res.set('Timezone-Offset', '240') // Add this!
  }

  next()
})
app.get('/api/posts', (req, res) => {
  res.json([
    {id: 1, content: 'foo'},
    {id: 1, content: 'bar'},
  ])
})
app.delete('/api/posts', (req, res) => {
  res.json({success: true})
})
app.listen(port, () => {
  console.log(`listening on port ${port}`)
})


Enter fullscreen mode Exit fullscreen mode

现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么

客户端标头

等等,我们看到的只是相同的标头。新的标头并没有提供。这是因为服务器需要使用响应标头来设置允许客户端读取哪些标头Access-Control-Expose-Headers

让我们添加Access-Control-Expose-Headers一个常规响应标头来告诉浏览器客户端被允许读取自定义标头:

文件server.js



const express = require('express')
const app = express()
const port = 8000

const isPreflight = (req) => {
  return (
    req.method === 'OPTIONS' &&
    req.headers['origin'] &&
    req.headers['access-control-request-method']
  )
}

app.use(express.static(__dirname))
app.use((req, res, next) => {
  res.set('Access-Control-Allow-Origin', '*')

  if (isPreflight(req)) {
    res.set('Access-Control-Allow-Methods', 'DELETE')
    res.set('Access-Control-Allow-Headers', 'My-Custom-Header')
    res.status(204).end()
    return
  } else {
    res.set('Access-Control-Expose-Headers', 'Timezone-Offset') // Add this!
    res.set('Timezone-Offset', '240')
  }

  next()
})
app.get('/api/posts', (req, res) => {
  res.json([
    {id: 1, content: 'foo'},
    {id: 1, content: 'bar'},
  ])
})
app.delete('/api/posts', (req, res) => {
  res.json({success: true})
})
app.listen(port, () => {
  console.log(`listening on port ${port}`)
})


Enter fullscreen mode Exit fullscreen mode

重启服务器:



$ node server.js
listening on port 8000


Enter fullscreen mode Exit fullscreen mode


$ node server2.js
listening on port 9000


Enter fullscreen mode Exit fullscreen mode

现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么

读取自定义标头时没有 CORS 错误

我们现在可以在客户端读取服务器发送到浏览器的自定义标头。

TLDR;

用于Access-Control-Expose-Headers允许客户端读取额外的非简单标头。

访问控制最大年龄

Access-Control-Max-Age头指示预检响应应缓存多长时间(以秒为单位)。让我们通过Access-Control-Max-Age在预检响应中设置标头来告诉浏览器将预检响应缓存 2 分钟(120 秒)。

文件server.js



app.use((req, res, next) => {
  res.set('Access-Control-Allow-Origin', '*')

  if (isPreflight(req)) {
    res.set('Access-Control-Allow-Methods', 'DELETE')
    res.set('Access-Control-Allow-Headers', 'My-Custom-Header')
    res.set('Access-Control-Max-Age', '120') // Add this!
    res.status(204).end() 
    return
  } else {
    res.set('Access-Control-Expose-Headers', 'Timezone-Offset')
    res.set('Timezone-Offset', '240')
  }

  next()
})


Enter fullscreen mode Exit fullscreen mode

重启服务器:



$ node server.js
listening on port 8000


Enter fullscreen mode Exit fullscreen mode


$ node server2.js
listening on port 9000


Enter fullscreen mode Exit fullscreen mode

我们可以Access-Control-Max-Age在预检响应头中看到该头:

CORS 最大年龄

Firefox 不允许缓存项目超过 24 小时,而 Chrome、Opera 和 Safari 最多缓存项目 5 分钟。如果Access-Control-Max-Age未指定,则 Firefox 不会缓存预检,而 Chrome、Opera 和 Safari 会缓存预检 5 秒。

通过减少网络请求的数量来最大化Access-Control-Max-Age标头以获得更好的移动体验。

为了防止代理服务器缓存来自一个客户端的响应并将其发送给另一个客户端,请使用Vary: Origin响应标头来指示将有所Access-Control-Allow-Origin不同,不应缓存



Vary: Origin


Enter fullscreen mode Exit fullscreen mode

例如:



if (isPreflight(req)) {
  if (whitelist.contains(req.get('origin'))) {
    res.set('Access-Control-Allow-Origin', req.get('origin'))
  }

  res.set('Vary', 'Origin')
} else {
  res.set('Access-Control-Allow-Origin', '*')
}


Enter fullscreen mode Exit fullscreen mode

TDLR;

告诉Access-Control-Max-Age浏览器缓存预检响应的时间(以秒为单位)。

访问控制允许凭证

Access-Control-Allow-Credentials标头用于允许客户端在跨源请求中发送 cookie 等敏感信息。

由于 Cookie 包含敏感信息,出于安全考虑,需要进行额外配置。如果始终启用“选择加入”功能,意外将个人信息发送到其他来源的网站将非常危险。

JavaScript document.cookieAPI 无法从其他来源读取或写入该值。调用document.cookie仅返回客户端自身的 Cookie,而不会返回跨源 Cookie。Cookie 本身使用同源策略,每个 Cookie 都包含一个路径和一个域名,只有与路径和域名匹配的页面才能读取该 Cookie。

Cookie 在以下情况下最有效:

  • 您希望在自己的客户端和服务器生态系统内授权用户。
  • 您确切地知道哪些客户端将访问您的服务器。

网站通过用户凭证识别用户,其中最常用的形式是 Cookie。服务器使用 Cookie 来存储用于标识用户使用情况的唯一标识符,例如与用户 ID 绑定的会话 ID。

同源HTTP请求总会包含cookie,但跨源请求默认不包含cookie。

在客户端启用凭证选项来发送:

  • 曲奇饼
  • 基本身份验证
  • 客户端 SSL 证书

让我们在服务器上设置一个 Cookie,并在客户端读取它。在本例中,我们将在帖子的 GET 请求中执行此操作。Cookie 的响应头将是一个简单的键/值,可以从任何路径读取。



Set-Cookie: username=alice; Path=/


Enter fullscreen mode Exit fullscreen mode

文件server.js



const express = require('express')
const app = express()
const port = 8000

const isPreflight = (req) => {
  return (
    req.method === 'OPTIONS' &&
    req.headers['origin'] &&
    req.headers['access-control-request-method']
  )
}

app.use(express.static(__dirname))
app.use((req, res, next) => {
  res.set('Access-Control-Allow-Origin', '*') 

  if (isPreflight(req)) {
    res.set('Access-Control-Allow-Methods', 'DELETE')
    res.set('Access-Control-Allow-Headers', 'My-Custom-Header')
    res.set('Access-Control-Max-Age', '120')
    res.status(204).end()
    return
  } else {
    res.set('Access-Control-Expose-Headers', 'Timezone-Offset')
    res.set('Timezone-Offset', '240')
  }

  next()
})
app.get('/api/posts', (req, res) => {
  res.set('Set-Cookie', 'username=alice; Path=/') // Add this!
  res.json([
    {id: 1, content: 'foo'},
    {id: 1, content: 'bar'},
  ])
})
app.delete('/api/posts', (req, res) => {
  res.json({success: true})
})
app.listen(port, () => {
  console.log(`listening on port ${port}`)
})


Enter fullscreen mode Exit fullscreen mode

文件index.html



<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
  <script>
    (async () => {
      const res = await fetch('http://localhost:8000/api/posts')

      console.log(document.cookie)
    })()
  </script>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

重启服务器:



$ node server.js
listening on port 8000


Enter fullscreen mode Exit fullscreen mode


$ node server2.js
listening on port 9000


Enter fullscreen mode Exit fullscreen mode

现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么

没有读取cookies

什么都没记录!这是因为跨域请求默认允许读取 Cookie。我们可以通过credentials: 'include'在 fetch 请求选项中设置来告诉服务器我们希望读取 Cookie。

在 JavaScript fetch 调用中,您需要设置credentials: 'include'为在跨域请求时发送 Cookie。如果使用XMLHttpRequest,则需要设置withCredentials: true

文件index.html



<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
  <script>
    (async () => {
      const res = await fetch('http://localhost:8000/api/posts', {
        credentials: 'include'
      })

      console.log(document.cookie)
    })()
  </script>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么

Cookies读取通配符错误

我们得到错误:



Access to fetch at 'http://localhost:8000/api/posts' from origin 'http://localhost:9000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.


Enter fullscreen mode Exit fullscreen mode

这意味着为了让客户端读取 cookie,Access-Control-Allow-Origin不能使用通配符*,并且必须明确设置为允许读取 cookie 的来源。

很简单,我们可以动态地将允许的来源设置为请求标头中的来源。在实际应用中,为了更安全,最好将允许读取 Cookie 的来源设置为白名单。

文件server.js



app.use((req, res, next) => {
  // res.set('Access-Control-Allow-Origin', '*') // remove this!
  res.set('Access-Control-Allow-Origin', req.get('origin')) // Add this!

  if (isPreflight(req)) {
    res.set('Access-Control-Allow-Methods', 'DELETE')
    res.set('Access-Control-Allow-Headers', 'My-Custom-Header')
    res.set('Access-Control-Max-Age', '120')
    res.status(204).end()
    return
  } else {
    res.set('Access-Control-Expose-Headers', 'Timezone-Offset')
    res.set('Timezone-Offset', '240')
  }

  next()
})


Enter fullscreen mode Exit fullscreen mode

重启服务器:



$ node server.js
listening on port 8000


Enter fullscreen mode Exit fullscreen mode


$ node server2.js
listening on port 9000


Enter fullscreen mode Exit fullscreen mode

现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么

Cookies凭证标头错误

我们得到以下错误:



Access to fetch at 'http://localhost:8000/api/posts' from origin 'http://localhost:9000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.


Enter fullscreen mode Exit fullscreen mode

这是因为浏览器需要知道服务器允许从跨源请求中读取 cookie。

为了启用 cookie 支持,服务器必须Access-Control-Allow-Credentials设置标头,true以表明允许客户端读取 cookie 以及服务器允许接收 cookie。

标题唯一Access-Control-Allow-Credentials可以具有的值是true



Access-Control-Allow-Credentials: true


Enter fullscreen mode Exit fullscreen mode

文件server.js



app.use((req, res, next) => {
  res.set('Access-Control-Allow-Origin', req.get('origin'))

  if (isPreflight(req)) {
    res.set('Access-Control-Allow-Methods', 'DELETE')
    res.set('Access-Control-Allow-Headers', 'My-Custom-Header')
    res.set('Access-Control-Max-Age', '120')
    res.status(204).end()
    return
  } else {
    res.set('Access-Control-Expose-Headers', 'Timezone-Offset')
    res.set('Access-Control-Allow-Credentials', 'true') // Add this!
    res.set('Timezone-Offset', '240')
  }

  next()
})


Enter fullscreen mode Exit fullscreen mode

重启服务器:



$ node server.js
listening on port 8000


Enter fullscreen mode Exit fullscreen mode


$ node server2.js
listening on port 9000


Enter fullscreen mode Exit fullscreen mode

现在让我们看看当我们访问第二台服务器的网页http://localhost:9000/ 时会发生什么

客户端读取的 Cookie

客户端现在可以读取 cookie!

我们可以看到Access-Control-Allow-Credentials设置为trueAccess-Control-Allow-Origin设置为实际来源而不是通配符,这两个标头都是客户端允许使用 cookie 所必需的。

凭证包括标题

Access-Control-Allow-Credentials头可以出现在预检请求和实际请求中,但 Cookie 只会在实际请求中发送。只有非预检响应才需要标头。

TLDR;

设置响应标头Access-Control-Allow-Credentials: true以允许客户端读取 Cookie 等敏感信息。使用credentials: 'include' if using the fetch API or usewithCredentials: true if using the XMLHttpRequest API.Access-Control-Allow-Origin 请求凭据时,不能使用通配符。

CSRF

CSRF 令牌是客户端和服务器之间共享的不可猜测的令牌,用于防御跨站请求伪造(CSRF) 攻击。服务器将页面以令牌作为标头提供给客户端,客户端在每次请求时都发送 CSRF 令牌。如果服务器无法验证令牌,则请求无效。当客户端使用 Cookie 请求受保护的数据时,需要使用拒绝的 CSRF 保护。

使用 cURL 进行欺骗Origin是可能的并且很容易,但使用 cookie 进行欺骗则比较困难,因为 cURL 无法直接或远程访问浏览器 cookie。

在可能的情况下,考虑同源请求而不是使用 CSRF,因为它更安全。

如果构建公共 API,请使用 cookie 以外的其他方式来验证用户,例如使用 Oauth2。

浏览器支持

CORS 完全支持:

  • Chrome 3+
  • Firefox 3.5+
  • Safari 4+
  • IE 10+
  • Opera 12+
  • iOS 3.2+
  • Android 2.1+

结论

我们介绍了 CORS 的各种标头,包括:

添加 CORS 支持时需要问的问题:

  • 为什么服务器需要支持跨域请求?
  • CORS 是否被添加到新服务或现有服务中?
  • 哪些客户端应该有权访问该网站?
  • 用户将使用哪些浏览器和设备访问该网站?
  • 服务器支持哪些 HTTP 方法和标头?
  • API 是否应该支持用户特定数据?
  • 是否使用 cookies 来验证用户身份?

TLDR;

仅在绝对必要时才允许跨域请求,并且明确说明允许哪些来源、方法、标头和凭据。

资源:

文章来源:https://dev.to/miguelmota/understanding-cross-origin-resource-sharing-cors-2i3e
PREV
15 个初级 JavaScript 项目可提升您的前端技能!
NEXT
餐厅里的 Unix 程序员如何在菜单中寻找自己喜欢的菜品