Два идентичных узла ESIA Adapter размещаются за балансировщиком HAProxy. Адаптер хранит состояние входа в памяти узла, поэтому запросы одного входа и выданные узлом токены направляются на тот же узел. Для этого каждому узлу задаётся уникальный NODE_ID, а балансировщик определяет узел по cookie и по значению nid в токенах.
Параметр NODE_ID
NODE_ID — необязательный идентификатор узла. Значение возвращается в JWT и используется балансировщиком. Значение задаётся переменной окружения службы и должно быть уникальным для каждого узла, например node-a и node-b.
Идентификатор узла содержится в:
| Объект | Где находится |
|---|---|
| Код авторизации (JWT) | claim nid |
Токен доступа access_token (JWT) | claim nid |
Токен обновления refresh_token (JWT) | claim nid |
id_token (JWT) | claim nid |
Ответ /oauth2/token | поле node_id |
Если NODE_ID не задан, nid и node_id пустые.
Состояние на узле
| Данные | Где хранятся |
|---|---|
Сессия пользователя (cookie sid) | память узла |
Запрос к ЕСИА (параметр state) | память узла |
Данные пользователя (userinfo) | память узла |
| Коды и токены | память узла; при заданном DB_URL — PostgreSQL |
Данные пользователя и сессия хранятся в памяти узла в любом варианте, включая DB_URL, поэтому привязка к узлу нужна всегда.
| Запрос | Откуда определяется узел |
|---|---|
/oauth2/authorize, /extidp/esia, /extidp/esia/callback, /oauth2/logout (от браузера) | cookie балансировщика ESIA_NODE |
/oauth2/token, обмен кода (code) | claim nid кода авторизации в теле запроса |
/oauth2/token, обновление (refresh_token) | claim nid токена обновления в теле запроса |
/oauth2/userinfo | claim nid токена доступа в заголовке Authorization |
/oauth2/logout с id_token_hint | claim nid в параметре id_token_hint |
Настройка узлов
На обоих узлах значения одинаковы:
| Параметр | Значение |
|---|---|
ISSUER, BASE_URL | адрес балансировщика (https://...) |
KEY | один и тот же ключ подписи токенов: тогда /oauth2/public_keys возвращает одинаковый набор ключей на обоих узлах |
CLIENTS | одинаковый clients.json |
EXTIDP | extidp.json одной и той же информационной системы ЕСИА |
SESSION_*, *_TTL, FORCE_PKCE | одинаковые значения |
Различаются NODE_ID и параметры криптопровайдера (контейнер и сертификат узла) в extidp.json.
Пример для службы systemd (установка):
# узел A Environment="NODE_ID=node-a" # узел B Environment="NODE_ID=node-b"
Настройка балансировщика Nginx описана в разделе «Резервирование и балансировка (Nginx)».
Конфигурация HAProxy
Имена серверов в backend совпадают со значениями NODE_ID. Узел из токенов определяется стандартными средствами HAProxy: токен — JWT, его вторая часть (payload) кодируется base64url.
global
log /dev/log local0
stats socket /run/haproxy/admin.sock mode 660 level admin
defaults
mode http
log global
option httplog
option redispatch
retries 2
timeout connect 5s
timeout client 60s
timeout server 60s
timeout http-request 10s
frontend esia_https
bind *:443 ssl crt /etc/haproxy/certs/esia-adapter.pem
option http-buffer-request
http-request set-header X-Forwarded-Proto https
# узел из access_token (/oauth2/userinfo и другие запросы с Bearer)
http-request set-var(txn.nid) req.hdr(Authorization),word(2,.),ub64dec,json_query('$.nid') if { req.hdr(Authorization) -m beg -i "Bearer " }
# узел из кода авторизации (обмен кода на токен)
http-request set-var(txn.nid) req.body_param(code),word(2,.),ub64dec,json_query('$.nid') if { path /oauth2/token } { req.body_param(code) -m found } !{ var(txn.nid) -m found }
# узел из токена обновления
http-request set-var(txn.nid) req.body_param(refresh_token),word(2,.),ub64dec,json_query('$.nid') if { path /oauth2/token } { req.body_param(refresh_token) -m found } !{ var(txn.nid) -m found }
# узел из id_token_hint (выход)
http-request set-var(txn.nid) url_param(id_token_hint),word(2,.),ub64dec,json_query('$.nid') if { path /oauth2/logout } { url_param(id_token_hint) -m found } !{ var(txn.nid) -m found }
# узел в журнале
http-request capture var(txn.nid) len 32
default_backend esia_nodes
backend esia_nodes
balance roundrobin
option httpchk
http-check send meth GET uri /health/ready ver HTTP/1.1 hdr Host esia-adapter.corp
http-check expect status 200
# браузерный вход: cookie со значением NODE_ID
cookie ESIA_NODE insert indirect nocache httponly secure
# запросы с токеном: узел из claim nid
use-server node-a if { var(txn.nid) -m str node-a }
use-server node-b if { var(txn.nid) -m str node-b }
default-server check inter 2s fall 2 rise 2
server node-a 10.0.0.11:4010 cookie node-a
server node-b 10.0.0.12:4010 cookie node-b
| Строка | Назначение |
|---|---|
option http-buffer-request | HAProxy дожидается тела запроса: без этого req.body_param недоступен при выборе сервера |
set-var(txn.nid) ... json_query('$.nid') | извлекает nid из payload токена; подпись токена балансировщик не проверяет, её проверяет адаптер |
cookie ESIA_NODE insert indirect nocache | при первом запросе браузера выдаёт cookie со значением NODE_ID выбранного сервера; все запросы входа (authorize, callback, logout) возвращаются на тот же узел |
use-server ... if { var(txn.nid) ... } | запросы с токеном направляются на узел, выдавший токен; правило выполняется раньше проверки cookie |
option redispatch, check | при недоступности узла запрос передаётся на другой узел |
http-check ... /health/ready | узел считается доступным, если /health/ready возвращает 200 |
Для разбора токенов можно также использовать конвертер jwt_payload_query('$.nid') (HAProxy 2.8 и выше, сборка с OpenSSL).
Поведение при отказе узла
- Правило
use-server, указывающее на недоступный сервер, игнорируется: запрос передаётся на другой узел по обычным правилам балансировки. - Отказ узла обнаруживается проверкой состояния за время
inter×fall(в примере около 4 секунд); запросы, пришедшие в этот интервал, могут завершиться ошибкой, и клиент повторяет запрос. - Состояние входа, сессии и данные пользователя находятся в памяти отказавшего узла. Пользователю, который находился в процессе входа, требуется повторная аутентификация.
- Токены, выданные отказавшим узлом, на другом узле принимаются только при заданном
DB_URL; при этом данные пользователя в памяти другого узла отсутствуют, и запросы/oauth2/userinfoи выдачаid_tokenзавершаются ошибкой до повторной аутентификации. - После перезапуска узла его память пуста: ранее выданные узлом коды и токены без
DB_URLнедействительны.
Для планового обслуживания узел переводится в режим drain: новые входы на него не направляются, а запросы уже начатых сессий продолжают обслуживаться.
echo "set server esia_nodes/node-a state drain" | socat stdio /run/haproxy/admin.sock
После обновления и проверки /health/ready узел возвращается командой set server esia_nodes/node-a state ready.
Проверка
- Получить токены и убедиться, что ответ
/oauth2/tokenсодержитnode_id, совпадающий сNODE_IDузла. - Декодировать payload токена и проверить
nid:
echo "<access_token>" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null
- Повторить
GET /oauth2/userinfoс токеном: в журнале HAProxy значение захваченногоnidсоответствует выбранному серверу. - Остановить узел
node-a: запрос с токеном этого узла передаётся наnode-b, а/health/readyна остановленном узле возвращает ошибку; вход пользователя выполняется заново наnode-b.