Вариант балансировки двух идентичных узлов ESIA Adapter на Nginx. Принцип тот же, что для HAProxy: запросы одного входа и выданные узлом токены направляются на тот же узел, а узел определяется по cookie и по значению nid в токенах. Параметр NODE_ID, состояние входа на узле и настройка самих узлов описаны в разделе «Резервирование и балансировка (HAProxy)» и здесь не повторяются.
Возможности Nginx
Nginx в открытой редакции не разбирает JWT, не выдаёт cookie привязки к серверу и не выполняет активные проверки состояния. Поэтому в конфигурации:
- узел из токена определяется скриптом njs (модуль
ngx_http_js_module); - cookie
ESIA_NODEвыдаётся именованным location выбранного узла; - отказ узла обнаруживается пассивно (
max_fails,fail_timeout), а резервным сервером группы служит второй узел.
Модуль njs входит в состав сборок nginx.org (пакет nginx-module-njs) и подключается директивой load_module.
Определение узла
| Запрос | Откуда определяется узел |
|---|---|
/oauth2/authorize, /extidp/esia, /extidp/esia/callback, /oauth2/logout (от браузера) | cookie ESIA_NODE |
/oauth2/token, обмен кода (code) или обновление (refresh_token) | claim nid токена в теле запроса |
/oauth2/userinfo | claim nid токена доступа в заголовке Authorization |
/oauth2/logout с id_token_hint | claim nid в параметре id_token_hint |
| Новый вход без cookie и токена | случайный выбор узла, 50% на каждый |
Приоритет: токен, затем cookie, затем выбор для нового входа.
Скрипт njs
Файл /etc/nginx/njs/node.js. Имена узлов в массиве NODES совпадают со значениями NODE_ID.
// Определение узла: токен (Authorization, тело запроса, id_token_hint) -> cookie ESIA_NODE -> новый вход
var NODES = ['node-a', 'node-b'];
function nidFromJwt(token) {
var parts = token.split('.');
if (parts.length < 2) { return ''; }
try {
return JSON.parse(Buffer.from(parts[1], 'base64url').toString()).nid || '';
} catch (e) {
return '';
}
}
function tokenOf(r) {
var auth = r.headersIn['Authorization'];
if (auth && /^Bearer\s+/i.test(auth)) {
return auth.replace(/^Bearer\s+/i, '');
}
if (r.uri === '/oauth2/token') {
var m = /(?:^|&)(?:code|refresh_token)=([^&]+)/.exec(r.requestText || '');
if (m) { return decodeURIComponent(m[1]); }
}
if (r.uri === '/oauth2/logout' && r.args.id_token_hint) {
return r.args.id_token_hint;
}
return '';
}
function route(r) {
var node = nidFromJwt(tokenOf(r));
if (NODES.indexOf(node) < 0) { node = r.variables.cookie_ESIA_NODE; }
if (NODES.indexOf(node) < 0) { node = r.variables.new_node; }
r.internalRedirect('@' + node);
}
export default { route };
Скрипт читает тело запроса /oauth2/token и после выбора узла выполняет внутренний переход на именованный location узла, при этом тело передаётся на узел без изменений. Подпись токена скрипт не проверяет, её проверяет адаптер.
Конфигурация Nginx
Файл /etc/nginx/nginx.conf:
load_module modules/ngx_http_js_module.so;
events {}
http {
js_path /etc/nginx/njs;
js_import node from node.js;
# узел для нового входа (без cookie и токена)
split_clients "${request_id}" $new_node {
50% node-a;
* node-b;
}
# в каждой группе первым идёт свой узел, вторым — резервный
upstream node-a {
server 10.0.0.11:4010 max_fails=2 fail_timeout=10s;
server 10.0.0.12:4010 backup;
}
upstream node-b {
server 10.0.0.12:4010 max_fails=2 fail_timeout=10s;
server 10.0.0.11:4010 backup;
}
server {
listen 443 ssl;
ssl_certificate /etc/nginx/certs/esia-adapter.crt;
ssl_certificate_key /etc/nginx/certs/esia-adapter.key;
client_body_buffer_size 64k;
client_max_body_size 1m;
location / {
js_content node.route;
}
location @node-a {
proxy_pass http://node-a;
include /etc/nginx/esia-proxy.conf;
add_header Set-Cookie "ESIA_NODE=node-a; Path=/; HttpOnly; Secure" always;
}
location @node-b {
proxy_pass http://node-b;
include /etc/nginx/esia-proxy.conf;
add_header Set-Cookie "ESIA_NODE=node-b; Path=/; HttpOnly; Secure" always;
}
}
}
Файл /etc/nginx/esia-proxy.conf:
proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_next_upstream error timeout http_502 http_503 http_504; proxy_connect_timeout 3s;
| Элемент | Назначение |
|---|---|
js_content node.route | все запросы проходят через скрипт, который выбирает узел и выполняет переход на @node-a или @node-b |
split_clients | распределяет новые входы между узлами |
upstream node-a, upstream node-b | группа с основным сервером и вторым узлом в роли backup: при отказе основного запросы уходят на второй |
max_fails, fail_timeout | сервер считается недоступным после двух неудачных обращений на 10 секунд |
add_header Set-Cookie ... ESIA_NODE | закрепляет браузерный вход за узлом |
client_body_buffer_size | тело запроса обмена токена должно помещаться в буфер, чтобы скрипт мог его прочитать |
proxy_next_upstream | условия передачи запроса на резервный сервер |
Поведение при отказе узла
- Запросы, адресованные недоступному узлу, передаются на второй узел группы. Состояние входа, сессии и данные пользователя находятся в памяти отказавшего узла, поэтому пользователю, находившемуся в процессе входа, требуется повторная аутентификация; последствия для токенов описаны в разделе про HAProxy.
- Отказ обнаруживается пассивно: запрос, пришедший к недоступному узлу, повторяется на втором узле при ошибке соединения или таймауте. Запрос
POSTповторяется только если ошибка произошла до его передачи на узел. - Узел возвращается в работу после истечения
fail_timeout, когда очередной запрос к нему завершается успешно. - Проверка
/health/readyбалансировщиком в открытой редакции Nginx не выполняется; активные проверки (health_check) доступны в NGINX Plus и контролируются мониторингом.
Плановое обслуживание
Чтобы вывести узел из приёма новых входов, в split_clients заменяют блок на * node-b; и выполняют перезагрузку конфигурации:
sudo nginx -t && sudo nginx -s reload
Запросы уже начатых входов и выданных токенов продолжают направляться на узел node-a по cookie и nid. После истечения срока жизни сессий и токенов (SESSION_EXPIRED, TOKEN_TTL) узел останавливается и обновляется. Затем блок split_clients возвращается к исходному виду.
Проверка
- Выполнить
nginx -t: конфигурация и модуль njs загружаются без ошибок. - Выполнить запрос
/oauth2/userinfoс токеном, выданным узломnode-a: ответ приходит отnode-a; токен узлаnode-bнаправляется наnode-b. - Повторить для обмена кода и токена обновления (
POST /oauth2/token) и для/oauth2/logout?id_token_hint=.... - Выполнить
GET /oauth2/authorizeбез cookie: ответ содержитSet-Cookie: ESIA_NODE=..., повторный запрос с этим cookie попадает на тот же узел. - Остановить
node-a: запросы с его токеном и cookie обслуживаются узломnode-b; после запускаnode-aи истеченияfail_timeoutзапросы возвращаются на него.