Если вы сталкивались с CORS, то знаете всю ту боль, которую испытывает разработчик, когда нужно сходить к API на другом домене. Если конфигурация сервера не доступна для настройки, то использовали какое-нибудь решение на основе не менее популярного решения cors-anywhere.
Пятница вечер делать нечего
Не многим изестно, что директива proxy_pass поддерживает не только локальные домены и потоки (aka upstream
), но и внешние источники, например:
proxy_pass https://api.github.com/$request_uri
Так зародилась идея написать универсальный (с некоторыми оговорками) конфиг для nginx, который поддерживает любой переданный домен.
Чем мы можем управлять
Мы можем объявлять новые переменные на основе глобальных c поддержой регулярных выражений с помощью map:
map $request_url $my_request_path {
~*/(.*)$ $1;
default "";
}
Так, при запросе к http://example.com/api
в переменной $my_request_path
будет лежать api
.
Мы можем отправлять клиенту дополнительные заголовки с помощью add_header:
add_header X-Request-Path $my_request_path always;
Теперь у нас добавился заголовок X-Request-Path
с значением api
.
С помощью директивы proxy_set_header добавлять заголовки к запросу, который отправляется proxy_pass
. А с помощью proxy_hide_header скрывать заголовки, которые мы получили от proxy_pass
.
С помощью директивы if обрабатывать выражения, например, при запросе методом OPTIONS
отдавать сразу нужный код ответа:
if ($request_method = OPTIONS) {
return 204;
}
Собираем все вместе
Для начала объявим $proxy_uri
который мы будем извлекать из $request_uri
:
map $request_uri $proxy_uri {
~*/http://(.*)/(.+)$ "http://$1/$2";
~*/https://(.*)/(.+)$ "https://$1/$2";
~*/http://(.*)$ "http://$1/";
~*/https://(.*)$ "https://$1/";
~*/(.*)/(.+)$ "https://$1/$2";
~*/(.*)$ "https://$1/";
default "";
}
Если коротко это работает так: при запросе http://example.com/example.ru
, в переменной $proxy_uri
будет лежать https://example.ru
Из полученного $proxy_uri
извлечем часть, которая будет соответствовать заголовку Origin:
map $proxy_uri $proxy_origin {
~*(.*)/.*$ $1;
default "";
}
Для заголовка Forwarded нам понадобится обработать сразу 2 переменные:
map $remote_addr $proxy_forwarded_addr {
~^[0-9.]+$ "for=$remote_addr";
~^[0-9A-Fa-f:.]+$ "for=\"[$remote_addr]\"";
default "for=unknown";
}
map $http_forwarded $proxy_add_forwarded {
"" "$proxy_forwarded_addr";
default "$http_forwarded, $proxy_forwarded_addr";
}
Обработка заголовока X-Forwarded-For уже встроена в nginx
Теперь мы можем перейти к объявлению нашего проксирующего сервера:
server {
listen 443 ssl;
server_name cors.example.com;
proxy_http_version 1.1;
proxy_pass_request_headers on;
proxy_pass_request_body on;
proxy_redirect off;
resolver 77.88.8.8 77.88.8.1 8.8.8.8 8.8.4.4 valid=1d;
location / {
if ($proxy_uri = "") {
# empty uri
return 403;
}
# add proxy cors headers
add_header Access-Control-Allow-Headers "*" always;
add_header Access-Control-Allow-Methods "*" always;
add_header Access-Control-Allow-Origin "*" always;
if ($request_method = OPTIONS) {
return 204;
}
proxy_set_header Host $proxy_host;
proxy_set_header Origin $proxy_origin;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Forwarded "$proxy_add_forwarded;proto=$scheme";
proxy_pass $proxy_uri;
}
}
Мы получили минимально рабочий проксирующий сервер, у которого обрабатывается CORS Preflight Request и добавляются соответствующие заголовки.
Делаем красиво
Все бы хорошо, но если у сервера, к которому мы проксируем, будет настроена обработка CORS, то его заголовки будут передаваться клиенту. Давайте скроем все возможные:
# hide original cors
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Request-Headers;
proxy_hide_header Access-Control-Request-Method;
Хорошо бы еще передавать IP клиента, чтобы хоть как-то обходить rate limit, который может возникнуть, если несколько пользователей будут обращаться к одному ресурсу:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Client-IP $remote_addr;
proxy_set_header CF-Connecting-IP $remote_addr;
proxy_set_header Fastly-Client-IP $remote_addr;
proxy_set_header True-Client-IP $remote_addr;
proxy_set_header X-Cluster-Client-IP $remote_addr;
Мы же не говорим про анонимность, верно?)
И, напоследок, немного улучшим производительность выключив кэш/буферизацию/etc:
sendfile on;
tcp_nodelay on;
tcp_nopush on;
etag off;
if_modified_since off;
proxy_buffering off;
proxy_cache off;
proxy_cache_convert_head off;
proxy_max_temp_file_size 0;
client_max_body_size 0;
proxy_read_timeout 1m;
proxy_connect_timeout 1m;
reset_timedout_connection on;
gzip off;
gzip_proxied off;
# brotli off;
Конфиг полностью
map $request_uri $proxy_uri {
~*/http://(.*)/(.+)$ "http://$1/$2";
~*/https://(.*)/(.+)$ "https://$1/$2";
~*/http://(.*)$ "http://$1/";
~*/https://(.*)$ "https://$1/";
~*/(.*)/(.+)$ "https://$1/$2";
~*/(.*)$ "https://$1/";
default "";
}
map $proxy_uri $proxy_origin {
~*(.*)/.*$ $1;
default "";
}
map $remote_addr $proxy_forwarded_addr {
~^[0-9.]+$ "for=$remote_addr";
~^[0-9A-Fa-f:.]+$ "for=\"[$remote_addr]\"";
default "for=unknown";
}
map $http_forwarded $proxy_add_forwarded {
"" "$proxy_forwarded_addr";
default "$http_forwarded, $proxy_forwarded_addr";
}
server {
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/cors.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cors.example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/cors.example.com/chain.pem;
server_name cors.example.com;
sendfile on;
tcp_nodelay on;
tcp_nopush on;
etag off;
if_modified_since off;
proxy_buffering off;
proxy_cache off;
proxy_cache_convert_head off;
proxy_max_temp_file_size 0;
client_max_body_size 0;
proxy_http_version 1.1;
proxy_pass_request_headers on;
proxy_pass_request_body on;
proxy_read_timeout 1m;
proxy_connect_timeout 1m;
reset_timedout_connection on;
proxy_redirect off;
resolver 77.88.8.8 77.88.8.1 8.8.8.8 8.8.4.4 valid=1d;
gzip off;
gzip_proxied off;
# brotli off;
location / {
if ($proxy_uri = "") {
return 403;
}
# add proxy cors
add_header Access-Control-Allow-Headers "*" always;
add_header Access-Control-Allow-Methods "*" always;
add_header Access-Control-Allow-Origin "*" always;
if ($request_method = "OPTIONS") {
return 204;
}
# pass client to proxy
proxy_set_header Host $proxy_host;
proxy_set_header Origin $proxy_origin;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Client-IP $remote_addr;
proxy_set_header CF-Connecting-IP $remote_addr;
proxy_set_header Fastly-Client-IP $remote_addr;
proxy_set_header True-Client-IP $remote_addr;
proxy_set_header X-Cluster-Client-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Forwarded "$proxy_add_forwarded;proto=$scheme";
# hide original cors
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Request-Headers;
proxy_hide_header Access-Control-Request-Method;
proxy_pass $proxy_uri;
}
}
Комментарии (9)
mavir
13.02.2022 19:24+3Как выше написали, ставить звездочку может обернуться проблемами в браузере. Если хотите разрешить всем доступ, то лучше вставить такие правила
add_header 'Access-Control-Allow-Origin' "$http_origin" always; add_header 'Access-Control-Allow-Headers' "$http_access_control_request_headers" always; add_header 'Access-Control-Request-Method' "$http_access_control_request_method" always;
Т.е. в ответ будут отдаваться только запрашиваемые параметры. Если, например, Origin не передавался, то и не получите в ответе залоговок Access-Control-Allow-Origin
ProstakovAlexey
13.02.2022 20:14не знаю сработает ли такая конфигурация nginx, но идея подставить вместо * домен из запроса выглядит вполне рабочей. Я так делал на helidon 1, у которого не было CORS из коробки, работало нормально. Методы и заголовки правда харкодил.
mavir
13.02.2022 20:50+1Звездочка может приводить к подобной ошибке в браузере
Запрос из постороннего источника заблокирован: Политика одного источника запрещает чтение удаленного ресурса на «http://xxxx.yyy/». (Причина: Учётные данные не поддерживаются, если заголовок CORS «Access-Control-Allow-Origin» установлен в «*»).
RekGRpth
14.02.2022 05:18Нашёл на эту тему пару древних плагинов https://github.com/x-v8/ngx_http_cors_filter и https://github.com/detailyang/ngx_http_cors_filter
uyrij
14.02.2022 14:31выше почти всё сказали, вообще ++, идея рабочая, добавлю, что в гитхабе можно найти многозвездный API gateway в готовом докер образе на альпине. Что удобнее, чем просто конфиг
PlatinumThinker
15.02.2022 08:59+2Кажется плохой идеей позволять ходить куда угодно с твоего nginx — для тестового стенда только если и сгодится. На проде такое использовать опасно т.к. вы по факту делаете общедоступную http проксю
ProstakovAlexey
Как всегда, есть нюансы. Опытный фронтовик воткнет где нибудь:
и браузер получив в ответе
обругается. Так что лучше сразу разобраться с CORS на стороне back, иначе этот костыль с nginx стукнет по голове в неожиданный момент. А так пусть менеджер подумает, подоговаривается с авторами back, обычно в таких переговорах $ позволяют решить проблемку.
petrov_engineer Автор
Это не всегда решение, например, если нужно взаимодействовать со сторонним API. Что в таком случае делать изволите? Скажем, если есть поддержка JSONP это одно, а если нет?
По поводу кредов и кук, можно поддержать, использовав proxy_cookie_domain, но вообще это отдельная тема, как их правильно преобразовывать и обрабатывать.