Nginx 完全开放 cors 带基本认证 无 if

Nginx fully open cors with basic authentication without if

这是时不时出现的,但毫无疑问正确地涵盖了这个用例。

http的相关部分:

    map $http_origin $origin_with_default {
        default '*';
        ~. $http_origin;
    }
    map $request_method $es_target {
        default '';
        POST 'search';
        GET 'search';
        HEAD 'search';
        OPTIONS 'options';
    }
    root         /app;

server的相关部分:

server {
  location ~* /(.*)/_search {
    limit_except OPTIONS {
      auth_basic "Read Users";
      auth_basic_user_file /etc/nginx/htpasswd_read;
    }
    rewrite ^ /internal/$es_target;
  }
  location /internal {
    return 405;
  }
  location /internal/search/ {
    internal;
    proxy_pass http://elasticsearch/;
    proxy_http_version 1.1;
    proxy_set_header Connection "Keep-Alive";
    proxy_set_header Proxy-Connection "Keep-Alive";

    proxy_hide_header Access-Control-Allow-Origin;
    proxy_hide_header Access-Control-Allow-Credentials;
    proxy_hide_header Access-Control-Allow-Headers;
    proxy_hide_header Access-Control-Allow-Credentials;

    include "cors.headers";
  }
  location /internal/options {
    internal;
    add_header 'Content-Type' 'text/plain; charset=utf-8';
    add_header 'Content-Length' 0;
    add_header 'Access-Control-Max-Age' 1728000;
    include "cors.headers";
    return 204;
  }
}

最后,cors.headers 文件:

add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Origin $origin_with_default always;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
  1. POST https://example.com/index_name/_search 给出 401。这符合预期。
  2. OPTIONS https://example.com/index_name/_search returns 选项 headers。这也符合预期。
  3. 然而,POST https://u:p@example.com/index_name/_search 给出了 404 并且服务器日志包含 open() "/app/index_name/_search" failed (2: No such file or directory),。在我添加 rewrite ^ /internal/$es_target;location /internal/search/ 部分之前,当 proxy_pass 刚好在 location ~* /(.*)/_search { 内的 limit_except 之后时,它确实起作用了。由于 1) 和 2),我相信重写和位置匹配是有效的。但为什么它尝试提供文件而不是执行代理传递?

这是一个有效的配置。它需要 limit_except 删除,每个位置都切换到正则表达式匹配消耗一切——最后一个位置是最后一个很重要,否则它会进入重写循环。重写的“如果 proxy_pass 指令指定了 URI,则当请求传递到服务器时,与位置匹配的规范化请求 URI 部分将替换为指令中指定的 URI”部分模块不是我能够开始工作的东西。

我们还需要 http 部分:

    map $http_origin $origin_with_default {
        default '*';
        ~. $http_origin;
    }
    map $request_method $es_target {
        default 'invalid';
        POST 'search';
        GET 'search';
        HEAD 'search';
        OPTIONS 'options';
    }

然后是server

server {
  location ~ /internal/search/(?<search>.*) {
    internal;
    auth_basic "Read Users";
    auth_basic_user_file /etc/nginx/htpasswd_read;
    proxy_pass http://elasticsearch/$search;
    proxy_http_version 1.1;
    proxy_set_header Connection "Keep-Alive";
    proxy_set_header Proxy-Connection "Keep-Alive";

    proxy_hide_header Access-Control-Allow-Origin;
    proxy_hide_header Access-Control-Allow-Credentials;
    proxy_hide_header Access-Control-Allow-Headers;
    proxy_hide_header Access-Control-Allow-Credentials;

    include "cors.headers";
  }
  location ~ /internal/options {
    internal;
    add_header 'Content-Type' 'text/plain; charset=utf-8';
    add_header 'Content-Length' 0;
    add_header 'Access-Control-Max-Age' 1728000;
    include "cors.headers";
    return 204;
  }
  location ~ /internal/invalid {
    return 405;
  }
  location ~* /_search$ {
    rewrite (.*) /internal/$es_target;
  }
}

