【问题标题】:nginx config to enable CORS with origin matchingnginx 配置以启用具有源匹配的 CORS
【发布时间】:2019-06-16 05:10:09
【问题描述】:

我尝试将a very popular config 用于 nginx,它启用了 CORS 并支持使用正则表达式进行源匹配。

这是我的配置:

server {
    listen 80 default_server;
    root /var/www;

    location / {
        if ($http_origin ~ '^http://(www\.)?example.com$') {
            add_header Access-Control-Allow-Origin "$http_origin";
        }

        # Handling preflight requests
        if ($request_method = OPTIONS) {
            add_header Content-Type text/plain;
            add_header Content-Length 0;
            return 204;
        }
    }
}

但是,此配置必须使用两个条件:一个与源域名匹配,另一个用于捕获预检请求。因此,当第二个条件匹配时,第一个条件的标头不会添加到响应中。

根据If Is Evil官方文章,这是nginx的预期行为。

如果If Is Evil 那么如何在 nginx 中启用 CORS?或者也许有办法以某种方式克服这个限制?

【问题讨论】:

标签: nginx cors cross-domain preflight


【解决方案1】:

你可以尝试使用map而不是第一个if块:

map $http_origin $allow_origin {
    ~^http://(www\.)?example.com$ $http_origin;
}
map $http_origin $allow_methods {
    ~^http://(www\.)?example.com$ "OPTIONS, HEAD, GET";
}

server {
    listen 80 default_server;
    root /var/www;

    location / {
        add_header Access-Control-Allow-Origin $allow_origin;
        add_header Access-Control-Allow-Methods $allow_methods;

        # Handling preflight requests
        if ($request_method = OPTIONS) {
            add_header Content-Type text/plain;
            add_header Content-Length 0;
            return 204;
        }
    }
}

nginx 将拒绝添加空的 HTTP 标头,因此只有在请求中存在 Origin 标头并且匹配此正则表达式时才会添加它们。

