theme: juejin
highlight: monokai-sublime
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
文章中有翻译不准确,词不达意的地方还请各位指正~,谢谢。
原文地址:Cache your CORS, for performance & profit
译者:xingba
校对者:xingba
CORS
对于很多 API
来说是有必要的,但在基本的配置下会创建大量额外的请求,减慢所有浏览器 API
客户端并且发送不必要的请求到你的后端。
这对传统 API
是一个问题,对使用无服务器平台来说是更大的问题,因为你的账单直接和收到的请求数量挂钩,所以这很容易将你的API花费翻倍。
以上这些问题都是没有必要出现的:这种情况的发生是因为你不知道 CORS
请求的缓存是如何工作的。让我们来解决这个问题。
CORS 预请求是什么?
当你在浏览器发起了一个不是 简单请求 的跨源请求(例如 example.com
到 api.example.com
),浏览器会先发送一个预请求,等待得到成功响应后才发送真正的请求。
这个预请求是一个对服务器的 OPTIONS
请求,首先会描述浏览器发送的这个请求,询问是否有权限访问。看起来类似这样:
OPTIONS /v1/documents
Host: https://api.example.com
Origin: https://example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: origin, x-requested-with
服务器必须响应消息头,确认它愿意接受请求,浏览器将等待服务器响应后再发送真正的请求。
如果你想要真实体验这些 CORS
规则是怎么工作的,你可以在练习场 Will it CORS? 来测试各种可能性。
实践中,几乎所有跨域接口请求都会需要这些预请求,特别是:
- 任何带有
JSON
或者XML
请求体的请求 - 任何包括
credentials
的请求 - 任何不适
GET
,POST
,或者HEAD
的请求 - 任何流请求或响应主体的交换
- 使用除
Accept
,Accept-Language
,Content-Language
和Content-Type
以外的任何请求头
为什么会这么糟糕
每一个这种请求将阻塞真正的请求至少一个来回到服务器的时间。OPTIONS
请求默认是不可缓存的,所以您的 CDN
通常不会处理它们,这将每次都增加服务器的负担。
OPTIONS
请求在客户端中默认只有5秒钟缓存时间。如果一个网页轮询了你的API,每10秒发送一次请求,那么每10秒也会重复预请求。
在许多情况下,这实际上使所有浏览器客户端的 API 延迟加倍。从用户端的视角看,你的性能减半了!并且你应该听过很多次了,几百毫秒的延迟在用户满意度和会话速度上表现的差异是巨大的。这相当糟糕。
另外,这也能增加相当多额外的负载和成本到你的API服务器。
这尤其适用于无服务器付费模型。包括 AWS Lambda
, Netlify Functions
, Cloudflare Workers
和 Google Cloud Functions
,这些平台的计费基于函数调用的次数,这些预请求像其他请求一样会被计算在内。无服务器模型在你的系统是很小的时候是免费的,但一旦大的生产系统投入使用,将会变得很贵,实际上会将你的成本翻倍,这是一个很大的打击。
即使没有无服务器,这也会让你陷入困境。如果你期望你的大部分 API
请求用 CDN
来处理,当你为浏览器添加了一个自定义请求头,就会为每一个客户端请求创建一个额外的请求到达你的后端服务器。这是非常令人惊讶的。
怎样缓存预请求响应信息。
有两个缓存步骤你需要落实到位:
- 缓存在浏览器,以至于个别客户端不会重复发送相同的预请求。
- 可能的话,缓存在
CDN
层,将这些缓存视为常量响应,这样你的后台服务器/函数不必处理他们了。
浏览器端的 CORS
缓存
将CORS响应缓存在浏览器,可以在你的预请求返回信息设置这个返回头:
Access-Control-Max-Age: 86400
这是以秒为单位的缓存时间
浏览器限制:该值的上限在火狐中是 86400
(24小时),而其他基于Chromium内核的浏览器上限为 7200
(2小时)。让预请求每两个小时执行一次而不是在每次请求之前执行一次,这在用户体验过程中是一个很大的提升,并且将值设置为更高也能确保在可能的情况下保持更长的生命周期,这是很容易做到的。
CDN
的 CORS
缓存
为了在浏览器和你的 API
服务器之间以CDN
和其他代理缓存响应,请添加:
Cache-Control: public, max-age=86400
Vary: origin
上面会将响应缓存在公共缓存里 24
小时(例如 CDN
),这对于大多数情况来说应该足够了,而不会使缓存失效成为一个问题。一开始测试,你可能想要将缓存的时间设置的短一些,并且在一切设置正确后再增加。
值得注意的是,这并不是标准的(OPTIONS
默认情况下是不可缓存的) ,但是它似乎得到了大多数 CDN
的广泛支持,他们会很高兴地缓存像这样显式地选择加入的 OPTION
响应。有些可能需要手动启用,所以请在配置中测试这一点。
最坏的情况下,如果你的 CDN
不支持这么做,那么这个操作将被忽略,所以没有真正的缺点。
Vary
头在这里是很重要的:告诉缓存给有相同Origin
头(来自同一跨域源的请求)的其他请求使用这个响应,此外还使用相同的 URL。
译者注:同一跨域源可以理解为
a.baidu.com
和b.baidu.com
。
如果不设置 Vary
头,将会有问题。预请求响应包含的Access-Control-Allow-Origin
头会匹配传输过来的 Origin
值。如果缓存了没有设置 Vary
头的响应,那么给当前origin的请求缓存的响应将会被用于来自其他origin的请求,而这也会导致跨域报错并且完全阻塞请求。
译者注:在同一个浏览器下,先打开了
aa.xingba.com
上的一个页面,访问了我们的资源,这个资源被浏览器缓存了下来,和资源内容一起缓存的还有Access-Control-Allow-Origin: https://aa.xingba.com
响应头。这时又打开bb.xingba.com
上的一个页面,这个页面也要访问那个资源,这时它会读取本地缓存,读到的Access-Control-Allow-Origin
头是缓存下的https://aa.xingba.com
而不是自己想要的https://bb.xingba.com
,这时就报跨域错误了,虽然它应该是能访问到这份资源的。
如果你正在使用其他依赖于请求的CORS响应头,你应该也要包含下面的,比如:
Access-Control-Allow-Headers: my-custom-header
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Vary: Access-Control-Request-Headers, Access-Control-Request-Method
如果你想现在测试这些东西,安装 HTTP Toolkit,添加一个匹配请求的规则,启动被拦截的浏览器,然后就可以手动注入这些头到API响应中来查看浏览器怎样处理他们的。
配置示例
如何在自己的案例中配置这些?下面有一些有用的现成例子。每个例子中,假定你已经设置好了预请求的 CORS处理,所以我们只需要考虑在此基础上如何添加缓存。
用 AWS Lambda
缓存 CORS
要使用 AWS Lambda
启用 CORS
,你可以在 HTTP响应中手动返回上面的头信息,也可以 配置 API 网关来处理 CORS
。
如果使用的是 API 网关的配置,它允许你配置 Access-Control-Max-Age
头,但默认不会设置 Cache-Control
,所以如果你用的是 CloudFront
或者其他的 CDN
,你需要手动配置这些,包括 Vary
也是。
或者,你可以自己在预请求 lambda
处理方法中控制这一切,像这样:
exports.handler = async (event) => {
const response = {
statusCode: 200,
headers: {
// Keep your existing CORS headers:
"Access-Control-Allow-Origin": event.headers['origin'],
// ...
// And add these:
"Access-Control-Max-Age": 86400,
"Cache-Control": "public, max-age=86400",
"Vary": "origin"
}
};
return response;
};
CloudFront 还特别包含了 单独配置 用来为 OPTIONS
响应启用缓存,所以如果你在这里用了 Cache-Control
应该确保启用了这个配置。
如果你使用的是 无服务器框架,你可以在 serverless.yml
手动进行配置,比如:
functions:
hello:
handler: handler.hello
events:
- http:
path: hello
method: get
cors:
origin: '*'
maxAge: 86400
cacheControl: 'public, max-age=86400'
在 Node.js
中缓存 CORS
如果你正在使用 Express
,Connect
,或者基于他们的扩展框架,那么你可能用的是 cors 模块来处理 CORS
。
默认情况下,cors
模块不会启用任何形式的缓存,但可以通过传入一个 maxAge
值来配置 Access-Control-Max-Age
。
由于不能简单的配置 Cache-Control
,所以如果现在使用的是 CDN
,你可能需要做一些稍微复杂一点的事情:
app.use(cors({
// Set the browser cache time for preflight responses
maxAge: 86400,
preflightContinue: true // Allow us to manually add to preflights
}));
// Add cache-control to preflight responses in a separate middleware:
app.use((req, res, next) => {
if (req.method === 'OPTIONS') {
res.setHeader('Cache-Control', 'public, max-age=86400');
// No Vary required: cors sets it already set automatically
res.end();
} else {
next();
}
});
在 Python
中缓存 CORS
Django
的 django-cors-headers 模块包含了一个合理的默认值 86400 作为他的 Access-Control-Max-Age
值。
同时,Flask
的 Flask-Cors 模块默认不启用缓存,若要启动缓存,需要在已有的配置中传入 max_age=86400
作为一个选项。
这样的话,你可以保证浏览器可以合适的缓存这些响应。如果你也想使用 CDN
缓存,那么你需要手动配置 Cache-Control
。不幸的是,据我所知的是,这两个模块既不支持自定义配置也没有其他简单方法,所以如果 CDN
缓存对你来说很重要那么你可能需要手动处理预请求了,或者自己封装这些模块。
在 Java Spring
中缓存 CORS
对于 Spring
,你可能已经在使用 @CrossOrigin
注解来处理 CORS 请求了。
Spring
默认会设置一个 30
分钟的 Access-Control-Max-Age
头,为每个单独的浏览器中添加时间上相对短一些的缓存,但不会设置 Cache-Control
头。
我建议在设置 maxAge
选项时增加最大时间到 24
小时(每个浏览器的最大值用的都是 86400
秒),如果你使用 CDN
缓存,还可以添加 Cache-Control
头。虽然 Spring
的内置 CORS
配置不支持自动设置后者,但是可以使用响应过滤器轻松的添加响应头。
@Component
public class AddPreflightCacheControlWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
exchange.getResponse()
.getHeaders()
.add("Cache-Control", "public, max-age=86400");
}
return chain.filter(exchange);
}
}
暂无评论内容