跳转至

cors:为 NGINX 提供正确的 CORS,包括预检和 Vary

需要 GetPageSpeed NGINX Extras 订阅的 Pro 计划(或更高版本)。

安装

您可以在任何基于 RHEL 的发行版中安装此模块,包括但不限于:

  • RedHat Enterprise Linux 7、8、9 和 10
  • CentOS 7、8、9
  • AlmaLinux 8、9
  • Rocky Linux 8、9
  • Amazon Linux 2 和 Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-cors
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-cors

通过在 /etc/nginx/nginx.conf 顶部添加以下内容来启用模块:

load_module modules/ngx_http_cors_module.so;

本文档描述了 nginx-module-cors v1.0.0 于 2026 年 8 月 15 日发布。


适用于 NGINX 的 CORS,处理 add_header 方案出错的部分。

为什么不直接使用 add_header

map $http_origin + add_header Access-Control-Allow-* 方案是每个人都在复制的,但它在四个方面存在问题:

  • add_header 是按层级继承或替换的。 一旦任何 location 添加了自己的单个响应头,则在其上方设置的每个 add_header —— 包括您的 CORS 响应头 —— 都会静默消失。没有任何警告。
  • 预检需要短路处理。 一个 OPTIONS 预检必须用 204 和正确的响应头来回答,而无需到达您的应用程序。手动执行此操作意味着使用 if 块,并且它与 try_filesproxy_pass 的交互不佳。
  • add_header 仅在一组白名单状态码上触发,除非您传递 always。因此,您的错误响应会丢失 CORS 响应头,浏览器会报告不透明的 CORS 失败,而不是实际发生的 404 或 502。
  • Vary: Origin 被遗忘。 当响应依赖于请求来源而您未声明时,您前面的任何共享缓存或 CDN 都会将一个来源的响应愉快地提供给另一个来源。这是一个缓存投毒错误,也是现实中最常见的错误。

此模块通过一个响应头过滤器加上一个 preaccess 阶段处理器,无需 if 块即可正确处理所有这四个方面。

概要

location /api/ {
    cors                on;
    cors_origin         https://app.example.com https://*.staging.example.com;
    cors_methods        GET HEAD POST PUT DELETE;
    cors_headers        Authorization Content-Type;
    cors_expose_headers X-Total-Count;
    cors_credentials    on;
    cors_max_age        86400;

    proxy_pass http://backend;
}

那么预检看起来像这样,并且永远不会到达 backend

$ curl -i -X OPTIONS https://api.example.com/api/things \
    -H 'Origin: https://app.example.com' \
    -H 'Access-Control-Request-Method: PUT'
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, HEAD, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Vary: Origin, Access-Control-Request-Method

指令

cors

语法: cors on | off;

默认值: cors off;

上下文: httpserverlocationif in location

启用 CORS 处理。每个其他指令都是独立继承的,因此设置其中一个指令的嵌套 location 不会丢失其余的指令——这就是与 add_header 的全部区别。

cors_origin

语法: cors_origin * | any | <spec> ...;

默认值: cors_origin *;

上下文: httpserverlocationif in location

哪些来源可以读取资源。两个保留字和三种规范形式:

含义
* 发出字面量 Access-Control-Allow-Origin: *。对所有人都一样,因此不添加 Vary: Origin。不能与 cors_credentials on 组合。
any 回显到达的任何 Origin。凭据安全,发出 Vary: Origin
https://app.example.com 精确匹配,不区分大小写。
https://*.example.com 通配符子域。
~^https://(a\|b)\.example\.com$ 正则表达式。~* 表示不区分大小写。

可以列出多个规范。当其中一个匹配时,该来源会被回显。 *any 不能与其他值混合。

通配符故意严格:* 必须紧跟 :// 之后,并且必须后跟 .https://*example.com 在启动时会被拒绝,而不是静默匹配 https://evilexample.com。通配符匹配任意数量的前导标签(https://a.b.example.com 匹配 https://*.example.com),但不匹配裸顶点域,也不忽略端口——带端口的来源需要精确条目或正则表达式。

Origin: null —— 沙盒 iframe、data: 文档或 file:// 页面发送的内容——仅在 cors_credentials on 时,才会被列表中的显式 null 条目匹配。在这种情况下,any 和正则表达式都不会匹配它。回显带有凭据的 null 将把响应的认证读取权限交给互联网上的每个沙盒框架。

cors_methods

语法: cors_methods * | <method> ...;

默认值: cors_methods GET HEAD POST OPTIONS;

