Предыдущий вариант ограничения был статичен, для внесения изменений в список API-KEY требовался перезапуск Nginx. Нужно решение с динамическими ключами. OpenResty - это комплекс полезных расширений для сервера Nginx, на его базе можно реализовать требуемое. Используем Docker, официальный образ OpenResty и официальный образ сервера Redis. Redis будет хранить в себе API-ключи.
Задача: все запросы, поступающие на адрес http://192.168.0.71:8080/ проверять на наличие заголовка API-KEY и на его базе принимать решение о фильтрации запросов. Запросы с правильными API-KEY перенаправлять на адрес http://192.168.0.72/.
Внимание! Все указанные операции проводятся на хосте 192.168.0.71!
Установить Docker. Для CentOS:
yum install docker
Установить контейнер OpenResty. Здесь мы запускаем контейнер, подключаем в него с хоста папку с конфигами nginx, чтобы было удобнее их редактировать:
docker run -d \
--name openresty \
-p 8080:8080 \
-v /media/docker-data/openresty/etc/nginx/conf.d:/etc/nginx/conf.d \
openresty/openresty:alpine
Установить контейнер Redis. Здесь также подключаем папку хоста, для хранения данных на случай восстановления при аварийных ситуациях:
docker run -d \
--name redis \
-p 6379:6379 \
-v /media/docker-data/redis/data:/data \
redis redis-server --appendonly yes
Создаем конфиг nginx:
#/media/docker-data/openresty/etc/nginx/conf.d/default.conf:
# Выделение памяти для функции расчета частоты
lua_shared_dict my_limit_req_store 100m;
server {
listen 8080;
server_name 192.168.0.71;
location / {
access_by_lua_file /etc/nginx/conf.d/apikey_module.lua;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://192.168.0.72/;
}
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
# Этот путь изменен на корректный для OpenResty, иначе в логах будет ошибка 404 вместо 503,
# просто потому, что файл описания 503-й ошибки не найден...
location = /50x.html {
root /usr/local/openresty/nginx/html;
}
}
Создаем скрипт LUA, подключаемый в конфиге выше (строка access_by_lua_file…):
#/media/docker-data/openresty/etc/nginx/conf.d/apikey_module.lua:
-- Функции в начале файла, т.к. интерпретатор LUA имеет последовательную обработку скрипта и должен видеть вызывающийся
-- код до его вызова.
-- Проверка наличия ключа в базе Redis
function check_key(key)
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000) -- 1 second
- Здесь укажите собственные настройки сервера Redis
local ok, err = red:connect("192.168.0.71", 16379)
-- Если не удалось подключиться к серверу Redis - пишем в лог
if not ok then
ngx.log(ngx.ERR, "failed to connect to Redis server: ", err)
return false
end
-- Запрос ключа в сервере Redis
local value, err = red:get(key)
-- Если не удалось выполнить запрос - пишем в лог
if not value then
ngx.log(ngx.ERR, "failed to get key in Redis server: ", err)
return false
end
-- Если для ключа нет значения - то возвращаем false
-- иначе - возвращаем true
if value == ngx.null then
ngx.log(ngx.ERR, "no value found for key ", key)
return false
else
-- Ключ найден в базе Redis, пишем в лог. Убрать потом.
--ngx.log(ngx.ERR, "key:", key, " value:", value)
return true
end
end
--Функция ограничения частоты запросов
function limit_requests()
local limit_req = require "resty.limit.req"
-- my_limit_req_store прописывается в перед блоком server конфига Nginx (default.conf)
-- 200 запросов в секунду с одного IP (исключительно для тестов)
local lim, err = limit_req.new("my_limit_req_store", 200, 0)
-- Если ограничитель не инициирован, то смиренно отдадим контент, но с задержкой
if not lim then
ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
-- Спим 1 сек
ngx.sleep(1)
return ngx.exit(200)
end
-- Ключ счетчика частоты - IP запрашивающего
local key = ngx.var.binary_remote_addr
local delay, err = lim:incoming(key, true)
if not delay then
-- Если запрос не пролазит по частоте - то даем ответ 503
if err == "rejected" then
ngx.log(ngx.ERR, "delay rejected: ", err)
return ngx.exit(503)
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(503)
end
if delay>= 0.001 then
local excess = err
ngx.sleep(delay)
end
end
-- Ищем ключ API_KEY в заголовках
local key = ngx.var.http_api_key
-- Если ключ есть, то нужно проверить его по базе Redis
if key then
-- Проверка на наличие ключа в базе Redis
local key_exist = check_key(key)
-- Если ключа нет - передаем управление на функцию ограничения частоты запросов
if not key_exist then
ngx.log(ngx.ERR, "check_key() returned false")
limit_requests()
end
else
limit_requests()
end
Для добавления ключей в Redis используем консоль:
docker run -it --link redis:redis --rm redis redis-cli -h redis -p 6379
#Добавляем ключи:
redis:6379> set 111 ""
redis:6379> set 222 ""
redis:6379> set 333 ""
quit
Испытания.
# Установка нагрузочного инструмента для испытаний
yum install siege
Коды результатов: (200 - запрос ушел на следующий сервер, 503 - запрос отброшен).
10 запросов с неверным ключом:
[root@localhost ~]# siege -b -r 1 -c 10 -H "API-KEY: 1aaa111" http://192.168.0.71:8080/icons/ubuntu-logo.png
** SIEGE 4.0.2
** Preparing 10 concurrent users for battle.
The server is now under siege...
HTTP/1.1 503 0.01 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 503 0.01 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 503 0.01 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 503 0.01 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 503 0.01 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 200 0.02 secs: 3338 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 503 0.01 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 200 0.02 secs: 3338 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 503 0.02 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 503 0.03 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
10 запросов без ключа:
[root@localhost ~]# siege -b -r 1 -c 10 http://192.168.0.71:8080/icons/ubuntu-logo.png
** SIEGE 4.0.2
** Preparing 10 concurrent users for battle.
The server is now under siege...
HTTP/1.1 503 0.01 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 503 0.01 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 503 0.01 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 503 0.01 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 200 0.02 secs: 3338 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 503 0.02 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 503 0.02 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 503 0.02 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 503 0.02 secs: 541 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 200 0.02 secs: 3338 bytes ==> GET /icons/ubuntu-logo.png
10 запросов с правильным ключом:
[root@localhost ~]# siege -b -r 1 -c 10 -H "API-KEY: 111" http://192.168.0.71:8080/icons/ubuntu-logo.png
** SIEGE 4.0.2
** Preparing 10 concurrent users for battle.
The server is now under siege...
HTTP/1.1 200 0.01 secs: 3338 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 200 0.01 secs: 3338 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 200 0.01 secs: 3338 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 200 0.01 secs: 3338 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 200 0.02 secs: 3338 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 200 0.02 secs: 3338 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 200 0.02 secs: 3338 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 200 0.02 secs: 3338 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 200 0.02 secs: 3338 bytes ==> GET /icons/ubuntu-logo.png
HTTP/1.1 200 0.02 secs: 3338 bytes ==> GET /icons/ubuntu-logo.png
Источники:
- https://habr.com/ru/company/southbridge/blog/329876/#nuzhny-li-ogranichenie-skorosti-i-sheyping-trafika
- https://hub.docker.com/r/openresty/openresty
- https://hub.docker.com/_/redis
- http://openresty.org/en/dynamic-routing-based-on-redis.html
- https://github.com/openresty/lua-resty-limit-traffic
- https://habr.com/ru/post/215235/