让我们谈谈跨域资源共享(CORS)
从源“ http://localhost:3000”访问“ https://coolserver.com”的请求已被 CORS 策略阻止:请求的资源上不存在“Access-Control-Allow-Origin”标头。如果不透明的响应满足您的需求,请将请求的模式设置为“no-cors”,以便在禁用 CORS 的情况下获取资源。
每个 Web 开发者在其职业生涯中可能至少遇到过一次这种违反 CORS 策略(跨域资源共享)的错误信息。我第一次遇到这个问题是在编程训练营里,当时我正在为一个小组项目开发一个全栈应用程序。当时我们只是在构建一个客户端应用程序,用于从我们自己开发的服务器获取数据,当这个错误出现时,我们惊慌失措。
这个错误本身其实很有说明性。它基本上告诉你,客户端不在访问正在获取数据的“白名单”来源之列。在这篇博文中,我们将学习跨域资源共享的基础知识、三种场景以及常见的错误。
什么是跨域资源共享?
首先,我们来了解一下CORS是什么以及它为何如此重要。CORS是跨域资源共享 (Cross-Origin Resource Sharing)的缩写,它是一种网络安全机制,允许/阻止一个源访问来自不同源的资源。服务器可以控制 CORS,限制哪些用户有权访问资源、访问数据的方式(允许使用哪些 HTTP 方法)、是否包含 Cookie 信息等等。
客户端应用程序通常很容易受到恶意用户的网络攻击。想想看,用户可以轻松打开浏览器开发工具来检查 DOM 的结构、它正在与哪个服务器通信以及资源的来源,而无需太多限制。CORS并非完美的安全措施,但它至少可以保证我们从其他来源获取的资源是安全的。
同源策略与跨源资源共享
有两种策略可以帮助浏览器保护用户免受通过动态加载代码发起的潜在网络攻击。它们是同源策略 (SOP) 和跨源资源共享。通常,禁止从其他来源读取数据。SOP 允许浏览器仅从同一来源请求资源。如果从其他来源请求资源,则会违反 SOP。例如,从 请求数据https://chuckchoi.me
通常https://dev.to
会违反 SOP,因为它们并非来自同一来源。
如果您无法从其他来源获取数据,这将违背 Web 的初衷和功能。值得庆幸的是,跨源资源共享 (CORS)允许 SOP 例外,并允许发出跨源请求。跨源请求主要涉及三种请求,让我们深入探讨每种请求中可能出现的常见错误。
在我们开始之前...
我构建了一个简单的客户端 React 应用和一个 Express 服务器,以便我们直观地了解正在发生的事情。您可以测试三种不同的跨域请求,并根据服务器设置查看可能遇到的常见错误。您可以查看每个场景的服务器和请求结构,然后点击“发送请求”按钮查看会收到的响应。您还可以打开浏览器控制台,查看“网络”选项卡,了解网络行为。您可以随意使用侧边的应用来补充理解,并根据需要查看代码库!
简单请求
我们即将讨论的请求并没有官方术语,但 MDN 的 CORS 文档称之为“简单请求”。简单请求是一种跨域请求,它无需任何预检请求(我们接下来会讲到),直接发送到服务器。服务器会返回一个响应,该响应的头部包含 Access-Control-Allow-Origin 属性,然后浏览器会检查是否违反了 CORS 策略。
简单请求仅在满足特定条件时才被允许,而大多数现代 Web 开发都不允许这样做。以下是 MDN 中列出的条件:
- 允许的方法之一:
GET
HEAD
POST
- 除了用户代理自动设置的标头(例如,Connection、User-Agent 或 Fetch 规范中定义为“禁止的标头名称”的其他标头)之外,唯一允许手动设置的标头是 Fetch 规范定义为“CORS 安全列表请求标头”的标头,它们是:
Accept
Accept-Language
Content-Language
Content-Type
(但请注意以下附加要求)
- Content-Type 标头唯一允许的值是:
application/x-www-form-urlencoded
multipart/form-data
text/plain
- 如果使用对象发出请求,则请求中使用的属性
XMLHttpRequest
返回的对象上没有注册任何事件监听器;也就是说,对于给定的实例,没有调用任何代码来添加事件监听器来监视上传。XMLHttpRequest.upload
XMLHttpRequest
xhr
xhr.upload.addEventListener()
ReadableStream
请求中未使用任何对象。
哇,这需求列表真长。正如我们讨论过的,在现代 Web 开发中,满足上述所有需求的情况非常罕见,因此您可能大多数时候都在处理预检或凭证请求。但是,为了使简单请求能够正常工作而不违反 CORS 错误,响应标头需要包含Access-Control-Allow-Origin并列出请求的来源,或者使用星号(* 符号)作为通配符以允许所有来源。
简单请求练习——CORS 教程应用
- 错误 #1:没有 Access-Control-Allow-Origin 标头
让我们继续打开CORS-Tutorial App。在“简单请求”选项卡 -> “错误 1”选项卡下,服务器结构如下:
我们调用的 fetch 方法是fetch('https://cors-tutorial-server.herokuapp.com/api/simple/no-origin')
。默认情况下,如果未指定该方法,fetch()
则会GET
向作为参数传递的 URL 发出请求。由于该请求非常简单,因此它将其作为简单请求发送,因为它符合简单请求的要求。让我们继续点击按钮,看看如果我们向该路由发出一个简单的 fetch 请求会得到什么响应:
根据上面的错误信息,我们从应用源发出的请求https://chuckchoiboi.github.io/cors-tutorial/
由于违反了 CORS 策略而被阻止。错误信息显示“请求的资源上不存在‘Access-Control-Allow-Origin’标头”。
- 解决方案 1:通配符来源
遵守 CORS 策略的第一步是添加Access-Control-Allow-Origin
到响应头中。您可以指定来源,也可以使用星号作为通配符来允许所有来源。在服务器端,您可以像这样添加通配符来源:
继续尝试发送请求。你会看到服务器返回一个有效的响应,如下所示:
- 错误 #2 - 来源不匹配
允许所有来源可能并非最佳做法,而且也不安全。最好将来源“列入白名单”,指定您期望的来源。以下是指定来源的服务器示例(点击“简单请求”选项卡 -> “错误 2”选项卡):
不过,这条路由所期望的来源是。这次https://www.website.notcool
从这里发出获取请求时,会显示略有不同的错误消息:https://chuckchoiboi.github.io/cors-tutorial/
这次,错误显示服务器期望此路由的来源是https://www.website.notcool
。假设我们从 发出请求www.website.notcool
,但发出请求的协议是 ,http://
而不是https://
。由于来源同时包含协议和主机,因此会引发相同的错误。
- 解决方案 #2:匹配原点
话虽如此,服务器的响应头必须包含与请求来源匹配的来源。一个有效的简单请求可以发送到指定来源的服务器,如下所示(“简单请求”选项卡 -> “有效条件”选项卡):
预检请求
在现代 Web 应用程序中,你会更多地遇到预检请求,而不是简单的请求。在这种情况下,浏览器会在实际请求之前发出预检请求,以请求权限。如果浏览器通过预检请求批准了服务器的响应,则发出实际请求。如果预检请求未获批准,则不会发出实际请求。
在此预检过程中,预检请求使用 OPTIONS 方法。预检响应需要在标头中允许请求的来源,并且实际请求的方法也需要被允许。一旦满足这些条件,就会发出实际请求。
飞行前请求练习——CORS 教程应用
- 错误 #1:预检响应来源不匹配
看这个例子。该请求正在尝试DELETE
向服务器发出请求。由于该请求使用了DELETE
方法,因此它将使该请求成为预检请求,因此浏览器将首先使用OPTIONS
方法发送预检请求来检查其权限。但是,由于请求的来源和响应的Access-Control-Allow-Origin
值不匹配,因此该预检请求将失败,甚至不会进入实际请求。
- 错误 #2:未指定方法的预检响应
我们再试一次。这次尝试向带有预检响应的路由发送一个 DELETE 请求,该响应包含允许请求来源的标头,如下所示:
是不是感觉好像漏掉了什么?这里有一点剧透。由于服务器的预检响应没有指定 DELETE 方法,因此这次请求甚至不会转到实际的请求。以下是您将收到的错误响应:
- 错误 #3:预检通过,实际请求失败
现在,预检响应已允许匹配来源和DELETE
方法,这将发送实际DELETE
请求。不过,你注意到响应头有什么问题吗?
你答对了!正如错误所示,服务器只允许https://www.website.notcool
源地址访问。即使预检通过,如果实际请求失败,你仍然违反了 CORS 政策。
- 解决方案
为了发出有效的预检请求,服务器需要处理预检请求,并在响应标头中提供有效的来源和有效的方法,正如我们之前讨论的那样。预检请求通过后,才会发送实际请求。实际请求需要允许请求来源符合 CORS 策略。
凭证请求
最后但同样重要的是,跨域请求还有第三种场景可以增强安全性。发送XMLHttpRequest
或时fetch
,不应在不带任何选项的情况下包含浏览器 Cookie 或与身份验证相关的标头。发送带有凭据选项的请求将允许我们在跨域请求中发送 Cookie 等敏感信息。
您可以通过{"credentials": "include"}
在 JavaScript 中添加选项来发送带凭证的请求。这将为 CORS 策略条件添加一些严格的规则。当浏览器发送带凭证的请求时,响应的 Access-Control-Allow-Origin 不应使用通配符“*”。它需要指定请求的来源,并且服务器还需要设置额外的标头Access-Control-Allow-Credentials
以true
允许发出有效的带凭证的请求。
凭证请求练习——CORS 教程应用
- 错误 1:通配符来源
这次,我们使用 fetch() 方法发送一个 GET 请求,其中包含{"credentials":"include"}
一个选项。服务器的响应头使用了 Access-Control-Allow-Origin 的通配符。让我们继续,通过点击应用程序中的按钮来发送请求。
从错误消息中可以看出,credentialed request 不允许Access-Control-Allow-Origin
使用通配符。为了能够向服务器发出凭证请求,我们需要服务器的路由允许使用https://chuckchoiboi.github.io
。
- 错误 2:Access-Control-Allow-Credentialed
好的,这次我们已经在服务器中指定了请求来源https://chuckchoiboi.github.io
。不用多说,我们继续点击“发送请求”按钮吧!
相信我,这是你今天看到的最后一个错误。正如我们之前讨论过的,凭证请求会给 CORS 策略规则添加更严格的条件。这条错误信息提示,响应头需要包含一个额外的头Access-Control-Allow-Credentials
,其值设置为true
。
- 解决方案
总而言之,可以通过{"credentials":"include"}
在请求中添加响应头来创建凭证请求,该响应头指定请求来源(不允许使用通配符),并在响应头中也Access-Control-Allow-Credentials
设置为true
。成功的凭证请求应该如下所示:
结论
想到跨域资源共享,我就会想起封闭式社区的宾客名单/访问权限。我去过几个朋友的封闭式社区,那里的房主必须把名字告诉门口的保安,才能知道哪些人被邀请进门。
跨域资源共享的有趣之处在于,真正遇到 CORS 策略违规问题的是前端开发人员,而后端开发人员则可以在响应头中控制如何解决这些问题。解决 CORS 错误并不难,只需与后端开发人员沟通,确保所有 CORS 策略条件都满足,即可获取资源。
非常感谢您花时间阅读这篇博文!如果您想了解我构建的 React 应用或 Express 服务器,或者想就应用/博文提供反馈,欢迎在LinkedIn上给我留言!
文章来源:https://dev.to/chuckchoiboi/demystifying-cross-origin-resource-sharing-cors-4lpj