[译] 为了性能以及带来的收益,缓存你的 CORS


theme: juejin
highlight: monokai-sublime

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

文章中有翻译不准确,词不达意的地方还请各位指正~,谢谢。

原文地址:Cache your CORS, for performance & profit

译者:xingba

校对者:xingba

CORS 对于很多 API 来说是有必要的,但在基本的配置下会创建大量额外的请求,减慢所有浏览器 API 客户端并且发送不必要的请求到你的后端。

这对传统 API 是一个问题,对使用无服务器平台来说是更大的问题,因为你的账单直接和收到的请求数量挂钩,所以这很容易将你的API花费翻倍。

以上这些问题都是没有必要出现的:这种情况的发生是因为你不知道 CORS 请求的缓存是如何工作的。让我们来解决这个问题。

CORS 预请求是什么?

当你在浏览器发起了一个不是 简单请求 的跨源请求(例如 example.comapi.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 的请求
  • 任何流请求或响应主体的交换
  • 使用除AcceptAccept-LanguageContent-Language 和 Content-Type以外的任何请求头

为什么会这么糟糕

每一个这种请求将阻塞真正的请求至少一个来回到服务器的时间。OPTIONS 请求默认是不可缓存的,所以您的 CDN 通常不会处理它们,这将每次都增加服务器的负担。

OPTIONS 请求在客户端中默认只有5秒钟缓存时间。如果一个网页轮询了你的API,每10秒发送一次请求,那么每10秒也会重复预请求。

在许多情况下,这实际上使所有浏览器客户端的 API 延迟加倍。从用户端的视角看,你的性能减半了!并且你应该听过很多次了,几百毫秒的延迟在用户满意度和会话速度上表现的差异是巨大的。这相当糟糕。

另外,这也能增加相当多额外的负载和成本到你的API服务器。

这尤其适用于无服务器付费模型。包括 AWS Lambda, Netlify Functions, Cloudflare WorkersGoogle Cloud Functions,这些平台的计费基于函数调用的次数,这些预请求像其他请求一样会被计算在内。无服务器模型在你的系统是很小的时候是免费的,但一旦大的生产系统投入使用,将会变得很贵,实际上会将你的成本翻倍,这是一个很大的打击。

即使没有无服务器,这也会让你陷入困境。如果你期望你的大部分 API 请求用 CDN 来处理,当你为浏览器添加了一个自定义请求头,就会为每一个客户端请求创建一个额外的请求到达你的后端服务器。这是非常令人惊讶的。

怎样缓存预请求响应信息。

有两个缓存步骤你需要落实到位:

  • 缓存在浏览器,以至于个别客户端不会重复发送相同的预请求。
  • 可能的话,缓存在 CDN 层,将这些缓存视为常量响应,这样你的后台服务器/函数不必处理他们了。

浏览器端的 CORS 缓存

将CORS响应缓存在浏览器,可以在你的预请求返回信息设置这个返回头:

Access-Control-Max-Age: 86400

这是以秒为单位的缓存时间

浏览器限制:该值的上限在火狐中是 86400(24小时),而其他基于Chromium内核的浏览器上限为 7200(2小时)。让预请求每两个小时执行一次而不是在每次请求之前执行一次,这在用户体验过程中是一个很大的提升,并且将值设置为更高也能确保在可能的情况下保持更长的生命周期,这是很容易做到的。

CDNCORS 缓存

为了在浏览器和你的 API 服务器之间以CDN 和其他代理缓存响应,请添加:

Cache-Control: public, max-age=86400
Vary: origin

上面会将响应缓存在公共缓存里 24 小时(例如 CDN),这对于大多数情况来说应该足够了,而不会使缓存失效成为一个问题。一开始测试,你可能想要将缓存的时间设置的短一些,并且在一切设置正确后再增加。

值得注意的是,这并不是标准的(OPTIONS 默认情况下是不可缓存的) ,但是它似乎得到了大多数 CDN 的广泛支持,他们会很高兴地缓存像这样显式地选择加入的 OPTION 响应。有些可能需要手动启用,所以请在配置中测试这一点。

最坏的情况下,如果你的 CDN 不支持这么做,那么这个操作将被忽略,所以没有真正的缺点。

Vary 头在这里是很重要的:告诉缓存给有相同Origin头(来自同一跨域源的请求)的其他请求使用这个响应,此外还使用相同的 URL。

译者注:同一跨域源可以理解为 a.baidu.comb.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

如果你正在使用 ExpressConnect,或者基于他们的扩展框架,那么你可能用的是 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);
    }
}

希望以上内容能帮你提升 CORS 性能并且减少 API流量。有任何问题随时联系我 Twitter 或者 直接联系

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容