【讨论】:

  • 一个有趣的想法,谢谢!有没有办法对正则表达式进行重复数据删除?
  • 我看到的唯一方法是使用带有第三个map 块的附加变量,例如map $http_origin $use_cors_headers { ... },然后在另外两个中检查这个变量值,但我不确定是否这将是任何性能改进。
  • 只有add_headers 内的if 对我和其他一些开发人员在这种情况下有效(参见stackoverflow.com/questions/29467671/…
【解决方案2】:

到目前为止,我发现的only solution 是一种使用变量来聚合多个条件,然后仅将其与单个 if 语句匹配的技巧,因此重复了一些指令:

server {
    listen 80 default_server;
    root /var/www;

    location / {
        set $cors '';
        set $cors_allowed_methods 'OPTIONS, HEAD, GET';

        if ($http_origin ~ '^https?://(www\.)?example.com$') {
            set $cors 'origin_matched';
        }

        # Preflight requests
        if ($request_method = OPTIONS) {
            set $cors '${cors} & preflight';
        }

        if ($cors = 'origin_matched') {
            add_header Access-Control-Allow-Origin $http_origin;
        }

        if ($cors = 'origin_matched & preflight') {
            add_header Access-Control-Allow-Origin $http_origin always;
            add_header Access-Control-Allow-Methods $cors_allowed_methods;
            add_header Content-Type text/plain;
            add_header Content-Length 0;
            return 204;
        }
    }
}

【讨论】:

    【解决方案3】:

    如果不深入了解您的 nginx 设置的详细信息,它无论如何都不会工作,因为您返回的 CORS 标头不正确...

    具体来说:

    • 对于预检 (OPTIONS) 请求,以下是唯一有意义的 CORS 响应标头:Access-Control-Allow Origin(必需)Access-Control-Allow Credentials(可选)、Access-Control-Allow-Methods、(必需)、Access-Control-Allow-Headers(必需)Access-Control-Max-Age,(可选)。任何其他都将被忽略。

    • 对于常规(非 OPTIONS)请求,以下是唯一有意义的 CORS 响应标头:Access-Control-Allow Origin(必需)Access-Control-Allow Credentials(可选)和 Access-Control-Expose-Headers(可选的)。任何其他都将被忽略。

    注意那些 required 飞行前请求的标头 - 目前您只传递其中两个...另外,请注意您不需要返回Access-Control-Allow-Methods 用于非 OPTIONS 请求 - 它不是“有效的”,因此将被忽略。

    就您的具体 nginx 问题而言,我认为 @Slava Fomin II 有最正确的答案...

    【讨论】:

    • 感谢您提及Access-Control-Allow-Methods 仅用于预检请求。我错过了。但是,如果我根本不关心请求标头,我应该在 Access-Control-Allow-Headers 中返回什么值?我主要提供静态内容。实际上,我是否需要为这种场景实现预检请求处理,或者简单的 CORS 请求对所有浏览器都足够了?
    • 好吧,提取规范(定义 CORS,位于 fetch.spec.whatwg.org/#http-cors-protocol)几年前已更改为允许 '*' 用于附加标头 - 特别是“注意: 对于Access-Control-Expose-HeadersAccess-Control-Allow-MethodsAccess-Control-Allow-Headers 响应标头,值* 算作没有凭据的请求的通配符。对于此类请求,无法单独匹配标头名称或方法* 。”但目前尚不清楚浏览器是否真的支持这一点。
    • 因此,如果不支持Access-Control-allow-Headers: *(而且似乎没有人知道它是否真的支持),那么最好的办法是提取Access-Control-Request-Headers 请求标头和“镜像”的值在Access-Control-Allow-Headers 响应标头中返回。
    • 这是有道理的,但是如果我想告诉浏览器——“给我发送尽可能少的标题”怎么办?那我应该在Access-Control-Allow-Headers 中输入什么?如果Access-Control-Allow-Headers headers 完全丢失,浏览器将如何处理响应?
    • 嗯,Access-Control-Request-Headers (ACRH) 请求标头是浏览器告诉服务器“我即将发出请求,这些是我要发送的标头作为要求”。服务器无法控制将在请求中发送哪些标头...您的Access-Control-Allow-Headers (ACAH) 响应标头需要(至少)包含任何不是“简单”标头的标头。如果您不从服务器发送 ACAH 响应标头,则请求将失败。
    【解决方案4】:

    更合规的解决方案涉及更多,但确实会删除重复的正则表达式以进行域匹配,并且可以放入 sn-ps。

    我在http { ... } 块内创建了文件/etc/nginx/snippets/cors-maps.conf,它必须是included。它包含如下规则:

    # always set value to append to Vary if Origin is set
    map $http_origin $cors_site_v
    {
        ~. 'Origin';
    }
    # set site-specific origin header if it matches our domain
    map $http_origin $cors_site_origin
    {
        '~^https://(?:[-a-z\d]+\.)+example\.com$' $http_origin;
    }
    # validate the options only if domain matched
    map '$request_method#$cors_site_origin#$http_access_control_request_method' $cors_site_options
    {
        # is an allowed method
        '~^OPTIONS#.+#(?:GET|HEAD|POST|OPTIONS)$' okay;
        # requested an unknown/disallowed method
        '~^OPTIONS#.' nope;
    }
    # set value of Access-Control-Allow-Origin only if domain matched
    map '$request_method#$cors_site_origin' $cors_site_acao
    {
        '~^(?:GET|HEAD|POST)#.' $cors_site_origin;
    }
    # set value of Access-Control-Allow-Credentials only if Origin was allowed
    map $cors_site_acao $cors_site_acac
    {
        ~. 'true';
    }
    

    然后/etc/nginx/snippets/cors-site.conf 可以是多个location { ... } 块内的included:

    # only using "if" safely with a "return" as explained in https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/
    # return early without access headers for invalid pre-flight, because origin matched domain
    if ($cors_site_options = nope)
    {
        add_header Vary $cors_site_v;
        return 204 '';
    }
    # return early with access headers for valid pre-flight
    if ($cors_site_options = okay)
    {
        add_header Access-Control-Allow-Origin $cors_site_origin;
        add_header Access-Control-Allow-Credentials $cors_site_acac;
        add_header Vary $cors_site_v;
        add_header Access-Control-Allow-Methods 'GET, HEAD, POST, OPTIONS';
        # probably overkill, gleaned from others' examples
        add_header Access-Control-Allow-Headers 'Accept, Accept-Language, Authorization, Cache-Control, Content-Language, Content-Type, Cookie, DNT, If-Modified-Since, Keep-Alive, Origin, User-Agent, X-Mx-ReqToken, X-Requested-With';
        add_header Access-Control-Max-Age 1728000;
        return 204 '';
    }
    # conditionally set headers on actual requests, without "if", because directive ignored when values are empty strings ("map" default)
    add_header Access-Control-Allow-Origin $cors_site_acao;
    add_header Access-Control-Allow-Credentials $cors_site_acac;
    add_header Vary $cors_site_v;
    

    要匹配的值中的# 并不特殊,它们只是用作分隔符以允许使用多个输入变量进行测试。可以将额外的域添加到 map$cors_site_origin,但需要进行一些调整以支持具有不同允许选项/标题的域。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-06-19
      • 2013-10-19
      • 1970-01-01
      • 2020-12-09
      • 2011-07-11
      • 2019-11-19
      • 2021-10-28
      • 2020-08-06
      相关资源
      最近更新 更多