以下答案需要对两个 nginx 基本概念有最低限度的了解——请求处理阶段(在 development guide 中描述)和位置内容处理程序(可以是本地的,当请求被服务时使用或不使用一些本地文件内容,当响应来自某种上游——HTTP 代理、FastCGI 或 uWSGI 守护进程等时)。

尽管我在配置 nginx 方面有相当丰富的经验,但 limit_except 并不是我过去经常使用的指令。为了了解它的行为,我做了几个测试。以下是我将要使用的 nginx 指令列表以及它们注册处理程序的请求处理阶段,按执行顺序排列:

  • rewrite - NGX_HTTP_REWRITE_PHASE
  • auth_basic - NGX_HTTP_ACCESS_PHASE
  • try_files - NGX_HTTP_PRECONTENT_PHASE
  • proxy_pass - NGX_HTTP_CONTENT_PHASE

在上述所有指令中,只有 auth_basicproxy_pass 允许在 limit_except 块内使用。 try_files "" <location> 我将要使用的技巧在 ServerFault 的 this 回答中有所描述,因此我将在此处跳过其详细描述。

TL;DR答案会在下一部分提供; limit_except 指令无法解决问题。


limit_except指令行为分析

我将使用以下配置来分析 limit_except 指令行为:

server {
    listen 8080;
    return 200 "upstream: request URI is \"$request_uri\", request method is $request_method";
}
server {
    listen 80;
    root /var/www/html;
    index index.html;
    ... locations will vary during the tests
}

/var/www/html 目录下,我将放置一个带有单个文本行的 index.html 文件 index.

我们开始了。

location / {
    limit_except GET {
        proxy_pass http://127.0.0.1:8080;
    }
}
> curl http://127.0.0.1/
index
> curl -X POST http://127.0.0.1/
upstream: request URI is "/", request method is POST

对于 GET 请求,nginx 使用本地内容处理程序。对于 POST 请求,nginx 使用 http_proxy_module 内容处理程序。

location / {
    limit_except GET {}
    proxy_pass http://127.0.0.1:8080;
}
> curl http://127.0.0.1/
upstream: request URI is "/", request method is GET
> curl -X POST http://127.0.0.1/
upstream: request URI is "/", request method is POST

这里 nginx 对两个请求都使用定义的 http_proxy_module 内容处理程序。我们还没有发现任何我们不能期待的东西。让我们更进一步。

location / {
    rewrite ^ /internal;
    limit_except GET {}
    proxy_pass http://127.0.0.1:8080;
}
location /internal {
    return 200 internal;
}
> curl http://127.0.0.1/
internal
> curl -X POST http://127.0.0.1/
upstream: request URI is "/", request method is POST

如果请求属于 limit_except 条件,则 rewrite 规则将被完全忽略。这看起来是我们没有预料到的。然而,快速搜索给我们 nginx trac ticket referring the following comment:

The problem that rewrite module directives (set, if) are not inherited into the limit_except block, and not executed there. ... This behaviour is basically identical to a nested location block.

现在让我们检查一下 try_files 指令的行为。为此,我们将添加以下 map

map $request_method $loc_name {
    POST    pst;
    default def;
}

和两个命名位置

location @def { return 200 def; }
location @pst { return 200 pst; }

我们的配置。

location / {
    try_files "" @$loc_name;
}
> curl http://127.0.0.1/
def
> curl -X POST http://127.0.0.1/
pst

无条件跳转到指定位置按预期工作。

location / {
    proxy_pass http://127.0.0.1:8080;
    try_files "" @$loc_name;
}
> curl http://127.0.0.1/
def
> curl -X POST http://127.0.0.1/
pst

这也是预期的,因为 NGX_HTTP_PRECONTENT_PHASE 其中 try_files 附加其处理程序在 NGX_HTTP_CONTENT_PHASE 之前执行。

