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? 测试各种可能性。

实际上,几乎所有跨源 API 请求都需要这些预检请求,特别是包括:

  • 任何带有 JSON 或 XML 正文的请求
  • 包括凭据的任何请求
  • 任何不是 GET、POST 或 HEAD 的请求
  • 流式传输请求或响应正文的任何​​交换
  • Accept使用、和以外的任何标Accept-LanguageContent-LanguageContent-Type

为什么这样不好?

这些请求中的每一个都会至少在到服务器的往返时间内阻止您的真实请求。默认情况下,OPTIONS 请求不可缓存,因此您的 CDN 通常不会处理它们,并且每次都必须访问您的服务器。

它们缓存在客户端中,但默认情况下仅缓存 5 秒。如果网页轮询您的 API,每 10 秒发出一次请求,它也会每 10 秒重复一次预检检查。

在许多情况下,这实际上会使所有浏览器客户端的 API 延迟加倍。从最终用户的角度来看,你的性能减半了!我相信您已经听过一百次了,几百毫秒的延迟会转化为转化率和用户满意度的巨大差异。这很糟糕。

此外,它还会为您的 API 服务器增加有意义的额外负载和成本。

这尤其适用于无服务器计费模型。包括 AWS Lambda、Netlify Functions、Cloudflare Workers 和 Google Cloud Functions 在内的平台都根据函数调用的数量计费,并且这些预检请求与其他请求一样计入其中。Serverless 在规模较小时可以是免费的,但一旦大型生产系统投入使用就会变得更加昂贵,并且可能使成本翻倍是一个巨大的打击!

即使没有无服务器,这仍然会让你陷入困境。如果您希望 CDN 处理大部分 API 请求,那么当向浏览器请求添加自定义标头时,可能会为每个客户端请求创建一个额外的请求,直达您的后端服务器。

如何缓存预检响应?

您应该为这些设置两个缓存步骤:

  • 在浏览器中缓存,因此各个客户端不会不必要地重复相同的预检请求。
  • 在您的 CDN 层中缓存,尽可能将这些视为持续响应,这样您的后端服务器/功能就不必处理它们。

浏览器的 CORS 缓存

要在浏览器中缓存 CORS 响应,只需将此标头添加到您的预检响应中:

Access-Control-Max-Age: 86400

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

浏览器对此进行限制:Firefox 将该值上限为 86400(24 小时),而所有基于 Chromium 的浏览器将其上限为 7200(2 小时)。不过,每 2 小时而不是在每次 API 请求之前发出一次此请求可以极大地改善用户体验,并且将值设置得更高以确保在可能的情况下应用更长的生命周期是一个轻松的胜利。

CDN 的 CORS 缓存

要在浏览器和 API 服务器之间的 CDN 和其他代理中缓存 CORS 响应,请添加:

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

这会将响应缓存在公共缓存(例如 CDN)中 24 小时,这对于大多数情况应该足够了,而不会冒缓存失效成为问题的风险。对于初始测试,您可能希望将缓存时间设置得更短,并在对所有设置正确感到满意后增加它。

重要的是要注意这不是标准的(默认情况下 OPTIONS 被定义为不可缓存)但它似乎得到大多数 CDN 的广泛支持,他们会很乐意缓存像这样明确选择加入的 OPTIONS 响应。有些可能需要手动启用,因此请在您的配置中进行测试。

在最坏的情况下,如果您的 CDN 不支持它,它将被忽略,因此没有真正的缺点。

这里的Vary标头很重要:这告诉缓存除了使用相同的 URL 之外,仅将此响应用于具有相同Origin标头的其他请求(来自相同跨域源的请求)。

如果你不设置Vary标题,你可能会遇到大问题。预检响应通常包含Access-Control-Allow-Origin与传入Origin值匹配的标头。如果您在未设置的情况下缓存响应,Vary则具有一个来源的响应可能会用于具有不同来源的请求,这将使 CORS 检查失败并完全阻止请求。

如果您使用其他依赖于请求的 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 Gateway来为您处理 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 框架,则可以在您的框架中自动执行此操作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。

默认情况下,这根本不会启用任何类型的缓存,但您可以Access-Control-Max-Age通过传递一个maxAge值来进行配置。

你不能轻易配置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

Djangodjango-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标头。

我建议您通过设置选项将最长期限增加到 24 小时(86400 秒,任何浏览器使用的最大值),如果您使用的是 CDN maxAge,还可以添加标头。Cache-ControlSpring 的内置 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 上直接与我们联系。

调试 API 并想检查、重写和模拟实时流量?立即试用HTTP 工具包。用于 Web、Android、服务器等的开源一键式 HTTP(S) 拦截和调试。

缓存您的 CORS,以提高性能和利润
标签: