Аутентификация JWT (JSON Web Token)
Настройка и использование аутентификации с токеном.
Введение
JWT (JSON Web Token) — это стандарт для обеспечения безопасности и контекста аутентификации в веб-приложениях, где фронтенд отделён от бэкенда.
Обычно используется, когда фронтенд веб-приложения разработан на ReactJS, AngularJS, VueJS и т. д., а бэкенд предоставляет API REST для передачи и получения динамических данных приложения.
Токен (JWT) — это безопасный код, идентифицирующий пользователя.
С JWT (JSON Web Token) API REST бэкенда позволяет приватным сервисам получать контекст аутентификации, чтобы знать, кто является аутентифицированным пользователем, то есть по токену можно определить, какой это пользователь, причём токен в данном случае — JWT (JSON Web Token).

CORS
API REST может находиться в другом поддомене, чем веб-сайт, что требует настройки CORS (Cross-Origin Resource Sharing), чтобы к сервисам можно было обращаться.
Поэтому если API REST находится в другом домене/поддомене, чем фронтенд, то даже при использовании JWT необходимо правильно настроить CORS.
CORS — это политика безопасности при обращении к ресурсам в разных доменах, поддоменах или портах. Она гарантирует, что один адрес действительно может взаимодействовать с другим адресом.
Это происходит потому, что браузеры блокируют использование сервисов или других типов ресурсов по внешним адресам, чтобы предотвратить атаки с целью получения конфиденциальных данных.
Активация и настройка
Чтобы активировать и настроить JWT в вашем приложении в Netuno, необходимо отредактировать файл конфигурации приложения, соответствующий используемой среде, например:
config/_development.jsonconfig/_production.json
Зависит от среды, настроенной в Netuno; в файле
netuno/config.jsсмотрите параметризациюconfig.env.
Вставьте и настройте следующие параметры:
...
"auth": {
"jwt": {
"enabled": true,
"secret": "СекретДолженИметь32Символа!!!",
"expires": {
"access": 1440,
"refresh": 1440
}
}
},
...
В параметре secret укажите сложную и случайную последовательность символов — это ключ, который обеспечит безопасность шифрования JWT.
Параметры expires задаются в минутах, например: 60 соответствует 1 часу, а 1440 — одному дню.
Ограниченный доступ для групп
Дополнительно можно передать параметр groups, который определяет код групп пользователей, которые могут аутентифицироваться с JWT, например:
...
"auth": {
"jwt": {
...
"groups": ["klient", "postavshchik"],
...
}
},
...
В приведённом выше примере только пользователи, принадлежащие к группам:
- Клиенты, код группы —
klient. - Поставщики, код группы —
postavshchik.
Только пользователи из этих групп могут аутентифицироваться с JWT.
Как получить Access Token
Чтобы получить Access Token JWT, Netuno предоставляет сервис _auth, который проверяет аутентификацию и, если она успешна, возвращает Access Token и Refresh Token.
Access Token получают следующим образом:
Fetch - чистый JavaScript
JavaScript в браузерах предоставляет fetch для интеграции с сервисами API REST бэкенда.
Ниже приведены некоторые примеры выполнения аутентификации с JWT и обновления токена.
Аутентификация
Пример получения Access Token с помощью fetch:
fetch("http://localhost:9000/services/_auth", {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
username: payload.username,
password: payload.password,
jwt: true
})
}).then((response) => {
if (response.status != 200) {
if (response.status != 403) {
console.log('Аутентификация завершилась неудачно с неожиданным статусом:', response.status);
}
alert('Неверные данные для входа.');
return null;
}
return response.json();
}).then((res) => {
if (res && res.result === true) {
console.log(`Мой Access Token: ${res.access_token}`);
console.log(`Мой Refresh Token: ${res.refresh_token}`);
console.log(`Истекает через: ${res.expires_in} минут`);
sessionStorage.setItem('token', JSON.stringify(res));
}
}).catch((error)=> {
console.log(error);
alert('Произошла техническая проблема, попробуйте позже.');
});
Обратите внимание, что token должен храниться как сессия, поэтому для большей безопасности предпочтительнее использовать sessionStorage, а не localStorage.
С помощью refresh_token можно создать новый token до истечения срока действия (expires_in в минутах).
Использование Access Token в других сервисах
Чтобы выполнять пользовательские сервисы, требующие предварительной аутентификации в приложениях Netuno, необходимо передать Access Token в заголовке HTTP-протокола как значение ключа Authorization, например:
Authorization: Bearer eyJhbGciOiJIUzU...
Пример выполнения пользовательского сервиса с фронтенда с помощью fetch, где Authorization передаётся в headers:
const token = JSON.parse(sessionStorage.getItem("token"));
fetch("http://localhost:9000/services/moy-servis", {
method: 'post',
credentials: 'include',
headers: {
'Authorization': `${token.token_type} ${token.access_token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
moyParametr1: '...',
moyParametr2: '...'
})
}).then((response) => {
if (response.status != 200) {
console.log(`Сервис завершился неудачно со статусом ${response.status}.`);
if (response.status == 403) {
alert('Не аутентифицирован.');
} else {
alert('Произошла ошибка в сервисе.');
}
return null;
}
return response.json();
}).then((res) => {
if (res) {
console.log(`Данные ответа моего сервиса:`, res);
alert('Сервис выполнен успешно.');
}
}).catch((error)=> {
console.log(error);
alert('Произошла техническая проблема, попробуйте позже.');
});
Обратите внимание, что в HTTP-заголовках в параметре Authorization используются token_type и access_token, полученные из объекта JWT, ранее сохранённого при аутентификации в sessionStorage.
Обновление с помощью Refresh Token
Чтобы обновить токен и продлить аутентификацию, нужно снова вызвать сервис _auth, но помимо параметра jwt: true также необходимо передать параметр refresh_token со значением, полученным при предыдущей успешной аутентификации.
Обновление токена должно выполняться до истечения срока действия (expires_in в минутах).
Итак, чтобы продлить аутентификацию и обновить токен, смотрите пример:
const token = JSON.parse(sessionStorage.getItem("token"));
fetch("http://localhost:9000/services/_auth", {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
refresh_token: token.refresh_token,
jwt: true
})
}).then((response) => {
if (response.status != 200) {
if (response.status != 403) {
console.log('Обновление токена завершилось неудачно с неожиданным статусом:', response.status);
}
alert('Обновление доступа с недействительным токеном.');
return null;
}
return response.json();
}).then((res) => {
if (res && res.result === true) {
console.log(`Мой новый Access Token: ${res.access_token}`);
console.log(`Мой новый Refresh Token: ${res.refresh_token}`);
console.log(`Истекает через: ${res.expires_in} минут`);
sessionStorage.setItem("token", JSON.stringify(res));
}
}).catch((error)=> {
console.log(error);
alert('Произошла техническая проблема, попробуйте позже.');
});
После получения нового токена следует использовать его вместо предыдущего (старого) в последующих вызовах сервисов.
Таким образом, аутентификация продлевается за счёт последовательных обновлений токена — это способ гарантировать, что пользователь остаётся аутентифицированным.
Auth Client - NPM
Для удобной интеграции с фронтендом предоставляется модуль NPM:
На фронтенде можно установить его с помощью Bun:
bun install @netuno/auth-client
Можно использовать другой менеджер зависимостей фронтенда, например PNPM, NPM, YARN или другой.
Настройка
Этот модуль зависит от @netuno/service-client; при указании адреса сервисов, например:
import _service from '@netuno/service-client';
...
_service.config({
prefix: 'http://localhost:9000/services/'
});
auth-client использует префикс service-client для внутреннего построения адреса сервиса _auth; в данном случае, следуя примеру, это будет:
http://localhost:9000/services/_auth
Таким образом, этот адрес автоматически формируется с использованием префикса адреса сервисов API REST приложения Netuno.
Посмотрите, как настроить параметры auth-client:
В версии ES module config() является асинхронной. Дождитесь её завершения перед использованием настроенного клиента:
import _auth from '@netuno/auth-client';
await _auth.config({
storage: 'local', // session — значение по умолчанию.
onLogin: () => { alert("Вход выполнен!"); },
onLogout: () => { alert("Выход выполнен!"); }
});
Если вы хотите, чтобы токен сохранялся в localStorage, то в настройках нужно указать storage: 'local', поскольку по умолчанию используется storage: 'session' с sessionStorage.
Аутентификация
Чтобы выполнить вход с помощью _auth:
import _auth from '@netuno/auth-client';
...
_auth.login({
username: "admin",
password: "секрет",
success: ()=> {
alert("Успешно.");
},
fail: ()=> {
alert("Ошибка.");
}
});
Токен сохраняется автоматически в session (sessionStorage) или local (localStorage) в зависимости от настроек; по умолчанию используется session.
Обновление токена
Пример обновления токена:
import _auth from '@netuno/auth-client';
...
_auth.refreshToken({
success: ()=> {
alert("Успешно.");
},
fail: ()=> {
alert("Ошибка.");
}
});
Токен сохраняется автоматически в session (sessionStorage) или local (localStorage) в зависимости от настроек; по умолчанию используется session.
Удаление токена
Пример завершения сессии, то есть процесса выхода и удаления токена:
import _auth from '@netuno/auth-client';
...
_auth.logout();
Токен удаляется из sessionStorage или localStorage в зависимости от настроек; по умолчанию используется sessionStorage.
Backend - сервисы API REST
Если при интеграции с сервисами передаётся заголовок Authorization с действительным токеном, то можно определить пользователя и группу.
В бэкенде можно использовать ресурс _user и _group, которые идентифицируют пользователя и группу соответственно.
Чтобы увидеть данные аутентифицированного пользователя, используем ресурс User, например:
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_log.info(
"Аутентифицированный пользователь:",
_val.map()
.set("id", _user.id())
.set("имя", _user.name())
.set("код", _user.code())
.set("все", _user.data())
)
_log.info(
"Аутентифицированный пользователь:",
_val.map()
.set("id", _user.id())
.set("имя", _user.name())
.set("код", _user.code())
.set("все", _user.data())
)
_log.info(
"Аутентифицированный пользователь:",
_val.map()
.set("id", _user.id())
.set("имя", _user.name())
.set("код", _user.code())
.set("все", _user.data())
)
_log.info(
"Аутентифицированный пользователь:",
_val.map()
.set("id", _user.id())
.set("имя", _user.name())
.set("код", _user.code())
.set("все", _user.data())
)
_log.info(
"Аутентифицированный пользователь:",
_val.map()
.set("id", _user.id())
.set("имя", _user.name())
.set("код", _user.code())
.set("все", _user.data())
)
_log.info(
"Аутентифицированный пользователь:",
_val.map()
.set("id", _user.id())
.set("имя", _user.name())
.set("код", _user.code())
.set("все", _user.data())
)
Чтобы увидеть группу аутентифицированного пользователя, используем ресурс Group, например:
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_log.info(
"Аутентифицированная группа:",
_val.map()
.set("id", _group.id())
.set("имя", _group.name())
.set("код", _group.code())
.set("все", _group.data())
)
_log.info(
"Аутентифицированная группа:",
_val.map()
.set("id", _group.id())
.set("имя", _group.name())
.set("код", _group.code())
.set("все", _group.data())
)
_log.info(
"Аутентифицированная группа:",
_val.map()
.set("id", _group.id())
.set("имя", _group.name())
.set("код", _group.code())
.set("все", _group.data())
)
_log.info(
"Аутентифицированная группа:",
_val.map()
.set("id", _group.id())
.set("имя", _group.name())
.set("код", _group.code())
.set("все", _group.data())
)
_log.info(
"Аутентифицированная группа:",
_val.map()
.set("id", _group.id())
.set("имя", _group.name())
.set("код", _group.code())
.set("все", _group.data())
)
_log.info(
"Аутентифицированная группа:",
_val.map()
.set("id", _group.id())
.set("имя", _group.name())
.set("код", _group.code())
.set("все", _group.data())
)
Безопасность
Если токен недействителен или Authorization не передан, Netuno гарантирует, что сервис не будет выполнен, кроме случаев, когда сервис настроен как публичный; по умолчанию сервисы являются приватными.
Чтобы управлять тем, является ли сервис публичным или приватным, настройте в вашем приложении:
server/core/_service_config.js
При этом в режиме разработки Netuno позволяет выполнять приватные сервисы напрямую без аутентификации для удобства, поскольку большинству пользовательских сервисов обычно не нужен контекст аутентификации, поэтому в целом разработка и тестирование становятся проще.
Если это не режим разработки, например в продакшене, то сервисы, очевидно, могут выполняться только с аутентификацией.
Ресурс аутентификации
Обычно ресурсов _user и _group достаточно, но бывают ситуации, когда нужно выполнять более сложные операции, и в этом случае есть ресурс _auth.
Ресурс _auth — это основной ресурс аутентификации; он управляет настройками и содержит все операции, связанные с процессом аутентификации.
Чтобы узнать больше, смотрите документацию ресурса Auth.
ReAuthKit
Netuno предоставляет базовый проект для создания платформ с онлайн-аутентификацией, который включает создание учётной записи, вход, профиль, аватар, восстановление пароля и многое другое.
Посмотрите на GitHub:
ReAuthKit — это приложение Netuno с фронтендом на React, Ant.Design и React Router, а база данных — PostgreSQL.
С ReAuthKit создание новых проектов с аутентификацией значительно ускоряется, поскольку уже есть все необходимые механизмы для любого проекта, требующего аутентификации.
Заключение
Всё просто: можно настроить и активировать аутентификацию с JWT (JSON Web Token) в приложениях.
Используйте аутентификацию с JWT для разработки безопасных сервисов в API REST.
Интеграцию JWT на фронтенде можно выполнять напрямую с помощью fetch.
Чтобы ускорить и стандартизировать реализацию, предоставляется модуль NPM @netuno/auth-client, который можно использовать с любой технологией фронтенда.
Всегда, когда требуется идентифицировать пользователя при выполнении сервисов API REST, используйте аутентификацию с JWT.
В бэкенде настройка и получение аутентифицированного пользователя очень просты.