diff --git a/articles/2017/05/16/nginx-authreq-1/authreq-302.patch b/articles/2017/05/16/nginx-authreq-1/authreq-302.patch new file mode 100644 index 0000000..221a130 --- /dev/null +++ b/articles/2017/05/16/nginx-authreq-1/authreq-302.patch @@ -0,0 +1,32 @@ +--- src/http/modules/ngx_http_auth_request_module.c 2017-03-03 12:55:31.236056000 +1000 ++++ src/http/modules/ngx_http_auth_request_module.c 2017-03-03 13:09:15.908223000 +1000 +@@ -161,6 +161,29 @@ + return ctx->status; + } + ++ if (ctx->status == NGX_HTTP_MOVED_TEMPORARILY) { ++ sr = ctx->subrequest; ++ ++ h = sr->headers_out.location; ++ ++ if (!h && sr->upstream) { ++ h = sr->upstream->headers_in.location; ++ } ++ ++ if (h) { ++ ho = ngx_list_push(&r->headers_out.headers); ++ if (ho == NULL) { ++ return NGX_ERROR; ++ } ++ ++ *ho = *h; ++ ++ r->headers_out.location = ho; ++ } ++ ++ return ctx->status; ++ } ++ + if (ctx->status >= NGX_HTTP_OK + && ctx->status < NGX_HTTP_SPECIAL_RESPONSE) + { diff --git a/articles/2017/05/16/nginx-authreq-1/index.markdown b/articles/2017/05/16/nginx-authreq-1/index.markdown new file mode 100644 index 0000000..8d57ab6 --- /dev/null +++ b/articles/2017/05/16/nginx-authreq-1/index.markdown @@ -0,0 +1,91 @@ +--- +title: nginx auth_request (1/3): Вводная +tags: nginx, software, devel, репост +--- +У меня тут накопилось немного опыта работы с этим модулем, решил поделиться. + +Прежде всего - что это? Это модуль, который разрешает или запрещает прохождение запроса в nginx на основе **подзапроса**. +Две основных схемы применения: + + * с его помощью можно соорудить WAF (web-application firewall) + * ...и кастомный портал предварительной авторизации + +...всё перечисленное - без модификации исходного сайта. + +- [Вводная](/articles/2017/05/16/nginx-authreq-1/) +- [Многофакторная авторизация, NFA](/articles/2017/05/17/nginx-authreq-2/) +- [WAF, Web-Application firewall](/articles/2017/05/18/nginx-authreq-3/) + +--- + +Выглядит это примерно так. Вот у нас есть типовой запрос, приходящий на некоторый вебсервер: + + GET /files/83084_s.jpg HTTP/1.1 + Host: example.com + User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0 + Accept: */* + Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3 + Accept-Encoding: gzip, deflate + Connection: keep-alive + +nginx пересылает его сначала на указанный location, затем на основе ответа, +решает что делать - пустить дальше или выдать ошибку. + +Общая схема запросов ([исходник](req-graph.msc)): + +![](req-graph.png) + +Authorizer - это отдельная internal локация nginx'а. +Там может быть или `proxy_pass` на внешний сервер, или вызов скриптов с этого же сервера. + +Примерный конфиг nginx'а: + + location / { + auth_request /auth; + proxy_pass http://site.example.com; + } + location = /auth { + internal; + proxy_pass http://127.0.0.1/check.pl; + proxy_pass_request_body off; # <- важно + proxy_set_header Content-Length "0"; # <- важно + proxy_set_header X-Original-URI $request_uri; + } + +Обратите внимание на `Content-Length "0"`. +Нужно это затем, чтобы в пересылаемом POST запросе получатель не ждал данных. + +Далее, допустим мы соорудили некий check.pl, который на основе пересланных запросов будет отвечать кодами 200/401/403/etc. +С 200/OK - всё ясно, запрос проходит дальше. В случае остальных, например 403 - в дефолте nginx покажет простенькую, +и совершенно неинформативную страничку "Access Denied". +Чтобы этого не было, нам нужно добавить в блок `location / {}` перехват этих кодов: + + location / { + <...> + error_page 401 /auth.pl; + error_page 403 /auth.pl; + } + +... где `/auth.pl` -- страница авторизации или сообщения об ошибке. + +Здесь вырисовывается две проблемы: во-первых, нам нужно прописать локейшн и для `/auth.pl`, +во-вторых -- 401/403 коды могут использоваться и в самом сайте. +Первое бы хрен с ним, но второе -- реально проблема, если перехватывать **все** 403 с сайта, +мы можем использовать для этой ошибки только одну *глобальную* страницу на сайт. + +Для обхода этого кейса, я написал [небольшой патч](authreq-302.patch), +который позволяет также использовать код 302, временный редирект. +Правда с включением в апстрим меня завернули, дескать это поломает +использование этого модуля как **одного из** факторов авторизации: + + location / { + proxy_pass http://site.example.com; + auth_req /auth; # запрос должен быть авторизован через nginx authreq + allow 192.168.0.0/16; # ...ИЛИ идти из локальной сети + deny all; + satisfy any; # <- "или" - берётся отсюда + } + +В принципе, надо - берите. Если таки забодаете апстрим - вообще респект и уважуха :-). + +Далее покажу примеры реального использования для каждого случая. diff --git a/articles/2017/05/16/nginx-authreq-1/req-graph.msc b/articles/2017/05/16/nginx-authreq-1/req-graph.msc new file mode 100644 index 0000000..f18e8a3 --- /dev/null +++ b/articles/2017/05/16/nginx-authreq-1/req-graph.msc @@ -0,0 +1,26 @@ +msc { + hscale = 1, + width = "500"; + + c [ label="Client" ], + n [ label="Nginx" ], + a [ label="Authorizer" ]; + + --- [ label="Good request" ]; + c->n [ label="Original request" ]; + n->a [ label="Mirrored request" ]; + a->n [ label="Authorizer decision (200)" ]; + n->c [ label="Client response (200)" ]; + + --- [ label="Bad request" ]; + c->n [ label="Original request" ]; + n->a [ label="Mirrored request" ]; + a->n [ label="Authorizer decision (400)" ]; + n->c [ label="Client response (403)" ]; + + --- [ label="Unsure request" ]; + c->n [ label="Original request" ]; + n->a [ label="Mirrored request" ]; + a->n [ label="Authorizer decision (401)" ]; + n->c [ label="Client response (401)" ]; +} diff --git a/articles/2017/05/16/nginx-authreq-1/req-graph.png b/articles/2017/05/16/nginx-authreq-1/req-graph.png new file mode 100644 index 0000000..55773cf Binary files /dev/null and b/articles/2017/05/16/nginx-authreq-1/req-graph.png differ diff --git a/articles/2017/05/17/nginx-authreq-2/index.markdown b/articles/2017/05/17/nginx-authreq-2/index.markdown new file mode 100644 index 0000000..d4bbf98 --- /dev/null +++ b/articles/2017/05/17/nginx-authreq-2/index.markdown @@ -0,0 +1,190 @@ +--- +title: nginx auth_request (2/3): Многофакторная авторизация, NFA +tags: nginx, software, devel, репост +--- + +- [Вводная](/articles/2017/05/16/nginx-authreq-1/) +- [Многофакторная авторизация, NFA](/articles/2017/05/17/nginx-authreq-2/) +- [WAF, Web-Application firewall](/articles/2017/05/18/nginx-authreq-3/) + +В этой статье рассмотрим, как *в принципе* делается кастомный портал с авторизацией на базе `nginx_authreq`. + +Вся авторизация держится на предположении, что юзеру известен некий секрет, недоступный другим. +Правда если его украсть или скопировать - система не может распознать обмана. + +--- + +N-факторная авторизация - это попытка убрать этот недостаток, путём использования нескольких "секретов" одновеременно. +Украсть N разных секретов в N раз сложнее (если только юзер не ССЗБ, хранящий всё в одном месте). + +В быту, под "N-факторной авторизацией" чаще все понимают пару *логин/пароль*, *смс/email/токен* и возможно что-то ещё, например заход строго с определённоой сети. +В качестве второго компонента, СМС встречается чаще, поскольку привязывает авторизацию к обладанию физической вещью (телефон с определённой симкой), скопировать которую проблематично (но не невозможно). + +Однако, давайте ближе к практике. Вот у нас есть некий сайт, с собственной системой авторизации, к которому нужно дополнительно ограничить доступ. +Например, мы - онлайн банк, предоставляем доступ к операциям со счётом. + +Схема авторизации может выглядеть следующим образом: + +- запросить логин/пароль +- при совпадении пары выше - выслать дополнительный код (действующий ограниченное время) по смс на предварительно указанный номер. +- запросить высланный код +- при совпадении кода - пустить на сайт, иначе - ошибка + +Эта схема может применяться только в том случае, если мы можем влиять на все этапы процесса авторизации. +Если же такой возможности нет - "многофакторная" авторизация превращается в "многоступенчатую". +Например, мы - хостер, предоставляем доступ к ipkvm. На саму железку мы влиять не можем, её прошивку писали не мы. +В этом случае, схема авторизации будет такой: + +- запросить логин/пароль хостера +- при совпадении пары выше - выслать дополнительный код (действующий ограниченное время) по смс на предварительно указанный номер. +- запросить высланный код +- при совпадении кода - пустить на вторую стадию авторизации (самого ipkvm'а) + +Сделать так, чтобы гипотетический ipkvm понимал данные с "первого этапа" не всегда представляется возможным, +вследствие частой практики фундаментального огораживания своих девайсов среди производителей. + +Давайте ещё ближе к практике. Как это будет выглядеть на уровне конфигов и кода? +Конфиг nginx из предыдущей статьи: + + location / { + auth_request /check; + proxy_pass http://site.example.com; + } + location = /check { + internal; + proxy_pass http://127.0.0.1:3000/check; + proxy_pass_request_body off; # <- важно + proxy_set_header Content-Length "0"; # <- важно + proxy_set_header X-Original-URI $request_uri; + } + +Обработчик `http://127.0.0.1:3000/check`: + + sub check { + <...> + + if ($self->session('user')) { + # авторизованный юзер + $self->res->code(200); + $self->render(text => 'OK'); + } + + eval { + # неизвестный юзер + $self->res->code(403); # default deny + my $user = $self->req->param('user'); + my $pass = $self->req->param('pass'); + my $code = $self->req->param('code'); + if ($code and $user) { + # формой отправлен предполагаемый логин и код из sms + my $test = $self->app->redis->get("$user:code"); + unless ($test) { + # мы не посылали кода этому юзеру в последнее время, пнх + $self->render('pages/stage1', msg => 'invalid session'); + } elsif ($code eq $test) { + # указан правильный код + $self->app->redis->del("$user:code"); + $self->session(user => $user); # см блок выше, до eval {} + $self->render(text => 'OK'); + } else { + # указан неправильный код, откатываемся на 1ю стадию + $self->app->redis->del("$user:code"); + $self->render('pages/stage1', msg => 'wrong code'); + } + } elsif ($user and $pass) { + # формой отправлен логин/пароль для проверки + if (check_user_credentials($user, $pass)) { + # юзер существует и пароль верен + my $phone = get_user_phone($user); + if ($phone) { + # для указанного юзера задан телефон + $code = generate_auth_code(); + $self->app->redis->set("$user:code" => $code); + $self->app->redis->expire("$user:code" => 60); # код будет верен в течении минуты + send_sms($phone, "your auth code is: $code"); + $self->render('pages/stage2', msg => 'code sent to your phone'); + } else { + $self->render('pages/stage1', msg => 'no configured phone number for this user'); + } + } else { + $self->render('pages/stage1', msg => 'no such user / wrong password'); + } + } else { + # данных нет, показываем стандартную форму логина + $self->render('pages/stage1'); + } + } or do { + $self->res->code(500); + self->render('pages/error', msg => 'internal error'); + }; + } + +В пример выше используется Mojolicious + redis, но с равным успехом может использоваться CGI, а в качестве хранилища - sqlite, bdb, dbm, memcached или просто обычные файлы. + +На самом деле, это - нерабочий пример. Почему? Потому что особенности™ работы `auth_req`. +На самом деле nginx полностью игнорирует тело ответа от этого модуля и вместо него выдаёт свою стандартную страницу ошибки. +Значения имеют только коды возврата, модуль обрабатытвает всего 3 случая: 200, 401 и *всё остальное. +Мы даже не можем манипулировать состоянием через cookie, поскольку этот заголовок тоже не копируется в ответ. +Только WWW-Authenticate и только при коде ответа 401. + +Следовательно, мы должны переписать пример выше так, чтобы использовались только коды статуса. +Здесь нам поможет следующее шаманство: + +- выносим из `sub check {}` в новый обработчик всё, кроме первого блока. +- в конце оставляем `$self->res->code(403); $self->render(text => 'auth');` +- в nginx `location /auth` переопределяем коды 401,403 на путь к новому обрабобтчику +- добавляем ещё один блок `location` уже для второго обработчика + +Второй блок нужен для того, чтобы `/auth`: а) не попала под действие `/check`, б) ему надо передавать данные через POST. + +Конфиг и код примут следующий вид: + + # Новый блок в nginx + location = /auth { + internal; + proxy_pass http://127.0.0.1:3000/auth; + # proxy_pass_request_body off; # <- важно + # proxy_set_header Content-Length "0"; # <- важно + proxy_set_header X-Original-URI $request_uri; + } + + # то что осталось от первоначального обработчика + sub check { + <...> + + if ($self->session('user')) { + # авторизованный юзер + $self->res->code(200); + $self->render(text => 'OK'); + } + $self->res->code(403); + $self->render(text => 'auth'); + } + # всё остальное перехало сюда: + sub auth { + <...> + } + +Граф вызовов в случае успешной авторизации с первого раза ([исходник](req-graph.msc)). + +![](req-graph.png) + +Примерное содержимое [pages/stage1](stage1.txt), [pages/stage2](stage2.txt). +Любителям подключать тонну css и js на заметку: это всё придётся либо встраивать в страничку, либо мутить ТРЕТИЙ блок `location` в nginx, чтобы оно опять не попало под действие `/check`. +Впрочем, можно сразу "провалить" всю машинерию на уровень ниже, например под `/auth`. Т.е. так: + +* /check -> /auth/check (описывается специальной локацией с точным соотвествием) +* /auth -> /auth/login (описывается общей локацией для /auth, т.е. всё что не /auth/login -- пересылать туда-то через `proxy_pass`) + +Ну и на закуску - что даёт патч, приведённый в первой части, применительно к данной конфигурации. Удобство! +Уходит необходимость в `error_page`, появляется возможность собрать логику в одном месте, и разруливать различные остояния не кодами статуса, а сразу редиректами. +Плюс к тому же появляется возможность передать если не куку, то какой-то параметр через url (хак с `error_page` опять же такого не может). + +В данном примере использования, нас спасает то, что состояний по сути всего 2: известный пользователь (пропускаем) и неизвестный пользователь (перенаправляем на логин). +3х доступных кодов ответа, из которых один (401й) использовате нежелательно из-за побочных эффектов (лезет браузерная форма с паролем) +как раз хватает на 2 основных состояния (варианты "неизвестного пользователя" дополнительно разруливаются в обработчике /auth). + +Теперь представьте, что основных состояний хотя бы 5-6 (известный юзер, неизвестный с кукой, неизвестный без куки, подозрительный, заблокирован временно, заблокирован постоянно). +Шаманство с `error_page` здесь уже не прокатит, тупо не хватит различаемых модулем кодов. + +В следующей части - построение кастомного WAF (Web Application Firewall) на базе этого же модуля. diff --git a/articles/2017/05/17/nginx-authreq-2/req-graph.msc b/articles/2017/05/17/nginx-authreq-2/req-graph.msc new file mode 100644 index 0000000..2f7b4ad --- /dev/null +++ b/articles/2017/05/17/nginx-authreq-2/req-graph.msc @@ -0,0 +1,39 @@ +msc { + hscale = 1, + width = "700"; + + c [ label="client" ], + n [ label="nginx" ], + k [ label="/check" ], + a [ label="/auth" ], + d [ label="/" ]; + + --- [ label="unknown user, redirect to login page" ]; + c -> n [ label="GET / HTTP/1.1" ]; + n -> k [ label="GET / HTTP/1.1" ]; + n <- k [ label="403 Forbidden" ]; + n -> n [ label="error_page /auth" ]; + n -> a [ label="GET /auth" ]; + n <- a [ label="200 OK (pages/stage1)"]; + c <- n [ label="200 OK (pages/stage1)"]; + + --- [ label="send auth data, stage 1 (user+pass)" ]; + c -> n [ label="POST /auth (user+pass)" ]; + n -> a [ label="POST /auth (user+pass)" ]; + n <- a [ label="200 OK (pages/stage2)" ]; + c <- n [ label="200 OK (pages/stage2)" ]; + + --- [ label="send auth data, stage 2 (user+code)" ]; + c -> n [ label="POST /auth (user+code)" ]; + n -> a [ label="POST /auth (user+code)" ]; + n <- a [ label="302 / +Set-Cookie: hmac(base64({user=$user}))" ]; + c <- n [ label="302 / +Set-Cookie: hmac(base64({user=$user}))" ]; + + --- [ label="authorized user" ]; + c -> n [ label="GET / HTTP/1.1" ]; + n -> k [ label="GET / HTTP/1.1" ]; + n <- k [ label="200 OK" ]; + n -> d [ label="GET / HTTP/1.1" ]; + n <- d [ label="200 OK" ]; + c <- n [ label="200 OK" ]; +} diff --git a/articles/2017/05/17/nginx-authreq-2/req-graph.png b/articles/2017/05/17/nginx-authreq-2/req-graph.png new file mode 100644 index 0000000..3a5d0d5 Binary files /dev/null and b/articles/2017/05/17/nginx-authreq-2/req-graph.png differ diff --git a/articles/2017/05/17/nginx-authreq-2/stage1.txt b/articles/2017/05/17/nginx-authreq-2/stage1.txt new file mode 100644 index 0000000..2a6437f --- /dev/null +++ b/articles/2017/05/17/nginx-authreq-2/stage1.txt @@ -0,0 +1,14 @@ + + + + + <% if (my $msg = stash('msg')) { %> +

<%= $msg %>

+ <% } %> +
+ + + +
+ + diff --git a/articles/2017/05/17/nginx-authreq-2/stage2.txt b/articles/2017/05/17/nginx-authreq-2/stage2.txt new file mode 100644 index 0000000..4a3d7a0 --- /dev/null +++ b/articles/2017/05/17/nginx-authreq-2/stage2.txt @@ -0,0 +1,14 @@ + + + + + <% if (my $msg = stash('msg')) { %> +

<%= $msg %>

+ <% } %> +
+ + + +
+ + diff --git a/articles/2017/05/18/nginx-authreq-3/index.markdown b/articles/2017/05/18/nginx-authreq-3/index.markdown new file mode 100644 index 0000000..abcfa32 --- /dev/null +++ b/articles/2017/05/18/nginx-authreq-3/index.markdown @@ -0,0 +1,161 @@ +--- +title: nginx auth_request (3/3): Web-Application firewall, WAF +tags: nginx, software, devel, репост +--- + +- [Вводная](/articles/2017/05/16/nginx-authreq-1/) +- [Многофакторная авторизация, NFA](/articles/2017/05/17/nginx-authreq-2/) +- [WAF, Web-Application firewall](/articles/2017/05/18/nginx-authreq-3/) + +Сразу оговорюсь - я знаю про существование `nginx naxsi` и `mod_security`, речь про то, как вообще такое делается. + +Под WAF я понимаю некое дополнение к веб-серверу, выполняющее одну или несколько следющих задач: + +* блокировка вредоносных запросов +* подтверждение юзером "сомнительных" запросов +* ограничение частоты запросов по сложным критериям + +Дополнительно можно собирать статистику. + +--- + +Принцип работы всё тот же - nginx сначала пересылает копию запроса на какой-то внешний сервис +и на основании ответа от этого сервиса - решает, пропускать ли исходный запрос или нет. + +Механизм работы опсан в предыдущей статье, поэтому интересны прежде всего алгоритмы: +1) выделение запросов одного пользователя, +2) ограничения частоты запросов. +3) определения "вредоносности" или "подозрительности" запроса, +Нетрудно заметить, третья задача зависит от первой, а вторая и третья тесно связаны. + +По порядку. + +Выделение запросов одного пользователя +-------------------------------------- + +Первая и очевидная мысль, приходящая в голову - просто смотреть запросы с одного ip. +Но тут есть множество тонкостей. Случаев, когда с одного адреса могут сидеть несколько +пользователей - достаточно много: nat, vpn, tor, ipv6-to-4 брокер. +И обратный случай - когда один юзер может сидеть с нескольких адресов: tor. + +Что делать? На заголовки полагаться нельзя, там можно подделать абсолютно всё. +Комбинация ip+заголовок, например User-Agent? Это ещё хуже, подделкой заголовка можно добиться, +чтобы система определяла тебя как разных юзеров, даже не меняя свой ip. + +Один из возможных вариантов - заставить каждого неопознанного клиента произвоить некую ресурсоёмкую операцию, +после выполнения которой этому клиенту присваивается уникальный идентификатор, +который тот будет предъявлять в дальнейшем как доказательство выполненной работы. +Разумеется, его нужно защитить от подделки и ограничить срок годности. + +Блок схема ([исходник](schema-1.dot)): + +[![](schema-1_tn.png)](schema-1.png) + +Путь "известного" юзера показан жирной линией, "неизвестного" - прерывистой. +Операция (3) может быть к/либо вычислением на стороне пользователя, с сообщением результата. +Или же - проверкой на робота, например следование по редиректам. сделанных средствами самого html. + +Схема может изменяться в некоторых пределах, например (2) "N" может переходить не в (4), а в (3). +Можно например, "неизвестных" клиентов сразу кидать на капчу, т.е. (1) "N" -> (4). + +Думаю не нужно напоминать, что все операции, кроме (3) следует оптимизировать на минимальные +затраты ресурсов и максимальное быстродействие. Если используется капча, её лучше нагенерить заранее +и продумать механизм "карантина" на какое-то время для показанных, но не решённых. + +Обратите внимание, путь по жирной линии выполняется в пределах одного обработчика. +А вот переходы к другим состояниям - требуют перехода на другие странички. +Помните, что я говорил про `error_page` и поддержку редиректов? + +Ограничение частоты запросов +---------------------------- + +Здесь может быть куча вариантов реализации. + +Например самое простое и железобетонное решение: +выделять временн*ы*е слоты определённой длительности +и считать запросы юзера в пределах текущего слота. + + my $len = 5; # новый слот каждые 5 минут + my $time = time(); + # вычисляем имя слота + my $slot = sprintf "req:%d:%d", $len, ($time - ($time % ($len * 60))); + # увеличиваем число запросов для юзера с $uuid + # и узнаём, сколько запросов он уже сделал в пределах данного слота + my $reqs = $redis->hincr($slot, $uuid => 1); # O(1) + $redis->expire($slot, 3600) if $reqs == 1; + # если $reqs >= $limit - блокируем запрос + +Метод хорош тем, что крайне прост (1 запрос), хорошо масштабируется +и может работать вообще без обслуживания. +Основной недостаток - низкая "разрешающая способность", +на границе временного слота можно превысить лимит *до* 2х раз. + +Если нам нужна гарантия, что в каждый момент времени лимит не будет превышен, подход несколько другой. +На каждого юзера заводится по персональному списку, туда пишется время запросов. +Недостатки: стоимость выше, больший расход памяти. + +Принцип действия такой: при частых запросах в начале очереди растёт число "недавних" запросов. +Как только N-ый элемент оказывается "недавним", значит лимит превышен. + +Вариант 2/а: + + my ($time, $limit) = (5, 60); # время окна в минутах и количество запросов + my $now = time(); + my $key = sprintf "user:%s", $uuid; + my $some = $redis->lindex($key, $limit - 1); # O(N) + my $next = $some + ($time * 60); + if ($now > $next) { + $redis->lpush($key, $now); # O(1) + # периодически подрезаем список, чтоб не разрастался сверх меры + $redis->ltrim($key, 0, $limit - 1); # O(N), где N - количество удалённых элементов + $redis->expire($key, 3600) if $some == 0; + } else { + # лимит превышен + } + +Вариант 2/б, где "гарантированно дорогой" lindex() с O(N), +заменяется на llen() + lindex(-1), т.е. 2 x O(1). +Хотя в теории, lindex(N) для списка в котором меньше N элементов - тоже должен отрабатывать за O(1). + + my ($time, $limit) = (5, 60); # время окна в минутах и количество запросов + my $now = time(); + my $key = sprintf "user:%s", $uuid; + my $len = $redis->llen($key); # O(1) + if ($len < $limit) { + # первичное заполнение + $redis->lpush($key, $now); # O(1) + $redis->expire($key, 3600) if $len == 0; + return; + } else { + $redis->ltrim($key, 0, $limit - 1); # O(N), где N - количество удалённых элементов + my $last = $redis->lindex($key, -1); # O(1) + my $next = $last + ($time * 60); + if ($now >= $next) { + $redis->lpush($key, $now); + return; + } + } + # лимит превышен + +Впринципе, всё это экономия на спичках. Значительно большего эффекта можно добиться, +если считать не абстрактные "запросы", а прикинуть стоимость конкретного запроса +в плане нагрузки и выделять пользователю "бюджет" на пользование. + +Например, показ странички с картинками, где половина содержимого - статика, +а остальное закешировано - это одно. Полнотекстовый поиск по сайту - это уже другое. +А попытка авторизации на сайте - совсем даже третье. + +"Вредоносность" и "подозрительность" запроса +-------------------------------------------- + +Здесь сложно дать какие-то рекомендации, смотрите по ситуации. +Можно анализировать заголовки (User-Agent/Referer/Accept/...), частоту запросов, их логическую взаимосвязь. +Например, запрос к автодополнению с X-Requested-With - это с высокой вероятностью человек. +Постоянная долбёжка поля поиска с интервалом в секунду - практически наверняка бот-дудосер. +Монотонные запросы несвязанные друг с другом - поисковый бот. +Пиковые всплески запросов с интервалом в несколько минут - юзер, который открывает несколько вкладок сразу, а потом сидит их читает. + +Вобщем, на практке быстро научитесь на что нужно смотреть. + +Ну и не стоит забывать про типовые запросы для поиска админок вордпресса, скуэля, гостевух и прочего говнокода на похапе. +Клиенту, спалившемся на таком можно просто возвращать 404 на все запросы, чтоб больше не приходил. diff --git a/articles/2017/05/18/nginx-authreq-3/schema-1.dot b/articles/2017/05/18/nginx-authreq-3/schema-1.dot new file mode 100644 index 0000000..e45e7fd --- /dev/null +++ b/articles/2017/05/18/nginx-authreq-3/schema-1.dot @@ -0,0 +1,30 @@ +digraph { + s [ shape="box", label="[0] Request" ]; + p [ shape="box", label="[A] Pass" ]; + b [ shape="box", label="[B] Block" ]; + t [ shape="oval", label="[6] Set new token" ]; + c [ shape="oval", label="[4] Show captcha" ]; + w [ shape="oval", label="[3] Some heavy\noperation" ]; + i [ shape="oval", label="[9] Accounting" ]; + rl [ shape="diamond", label="[8] Is rate-limit\nexceeded?" ]; + ht [ shape="diamond", label="[1] Has token?" ]; + vt [ shape="diamond", label="[2] Is token valid?" ]; + cv [ shape="diamond", label="[5] Is captcha valid?" ]; + ts [ shape="diamond", label="[7] Is token set?" ]; + + s -> ht [ style="bold", weight=2 ]; + ht -> vt [ label="Y", style="bold", weight=2 ]; + ht -> w [ label="N", style="dashed" ]; + w -> t [ style="dashed" ]; + t -> ts [ style="solid" ]; + ts -> b [ label="N" ]; + ts -> p [ label="Y", style="solid" ]; + vt -> rl [ label="Y", style="bold", weight=2 ]; + rl -> i [ label="N", style="bold", weight=2 ]; + i -> p [ style="bold", weight=2 ]; + rl -> b [ label="Y", style="bold" ]; + vt -> c [ label="N" ]; + c -> cv; + cv -> c [ label="N" ]; + cv -> t [ label="Y" ]; +} diff --git a/articles/2017/05/18/nginx-authreq-3/schema-1.png b/articles/2017/05/18/nginx-authreq-3/schema-1.png new file mode 100644 index 0000000..239dbb8 Binary files /dev/null and b/articles/2017/05/18/nginx-authreq-3/schema-1.png differ diff --git a/articles/2017/05/18/nginx-authreq-3/schema-1_tn.png b/articles/2017/05/18/nginx-authreq-3/schema-1_tn.png new file mode 100644 index 0000000..73e9477 Binary files /dev/null and b/articles/2017/05/18/nginx-authreq-3/schema-1_tn.png differ