location / {
    limit_except GET {}
    try_files "" @$loc_name;
}
> curl http://127.0.0.1/
def
> curl -X POST http://127.0.0.1/
(HTTP 405 Not Allowed)

看起来 nginx 试图对 POST 请求使用本地内容处理程序。

location / {
    limit_except GET {}
    proxy_pass http://127.0.0.1:8080;
    try_files "" @$loc_name;
}
> curl http://127.0.0.1/
def
> curl -X POST http://127.0.0.1/
upstream: request URI is "/", request method is POST

坏消息。如果请求属于 limit_except 条件,则在主要位置定义的 NGX_HTTP_PRECONTENT_PHASE 处理程序不会执行。这看起来类似于嵌套位置行为,尽管与嵌套位置相反,我们不能在 limit_except 块中使用 try_files 指令。

看起来 limit_except 指令有一些有限的用例。这是否意味着问题无法解决?不行。这意味着 limit_except 指令不能用来解决它,我们需要找到一些其他的方法。永不放弃:)


解决方案

您可以选择 enable/disable 使用我刚刚描述的技术 here 进行基本身份验证。将额外的 map 块添加到您的配置中:

map $es_target $realm {
    search    "Read Users";
    default   off;
}

现在您可以通过以下方式启用条件基本身份验证:

location ~ /_search$ {
    auth_basic $realm;
    auth_basic_user_file /etc/nginx/htpasswd_read;
}

但是您不能在此块中使用 rewrite 指令,因为该指令在 NGX_HTTP_REWRITE_PHASE 期间执行,并且 auth_basic 指令在稍后的 [=35] 注册其处理程序=].而对于常规 allow/deny 指令,有一种方法可以仅使用重写模块指令进行所有检查(一般示例是 here), there is no such a way for basic auth. Fortunately we still can use our try_files trick(将在稍后的 NGX_HTTP_PRECONTENT_PHASE). 如果碰巧你正在使用 OpenResty bundle 或 lua-nginx-module,你还有上述答案中描述的其他选项。

我看到您已经遇到了应该传递给上游的正确 URI 的问题。您的原始 proxy_pass http://elasticsearch/; 将针对每个请求传递 /,而 proxy_pass http://elasticsearch; 将传递重写的 URI。虽然您的原始请求 URI 始终可通过 $request_uri 变量获得(与 $uri 指令不同,rewrite 指令不会更改),并且 proxy_pass http://elasticsearch$request_uri; 之类的东西应该可以工作同样,我们将使用命名位置(我们不限于此,但这样我们应该完全防止任何 URI 更改)。这是整个解决方案(我稍微优化了您的第一个 map 块以防止(某种昂贵的)正则表达式库调用):

map $http_origin $origin_with_default {
    ''      '*';
    default $http_origin;
}
map $request_method $es_target {
    POST 'search';
    GET 'search';
    HEAD 'search';
    OPTIONS 'options';
    default 'wrong';
}
map $es_target $realm {
    search    "Read Users";
    default   off;
}
location ~ /_search$ {
    auth_basic $realm;
    auth_basic_user_file /etc/nginx/htpasswd_read;
    try_files "" @$es_target;
}
location @search {
    proxy_pass http://elasticsearch;
    proxy_http_version 1.1;
    proxy_set_header Connection "Keep-Alive";
    proxy_set_header Proxy-Connection "Keep-Alive";

    proxy_hide_header Access-Control-Allow-Origin;
    proxy_hide_header Access-Control-Allow-Credentials;
    proxy_hide_header Access-Control-Allow-Headers;
    proxy_hide_header Access-Control-Allow-Credentials;

    include "cors.headers";
}
location @options {
    add_header 'Content-Type' 'text/plain; charset=utf-8';
    add_header 'Content-Length' 0;
    add_header 'Access-Control-Max-Age' 1728000;
    include "cors.headers";
    return 204;
}
location @wrong {
    return 405;
}