配置 CORS
控制对服务器资源的访问
📣 默认情况下,Apollo Server 4 搭载了一项保护用户免受 CSRF 和 XS-Search 攻击的功能。此功能要求任何通过GET
或多部分上传请求必须包含一个特殊头(如Apollo-Require-Preflight
)防止跨站请求伪造 (CSRF).
跨源资源共享 (CORS)是一种基于 HTTP 头的协议,允许服务器指定哪些来源可以访问其资源。换句话说,您的服务器可以指定哪些网站可以告诉用户的浏览器与您的服务器进行通信,以及允许的确切 HTTP 请求类型。
“startStandaloneServer
”函数的CORS配置是不可修改的,并允许任何互联网上的网站指示用户的浏览器连接到您的服务器。根据您的使用情况,您可能需要进一步自定义CORS行为以确保服务器安全。为此,您首先需要切换到使用expressMiddleware
(或任何Apollo Server集成)。
⚠️ 如果您的应用仅在私人网络上可见并使用网络隔离进行安全,startStandaloneServer
's CORS行为是不安全的。有关更多信息,请参阅指定源。
默认情况下,运行在与服务器域名不同的域上的网站无法在其请求中传递Cookie。有关启用跨源Cookie传递以进行认证的详细信息,请参阅自定义CORS行为。
为什么使用CORS?
大多数开发者都知道CORS,因为它会遇到过于常见的CORS错误。CORS错误通常发生在您设置API调用或尝试使分别托管的服务器和客户端相互通信时。
当涉及到浏览网页时,我们经常导航到不同的网站,让我们的浏览器加载从这些网站加载的内容。这带有固有的风险。
作为Web开发者,我们不希望用户在访问其他网站时,浏览器对我们的服务器做任何事情来冒犯我们。浏览器安全机制(例如CORS或SOP)可以通过允许网站服务器指定哪些浏览器源可以请求该服务器上的资源来使开发者更安心。
一个网页内容的源包含该内容的域名、协议和端口。同源策略(SOP)是一种安全机制,限制来自同一源脚本的脚本与来自不同源的资源交互。这意味着网站上的脚本可以在不进行额外操作的情况下与同一源的资源交互。
如果两个URL在域名、协议或端口上有所不同,那么这些URL来自两个不同的源
# Same originhttp://example.com:8080/ <==> http://example.com:8080/# Different origin (difference in domain, protocol, and port)http://example.com:8080/ =X= https://example1.com:8081/
然而,众所周知,互联网是一个充满资源,可以帮助网站变得更好的激动人心的场所(导入图片,额外的字体,API 调用等)。开发者需要一种新的协议来放宽SOP并安全地共享不同源的资源。
跨源资源共享(CORS)是允许网页在不同源之间共享资源的机制。CORS通过允许服务器和客户端定义HTTP头,指定哪些外部客户端的脚本可以访问其资源,从而提供了额外的保护层。
请注意,SOP 和 CORS 都与 浏览器安全有关。两者均不能阻止 其他类型的软件从您的服务器请求数据。
为您的项目选择CORS选项
在考虑为您的应用程序配置CORS时,应考虑两个主要设置
- 哪些来源可以访问您的服务器资源
- 您的服务器是否接受用户凭据(即cookies)与请求
指定来源
CORS 使用 特定的HTTP响应头作为其协议的一部分,包括 Access-Control-Allow-Origin
。 Access-Control-Allow-Origin
头(ACAO)允许服务器指定哪些源可以使用脚本访问其资源。
根据您要构建的内容,当您准备部署应用程序时,在您的CORS配置中指定的源可能需要更改。
⚠️ 私有网络上的应用程序
如果您的浏览器在私有网络(即在公共互联网上)上运行您的API,并且它依赖于该网络的隐私来提供安全性, 我们强烈建议 指定哪些来源可以访问您的服务器资源。
特别是,startStandaloneServer
函数的 CORS 行为在此上下文中 不安全。相反,我们建议更换到其他 Apollo Server集成,通过指定来源来 自定义服务器当前的 CORS 行为。
如果不这样做,尽管您的个人电脑在您的私有网络上,任何网站上的脚本都可能导致浏览器与您的私有API通信。一些浏览器,如Chrome,正在开发本问题的功能。但在此期间,所有私有网络上的服务器都应该 始终在其CORS配置中指定来源。
联邦子图
如果您正在构建一个 联邦图,我们强烈建议您禁用所有生产 子图服务器 的 CORS 或限制子图的来源范围为像 Apollo Studio Explorer 这样的工具。大多数 子图 应仅允许来自您的网关的通信,并且这种通信不涉及浏览器。
更多信息,请参阅 保护您的子图。
需要cookie的API
如果您的API需要通过请求接受 跨域cookie,您必须在您的CORS配置中指定来源。否则,跨域cookie将被自动禁用。这 不是一个安全隐患,但这会阻止您的API成功提供cookie。
有关示例,请参阅 使用CORS传递凭据。
有已知的消费者API
如果您在公共互联网上创建一个API来为您的 自己的 网站或应用提供服务,您可能想 指定哪些来源可以访问您的服务器资源。明确指定来源可以提供额外的安全性。。
公共或嵌入式API
如果您创建了一个公开的API或用于嵌入您无法控制的网站的API,您可能希望允许 所有 来源访问您服务器资源。在这些情况下,您可以使用 startStandaloneServer
的 ACAB值(通配符 *
)允许来自任何来源的请求。
使用ACAO头部的通配符(*
)值允许 任何 网站告诉用户的浏览器向您的服务器发送任意请求(不带cookie或其他凭证)并读取该服务器的响应。
不属于以上任何一种
如果您的应用程序不属于上述任何一种类别,startStandaloneServerstartStandaloneServer
's CORS行为应符合您的用例。您始终可以选择稍后更换为其他Apollo Server集成以 自定义您的CORS配置。
配置Apollo Server的CORS选项
📣 Apollo Server 4 新特性: 如果您正在使用 Apollo Server 集成(例如,expressMiddleware
),则您需要负责设置您的 Web 框架的 CORS(跨源资源共享)。
Apollo Server's 独立服务器(即,startStandaloneServer
)使用通配符值(*
)服务 Access-Control-Allow-Origin
HTTP 头。这允许来自任何来源的脚本发出请求,无cookies,到服务器并读取其响应。
如果需要将凭证传递到您的服务器(例如,通过cookies),则不能为您的来源使用通配符值(*
)。您必须提供特定的来源。有关更多详细信息,请参阅通过 CORS 传递凭证。
The startStandaloneServer
函数 不支持配置服务器 CORS 行为。如果您想自定义服务器 CORS 行为(例如,通过指定来源或传递cookies),切换到使用expressMiddleware
(或任何其他 Apollo Server 集成)。
以下,我们使用cors
包配置和自定义我们的expressMiddleware
函数的 CORS 行为:
import { ApolloServer } from '@apollo/server';import { expressMiddleware } from '@apollo/server/express4';import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';import express from 'express';import http from 'http';import { typeDefs, resolvers } from './schema';import cors from 'cors';const app = express();const httpServer = http.createServer(app);const server = new ApolloServer({typeDefs,resolvers,plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],});await server.start();app.use('/graphql',cors<cors.CorsRequest>({ origin: ['https://www.your-app.example', 'https://studio.apollographql.com'] }),express.json(),expressMiddleware(server),);await new Promise<void>((resolve) => httpServer.listen({ port: 4000 }, resolve));console.log(`🚀 Server ready at https://127.0.0.1:4000/graphql`);
import { ApolloServer } from '@apollo/server';import { expressMiddleware } from '@apollo/server/express4';import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';import express from 'express';import http from 'http';import { typeDefs, resolvers } from './schema';import cors from 'cors';const app = express();const httpServer = http.createServer(app);const server = new ApolloServer({typeDefs,resolvers,plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],});await server.start();app.use('/graphql',cors({ origin: ['https://www.your-app.example', 'https://studio.apollographql.com'] }),express.json(),expressMiddleware(server),);await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve));console.log(`🚀 Server ready at https://127.0.0.1:4000/graphql`);
使用没有参数的 cors
函数将您服务器的 Access-Control-Allow-Origin
HTTP头设置为通配符值 (*
),允许来自任何源的脚本进行请求。因此,您的服务器会有与 startStandaloneServer
相同的 CORS 行为。
直接使用 cors
包,我们可以使用 origin
选项来配置 Access-Control-Allow-Origin
头。示例中启用了来自 https://www.your-app.example
和 https://studio.apollographql.com
的 CORS 请求。
如果您想将 Apollo Studio Explorer 作为一个 GraphQL 网页集成开发环境 (IDE),应在有效源列表中包含 https://studio.apollographql.com
。但是,如果您打算嵌入 Explorer 或使用 Apollo Sandbox,则 无需 在您的 CORS 源中指定 Studio 的 URL,因为请求将通过嵌入 Studio 的页面进行。
请注意,源 不包括 URL 路径,这意味着具有不同路径的两个 URL 仍然具有相同的源。因此,当指定源时,不要包含任何路径或尾部斜杠(例如,使用 http://example.com
,而不是 http://example.com/
)。
如果您使用的是 Apollo Server 的其他集成,您可以使用框架的标准功能添加和配置您的服务器的 CORS。
您还可以选择完全不使用 CORS 中间件以禁用跨源请求。这 建议用于联邦图中的子图。
使用 CORS 传递凭据
⚠️ 《startStandaloneServer》方法的CORS行为是不可更改的。要使用cookie传递凭据,您必须首先切换到另一种Apollo Server集成。
如果您的服务器需要请求包含用户的凭据(例如,cookie),您需要修改CORS配置,告诉浏览器这些凭据是被允许的。
您可以通过设置HTTP头
您必须指定一个源来启用认证请求。如果您的服务器将星号值*
分配给Access-Control-Allow-Origin
HTTP头,则浏览器将拒绝发送凭据。
要使浏览器可以使用expressMiddleware
和cors
npm包传递凭据,您可以指定origins
并将
app.use('/graphql',cors<cors.CorsRequest>({origin: yourOrigin,credentials: true,}),express.json(),expressMiddleware(server),);
app.use('/graphql',cors({origin: yourOrigin,credentials: true,}),express.json(),expressMiddleware(server),);
“origin”选项还接受布尔值,这意味着在技术上您可以配置CORS以允许所有带有凭据的跨源请求(即{origin: true, credentials: true}
)。这几乎肯定是不安全的,因为任何网站都可以读取由用户的cookie保护的信息。相反,您应该在启用凭据时指定CORS配置中的源。
有关从Apollo客户端发送cookie和授权头的示例,请参阅认证。有关向Apollo Server添加认证逻辑的更多指导,请参阅认证和授权。
防止跨站点请求伪造(CSRF)
默认情况下,Apollo Server 4带有防止CSRF和XS-Search攻击的功能。如果您想禁用这个推荐的特性,可以在ApolloServer
构造函数中传递csrfPrevention: false
。
服务器的CORS策略允许您控制哪些网站可以与您的服务器通信。在大多数情况下,浏览器通过发送一个预检请求来检查您的服务器CORS策略。发送实际操作之前。这是一个单独的HTTP请求。与大多数HTTP请求(使用GET
或POST
方法)不同,此请求使用一个名为OPTIONS
的方法。浏览器发送一个包含Origin
头以及以Access-Control-
开头的其他头部的请求。这些头信息描述了潜在不受信任的JavaScript想要进行的请求类型。您的服务器响应包含描述其策略的Access-Control-*
头信息,浏览器使用这个响应来决定是否可以发送真正的请求。处理OPTIONS
预检请求实际上从未执行GraphQL 操作。
然而,在某些情况下,浏览器不会发送该预检请求。如果请求被视为"简单",那么浏览器将会直接发送请求,而不会先发送预检请求。您的服务器的响应仍然可以包含Access-Control-*
头信息,如果这表明发送请求的来源不应该能够访问该网站,浏览器将会将您的服务器响应隐藏在问题JavaScript代码中。
不幸的是,这意味着您的服务器可能会执行来自不应允许与您的服务器通信的网站的“简单”请求发送的GraphQL操作。而且这些请求甚至可以包含cookie!尽管浏览器会隐藏您的服务器响应数据,但这可能并不足够。如果执行操作有副作用,那么攻击者可能不会关心是否能够读取响应,只要他们可以借助不知情用户浏览器的(以及cookie!)触发那些副作用。即使在只读的查询中,恶意代码也可能能够根据查询执行所需的时间来推断有关响应的一些信息。
利用简单请求的副作用进行的攻击被称为 "跨站请求伪造"攻击,或简称为CSRF。测量简单请求时长的攻击称为"跨站搜索"攻击,或简称XS-Search。
为了避免CSRF和XS-Search攻击, GraphQL服务器 应拒绝执行来自未能“预查”该操作的浏览器的任何操作。检测请求是否来自浏览器没有可靠的方法,因此GraphQL服务器不应在“简单请求”中执行任何操作。
判断一个请求是否是 "简单"的关键在于它是否尝试设置任意的HTTP请求头部。任何将Content-Type
头部设置为application/json
(或任何非三个特定值之一的)的请求都不是简单请求,因此它必须进行预查。因为所有经Apollo Server认识的POST
请求都必须包含一个指定application/json
的Content-Type
头部,我们可以确信这些请求不是简单请求,并且如果它们来自浏览器,它们已经进行了预查。
然而,Apollo Server还处理GET
请求。 GET
请求不需要Content-Type
头部,所以它们可能是简单请求。那么,我们如何确保只执行GET
请求,而不是简单的请求?如果我们要求请求包含浏览器永远不会自动设置的HTTP头部,那么这足以证明:设置了与规范中定义的头数目以外的HTTP头部的请求必须进行预查。
默认情况下,Apollo Server 4启用了CSRF防护功能。这意味着只有满足以下条件之一时,您的服务器才会执行GraphQL 操作:
- 进来的请求包含一个
Content-Type
头部,该头部指定了除了text/plain
,application/x-www-form-urlencoded
或multipart/form-data
以外的类型。值得注意的是,Content-Type
为application/json
(包括任何后缀如application/json; charset=utf-8
)就足够了。这意味着所有POST
请求(必须使用Content-Type: application/json
)都将执行。此外,所有支持GET
请求的 Apollo 客户端 Web 的所有版本都会包括Content-Type: application/json
头部,因此任何来自 Apollo 客户端 Web(POST
或GET
)的请求都将执行。 - 存在一个非空的
X-Apollo-Operation-Name
头部。该头部与所有 operations(POST
或GET
)一起由 Apollo iOS(v0.13.0+)和 Apollo Kotlin(所有版本,包括其以前的名称“Apollo Android”)一起发送,因此任何来自 Apollo iOS 或 Apollo Kotlin 的请求都将执行。 - 存在一个非空的
Apollo-Require-Preflight
头部。
注意,所有 HTTP 头部的名称都不区分大小写。
CSRF 防止仅适用于将执行 GraphQL 运作的请求,不适用于将加载 页面 的请求。
不满足上述任何条件的 HTTP 请求将被拒绝,并返回一个 400 状态码和一个明确解释需要添加哪些头才能使请求成功的消息。
此功能不会对您 graphql 的合法使用产生影响,除非以下两种情况:
- 您有发送
GET
请求且不是 Apollo 客户端 Web、Apollo iOS 或 Apollo Kotlin 的客户端 - 您已使用 文件上传 功能,并与
graphql-upload
如果上述任一情况适用于您,您应该配置相关的客户端在所有请求中发送一个非空的 Apollo-Require-Preflight
头部。
例如,如果您使用的是 apollo-upload-client
包与 Apollo Client Web 一起,在 {headers: {'Apollo-Require-Preflight': 'true'}}
中传给 createUploadLink
.
关于文件上传的最佳实践更多信息,请参阅 我们的博客文章.