Вопрос или проблема
Случайные выходы из системы при работе приложения Node.js за обратным прокси NGINX
У меня есть приложение next.js, работающее на сервере (в данный момент я не запускаю приложение node в docker, так как это нарушает ожидаемое поведение аутентификации на основе ролей, это отдельная тема для такой проблемы). Оно работает на “голом” сервере на порту 3001.
У меня также имеется сервер аутентификации keycloak, работающий внутри контейнера docker на том же компьютере на порту 8080.
Приложение находится за обратным прокси на (фейковое имя хоста здесь)
subdomain.domain.io
А keycloak работает на
subdomain.domain.io/authorisation
Когда я запускаю моё приложение node на локальном компьютере по адресу localhost:3001
и настраиваю keycloak.json для использования сервера аутентификации subdomain.domain.io/authorisation
, всё работает, как и ожидалось.
Когда я разворачиваю приложение node на удаленном компьютере (внутри или снаружи docker) за обратным прокси, меня случайным образом выкидывает из системы при определенных переходах по страницам, как будто сессия / куки теряются за nginx.
На протяжении нескольких недель я застрял с этой проблемой… В журналах ключа, nginx или самого приложения нет никаких примечательных записей, когда происходит выход из системы.
Вот конфигурация keycloak в контейнере docker
keycloak:
# restart: always
container_name: "keycloak-server"
image: keycloak/keycloak:latest
ports:
- "8080:8080"
- "8443:8443"
environment:
TZ: "Europe/London"
#~~~~~~~~~~~~~~~~~# Настройки пользователя
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin}
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-password}
KC_LOG_LEVEL: info
KC_HEALTH_ENABLED: true
#~~~~~~~~~~~~~~~~~#
KC_HOSTNAME: "https://subdomain.domain.io/authorisation"
KC_HOSTNAME_ADMIN: "https://admin.subdomain.domain.io/authorisation"
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: false # необходимо для связи других контейнеров с ключом на стороне сервера
KC_HTTP_ENABLED: true ## предполагается, что это крайний сервер, используемый между nginx и kc
KC_HOSTNAME_DEBUG: true
#~~~~~~~~~~~~~~~~~#
KC_PROXY_HEADERS: xforwarded ## включает парсинг нестандартных заголовков X-Forwarded-*, таких как X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host и X-Forwarded-Port`
KC_DB: postgres
KC_DB_USERNAME: postgres
KC_DB_PASSWORD: ${POSTGRES_PASSWORD:-password}
KC_DB_POOL_MAX_SIZE: 50
KC_DB_URL_HOST: postgres
command: start --import-realm # можно добавить --optimized, если сборка уже произошла
volumes:
- ./realm:/opt/keycloak/data/import:ro # область для импорта
depends_on:
- postgres
networks:
- auth-network
Настройки клиента в административной консоли имеют включенные только стандартный поток и прямые доступные гранты. Я думаю, что все настройки доступа правильно позволяют нужный источник и URL-адреса перенаправления.
Вот код server.js, где я думаю, может быть проблема, я следовал документации keycloak для nodejs при написании этого кода
app.prepare().then(() => {
const server = express();
if (keycloakEnabled) { // выполнять аутентификацию keycloak, если переменная окружения установлена
console.log('следующие страницы требуют аутентификации keycloak', process.env.PROTECTED_PAGES ? colourYellow : colourRed, process.env.PROTECTED_PAGES, colourReset)
console.log('следующие страницы требуют роль', process.env.ROLE ? colourYellow : colourRed, process.env.ROLE, colourReset, 'роль: ', process.env.ROLE_PROTECTED_PAGES ? colourYellow : colourRed, process.env.ROLE_PROTECTED_PAGES, colourReset)
server.set('trust proxy', true); // IP-адрес клиента понимается как самый левый элемент в заголовке X-Forwarded-For.
if (!dev) {
let redisClient;
console.log(`режим разработки:`, colourGreen, dev, colourReset, `-> подключение к redis session store на`, colourGreen, `${redisHost}:${redisPort}`, colourReset);
try {
redisClient = createClient({
socket: {
host: redisHost,
port: redisPort
}
});
} catch (error) {
console.log('Ошибка при создании клиента Redis, убедитесь, что Redis работает и хост указан как переменная окружения, если это визуальное приложение находится в контейнере Docker');
console.error(error);
}
redisClient.connect().catch('Ошибка при создании клиента Redis, убедитесь, что Redis работает и хост указан как переменная окружения, если это визуальное приложение находится в контейнере Docker', console.error);
store = new RedisStore({
client: redisClient,
prefix: "redis",
ttl: undefined,
});
} else {
store = new MemoryStore(); // использовать хранилище в памяти для данных сессии в режиме разработки
console.log(`режим разработки:`, dev ? colourYellow : colourRed, dev, colourReset, `-> использование хранилища сессии в памяти (express-session MemoryStore())`);
}
server.use(
session({
secret: 'login',
resave: false,
saveUninitialized: true,
store: store,
// cookie: {
// secure: !dev, // установить в true, если используется https
// maxAge: 24 * 60 * 60 * 1000, // 1 день
// sameSite: 'lax', // настраивайте по мере необходимости
// domain: 'bnl.theworldavatar.io' // убедитесь, что это соответствует вашему домену
// }
})
);
const keycloak = new Keycloak({ store: store });
server.use(keycloak.middleware());
server.get('/api/userinfo', keycloak.protect(), (req, res) => {
const { preferred_username: userName, given_name: firstName, family_name: lastName, name: fullName, realm_access: { roles }, resource_access: clientRoles } = req.kauth.grant.access_token.content;
res.json({ userName, firstName, lastName, fullName, roles, clientRoles });
});
server.get('/logout', (req, res) => {
req.logout(); // Выход по адаптеру Keycloak
req.session.destroy(() => { // Это разрушает сессию
res.clearCookie('connect.sid', { path: "https://stackoverflow.com/" }); // Очистить куку сессии
});
});
const protectedPages = process.env.PROTECTED_PAGES.split(',');
protectedPages.forEach(page => {
server.get(page, keycloak.protect());
});
const roleProtectedPages = process.env.ROLE_PROTECTED_PAGES.split(',');
roleProtectedPages.forEach(page => {
server.get(page, keycloak.protect(process.env.ROLE));
console.log('защита страницы', page, 'с ролью', process.env.ROLE);
});
}
и keycloak.json в папке проекта node
{
"realm": "REALM",
"auth-server-url": "https://subdomain.domain.io/authorisation",
"ssl-required": "external",
"resource": "job-portal",
"public-client": true,
"use-resource-role-mappings": true,
"confidential-port": 0
}
Если проблема не в этом, то, возможно, проблема может быть в конфигурации nginx (это конфигурация для подпроекта
###########################################################################
## --------------------------------------------------------------------- ##
## --------------------------------- APP ------------------------------- ##
## --------------------------------------------------------------------- ##
###########################################################################
location / {
proxy_pass http://192.168.1.xxx:3001/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Server $http_host;
proxy_set_header X-Forwarded-Proto https;
}
########################### keycloak
location /authorisation/realms/ {
proxy_pass http://192.168.1.xxx:8080/realms/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /authorisation/resources/ {
proxy_pass http://192.168.1.xxx:8080/resources/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /authorisation/robots.txt {
proxy_pass http://192.168.1.xxx:8080/robots.txt/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-Proto $scheme;
}
Извините за длинный пост, но я совершенно не понимаю, почему мои пользователи продолжают выходить из системы. Я застрял с этой проблемой на протяжении нескольких недель и был бы очень благодарен за любую помощь!
Ответ или решение
Судя по вашему описанию, проблема с неожиданными выходами из системы в вашем приложении на Node.js, работающем за обратным прокси NGINX, может возникать по нескольким причинам. Разберем основные аспекты, которые могут вызывать эту проблему, и предложим решения.
Возможные причины и решения:
-
Настройки куки иSameSite атрибуты:
Убедитесь, что куки для сессий правильно настроены. При работе с обратным прокси важно установить параметры для кукиSameSite
иsecure
.Вы можете протестировать настройки, раскомментировав блок
cookie
в вашем коде:cookie: { secure: true, // Убедитесь, что это true, если у вас включен HTTPS maxAge: 24 * 60 * 60 * 1000, // 1 день sameSite: 'None', // или 'Lax', в зависимости от вашей конфигурации domain: 'subdomain.domain.io' // Убедитесь, что это соответствует вашему домену }
Установка
sameSite: 'None'
может помочь, если вы находитесь за обратным прокси. -
Прокси-заголовки:
Убедитесь, что в NGINX правильно настроены заголовки X-Forwarded:proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host;
Убедитесь, что ваши настройки соответствуют тому, как Keycloak ожидает обращаться к вашему приложению.
-
Перепроверьте настройки Keycloak:
Вkeycloak.json
убедитесь, что URL для аутентификации и конфигурации клиента совпадают с тем, как они настроены в NGINX:"auth-server-url": "https://subdomain.domain.io/authorisation",
Также убедитесь, что перенаправления из клиентских приложений настроены правильно в консоли управления Keycloak, и они не содержат лишних символов или пробелов.
-
Проверка настроек NGINX:
Ваша конфигурация NGINX выглядит в целом корректной, но могло бы помочь следующее:- Настройте кэширование, чтобы NGINX не кэшировал запросы сессий или пользователями. Это может привести к тому, что NGINX будет кэшировать состояние аутентификации.
- Включите журналирование, чтобы понять, какие запросы приходят на сервер, и возможно, какие заголовки приходят и уходят.
-
Валидация сеансов:
Здесь стоит дополнительно проверить, как хранятся сессии. Используйте Redis для более стабильного хранения сессий (как указано в вашем коде). Убедитесь, что Redis работает должным образом и соединение установлено без ошибок. - Логи и отладка:
Добавьте больше инструментов для логирования и отладки в ваше приложение. Это поможет вам получить больше информации о том, что происходит перед тем, как сессия завершается. Вы можете попробовать использовать библиотеки, такие какmorgan
илиwinston
, для получения подробных логов.
Подводя итог:
Проблема с неожиданными выходами из системы в вашем Node.js приложении за NGINX может быть связана с настройками куки, параметрами прокси и обработкой сессий. Проверка каждого из этих аспектов и внесение необходимых правок может помочь решить проблему. Следует также учитывать возможность использования инструментов отладки для улучшения видимости и анализа происходящих процессов.
Если ни одно из предложенных решений не помогает, может быть целесообразным создать минимальный пример вашего приложения и конфигурации, который воспроизводит проблему, для дальнейшего изучения и диагностики.