Browse Source
* /articles/2017/05/17/nginx-authreq-2/ * /articles/2017/05/18/nginx-authreq-3/master
Alex 'AdUser' Z
8 years ago
13 changed files with 597 additions and 0 deletions
@ -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)
|
||||
{
|
@ -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; # <- "или" - берётся отсюда |
||||
} |
||||
|
||||
В принципе, надо - берите. Если таки забодаете апстрим - вообще респект и уважуха :-). |
||||
|
||||
Далее покажу примеры реального использования для каждого случая. |
@ -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)" ]; |
||||
} |
After Width: | Height: | Size: 21 KiB |
@ -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) на базе этого же модуля. |
@ -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" ]; |
||||
} |
After Width: | Height: | Size: 50 KiB |
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html> |
||||
<html> |
||||
<!-- поскипано --> |
||||
<body> |
||||
<% if (my $msg = stash('msg')) { %> |
||||
<h3><%= $msg %><h3> |
||||
<% } %> |
||||
<form method="POST" action="/auth"> |
||||
<input name="user" type="text"> |
||||
<input name="pass" type="password"> |
||||
<input type="submit" value="Получить код"> |
||||
</form> |
||||
</body> |
||||
</html> |
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html> |
||||
<html> |
||||
<!-- поскипано --> |
||||
<body> |
||||
<% if (my $msg = stash('msg')) { %> |
||||
<h3><%= $msg %><h3> |
||||
<% } %> |
||||
<form method="POST" action="/auth"> |
||||
<input name="user" type="text"> |
||||
<input name="code" type="text"> |
||||
<input type="submit" value="Проверить код"> |
||||
</form> |
||||
</body> |
||||
</html> |
@ -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 на все запросы, чтоб больше не приходил. |
@ -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" ]; |
||||
} |
After Width: | Height: | Size: 70 KiB |
After Width: | Height: | Size: 53 KiB |
Loading…
Reference in new issue