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;
POST https://example.com/index_name/_search
给出 401。这符合预期。
OPTIONS https://example.com/index_name/_search
returns 选项 headers。这也符合预期。
- 然而,
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_basic
和 proxy_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;
}
这是时不时出现的,但毫无疑问正确地涵盖了这个用例。
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;
POST https://example.com/index_name/_search
给出 401。这符合预期。OPTIONS https://example.com/index_name/_search
returns 选项 headers。这也符合预期。- 然而,
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_basic
和 proxy_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 thelimit_except
block, and not executed there. ... This behaviour is basically identical to a nestedlocation
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;
}