上下文: httpserverlocationif in location

在预检时发送的 Access-Control-Allow-Methods 值。* 不能与 cors_credentials on 组合。

cors_headers

语法: cors_headers * | any | <name> ...;

默认值: —(省略响应头)

上下文: httpserverlocationif in location

在预检时发送的 Access-Control-Allow-Headers 值。any 将请求的 Access-Control-Request-Headers 原样回显;当请求未要求任何响应头时,该响应头被省略。* 不能与 cors_credentials on 组合。

cors_expose_headers

语法: cors_expose_headers <name> ...;

默认值: —(省略响应头)

上下文: httpserverlocationif in location

浏览器应使脚本可读的响应头,超出安全列表集。在实际响应上发送,而不是在预检上发送。

cors_credentials

语法: cors_credentials on | off;

默认值: cors_credentials off;

上下文: httpserverlocationif in location

发出 Access-Control-Allow-Credentials: true,允许跨域请求上的 Cookie 和 HTTP 认证。

CORS 规范禁止将凭据与通配符配对,并且每个浏览器都强制执行——因此同时执行两者的配置是一个 CORS 永远无法工作的配置。NGINX 拒绝启动,而不是让您发布它:

cors_credentials on;
cors_origin      *;      # nginx: [emerg] ... cannot be combined with "cors_origin *"

使用显式列表,或使用 cors_origin any 进行回显。请注意,any 加上凭据允许任何网站读取该位置的认证响应;这是允许的,并且会在启动时记录警告。

cors_max_age

语法: cors_max_age <time>;

默认值: —(省略响应头)

上下文: httpserverlocationif in location

浏览器可以缓存预检结果多长时间。cors_max_age 0; 是一个有意义的值并且会被发出;省略该指令则省略该响应头。

cors_preflight

语法: cors_preflight on | off;

默认值: cors_preflight on;

上下文: httpserverlocationif in location

是否在内部回答匹配的预检。当您的应用程序自己实现 OPTIONS 并且您只想添加响应头时,请将其关闭。

仅当请求确实是预检——一个同时携带 OriginAccess-Control-Request-MethodOPTIONS——并且来源匹配时,预检才会被短路。其他所有内容都会原封不动地通过,因此 WebDAV 和应用程序级别的 OPTIONS 继续工作,来自不允许来源的预检只是不会收到 Access-Control-Allow-Origin,这会使浏览器拒绝它。

因为处理器在 preaccess 阶段运行,预检会在 auth_basicauth_requestdeny 有机会拒绝它之前得到回答。这是故意的:浏览器从不在预检上发送凭据,因此其前面的任何认证都会完全破坏 CORS。对同一位置的实际请求仍然正常进行认证。

cors_vary

语法: cors_vary on | off;

默认值: cors_vary on;

上下文: httpserverlocationif in location

当响应依赖于请求来源时,是否发出 Vary: Origin

请保持开启。每当策略依赖于来源时,它都会被发出——包括当来源匹配时,以及当请求根本没有携带 Origin 时,因为该响应的缓存副本绝不能重放给一个其来源会产生不同响应头的请求。对于静态的 cors_origin *,它故意发出,因为这对每个人都是一样的,不会变化。

仅当您知道没有共享缓存位于此位置之前,或者您的 CDN 已经以 Origin 为键时,才将其关闭。

注意事项

  • 响应上已有的 Vary 会被扩展,而不会被替换:上游发送的 Vary: Accept-Language 会变成 Vary: Accept-Language, Origin,并且多个上游 Vary 行会被合并为一个。Vary: * 保持不变,因为根据 RFC 9110,它已经包含了所有内容。
  • 有两件事会在此模块之后添加它们的 Vary,因此会作为单独的字段行到达,而不是被合并:同一位置中的 add_header Vary ...,以及 gzip_vary onVary: Accept-Encoding。多个 Vary 字段行与一个组合行(RFC 9110 §5.3)的含义完全相同,并且每个缓存都能处理它,所以这是正确的——只是不够整洁。如果您希望将所有内容合并为一行,请加载 nginx-module-compression-vary
  • 不要同时在同一位置使用 add_header 设置 CORS 响应头。此模块会替换它找到的任何 Access-Control-Allow-Origin——两个这样的响应头在每个浏览器中都是硬性失败——但其他响应头最终会被重复。
  • 响应头在 304 和 206 响应以及错误上都会发出。
  • 子请求被跳过,与 add_header 的行为一致。

仅返回翻译后的 Markdown。