Как на сервере хранить токен стороннего сервиса?
Коллеги, подскажите, как в бэкенде хранить токен? У меня spring boot приложение пользуется сервисом с курсами валют, от которого был получен токен. Какие стандартные практики хранения здесь, если требуется безопасность?
Отслеживать
задан 22 янв 2023 в 7:07
313 1 1 серебряный знак 7 7 бронзовых знаков
2 ответа 2
Сортировка: Сброс на вариант по умолчанию
рекомендую хранить токен в безопасном месте на стороне сервера, используя шифрование и хранение его в базе данных. Это позволяет вам контролировать доступ к токену и защитить его от несанкционированного доступа.
import org.springframework.security.crypto.bcrypt.BCrypt; import org.springframework.security.crypto.encrypt.Encryptors; import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.security.crypto.keygen.KeyGenerators; // создание соли String salt = KeyGenerators.string().generateKey(); // шифрование токена TextEncryptor encryptor = Encryptors.text("password", salt); String tokenEncrypted = encryptor.encrypt(token); // сохранение токена в базе данных tokenRepository.save(new Token(tokenEncrypted, salt)); // при извлечении токена из базы данных Token tokenFromDb = tokenRepository.findById(id); String salt = tokenFromDb.getSalt(); // дешифровка токена TextEncryptor decryptor = Encryptors.text("password", salt); String tokenDecrypted = decryptor.decrypt(tokenFromDb.getTokenEncrypted());
Отслеживать
ответ дан 22 янв 2023 в 9:47
versetty777 versetty777
1,026 1 1 золотой знак 2 2 серебряных знака 12 12 бронзовых знаков
зачем все эти танцы с бубном? какой смысл хранить токен в базе данных, заморачиваясь с криптованием? токен — не пароль, он не действует условно бесконечно. его время жизни ограничено. кроме того, даже если что-то произойдет (например, перезапуск сервера) у вас всегда есть механизм авторизации, позволяющий получить новый токен. а вы тратите системные ресурсы на обращение к баще и на расшифровку токена каждый раз, когда он необходим (а это с высокой вероятностью часто).
22 янв 2023 в 12:17
У вас под капотом спринг. Посему создайте простой интерфейс, имеющий методы:
public String authorize(); public String getToken();
Для каждого внешнего ресурса, который требует авторизации, вы пишите отдельный сервис, который имплементирует данный интерфейс. Переопределяя данный методы, вы пишите логику авторизации на данном ресурсе (получение токена) для метода public String authorize(). В данном сервисе заводите потокобезопасную переменную , к примеру, таким образом:
AtomicReference token = new AtomicReference();
Теперь переопределяете метод String getToken(), возвращая токен, спрятанный в AtomicReference. кроме того, для предыдущего метода String authorize() перед возвращением полученного при авторизации токена, вы пишите его сюда (в AtomicReference).
Здесь Можно создавать пару для токена и рефреш токена, можно коллекцию, в т.ч. на уровне супер класса (все зависит от вашей задачи)
Данный сервис добавляете на уровень отправки запроса (это может быть отдельный бин ресттемплейта, дополнительно сконфигурированный на добавление хедера авторизации, или что-либо другое), где отлавливаете исключения авторизации , связанные с истечением срока службы токена. Теперь при отправке каждого запроса (на уровне конфигурации ресттемплейта) вы дергаете метод getToken(), а при исключении авторизации — метод authorize() и повторяете запрос с новым токеном.
Все, теперь все запросы будут авторизироваться автоматически, подставляя и обновляя нужный токен
Добавляем токены для обращения к сервисам

Голосовые роботы могут обращаться в различные сервисы для обмена данными. При этом для аутентификации запросов голосовому роботу необходимы токены.
Для повышения безопасности токены хранятся и используются в роботах в зашифрованном виде. Токены хранятся в глобальных переменных, которые передаются в соответствующих блоках и сервисах. Например, в качестве параметров запросов сервиса http , если вы разрабатываете сценарий в Code.
Добавлять и изменять токены могут только администраторы проекта. Пользователи могут просматривать список токенов голосового робота и копировать переменные, в которых хранятся токены.
Вы можете использовать собственные токены для хранения сертификатов и ключей, которые используются при выполнении http-запросов (блок HTTP-запрос или сервис http ).
Системные токены добавляются автоматически при подключении соответствующих сервисов.
Добавление собственных токенов
Используйте переменные с токенами в параметрах запросов и заголовках в блоке HTTP-запрос.
Чтобы добавить собственный токен:
- Откройте проект и перейдите в раздел Токены:
- Если вы разрабатываете сценарий в Graph для SaluteBot Voice выберите Разработка → Токены.
- Если вы разрабатываете сценарий в Code для SaluteBot Voice выберите Разработка → Редактор → Токены.
- Выберите вкладку Мои токены.
- Нажмите кнопку Добавить.
- В окне Добавить токен заполните следующие поля:
- В поле Имя переменной укажите имя переменной, в которой будет храниться токен. Переменную можно использовать в любом месте сценария. Максимальная длина имени переменной — 40 символов. Имя может содержать только цифры от 0 до 9, латинские буквы верхнего и нижнего регистров (Aa-Zz), и нижнее подчеркивание _ .
Имя переменной должно быть уникальным.
- В поле Значение укажите необходимый токен. Максимальная длина токена — 1024 символа.
- В поле Описание можно указать дополнительную информацию о токене.
Токен будет добавлен в список токенов голосового робота.
В собственных токенах можно сохранять app_key и app_secret необходимые для аутентификации запросов к проектам Platform V, созданным в других пространствах.
Изменить имя переменной и описание токена можно в окне Просмотр токена, которое открывается при выборе токена в списке. Значения добавленных токенов изменять нельзя.
Просмотр системных токенов
Чтобы просмотреть системные токены:
- Откройте проект и перейдите в раздел Токены:
- Если вы разрабатываете сценарий в Graph для SaluteBot Voice выберите Разработка → Токены.
- Если вы разрабатываете сценарий в Code для SaluteBot Voice выберите Разработка → Редактор → Токены.
- Выберите вкладку Системные токены.
Откроется список системных токенов, сгруппированных по типу сервиса. Системные токены нельзя удалить.
ПАО Сбербанк использует cookie для персонализации сервисов и удобства пользователей.
Вы можете запретить сохранение cookie в настройках своего браузера.
Локальное хранилище или куки? Безопасное хранение JWT на клиенте
JWT (JSON Web Token) — это замечательный стандарт, основанный на формате JSON, позволяющий создавать токены доступа, обычно используемые для аутентификации в клиент-серверных приложениях. При использовании этих токенов возникает вопрос о том, как безопасно хранить их во фронтенд-части приложения. Этот вопрос нужно решить сразу же после того, как токен сгенерирован на сервере и передан клиентской части приложения.

Материал, перевод которого мы сегодня публикуем, посвящён разбору плюсов и минусов использования локального хранилища браузера ( localStorage ) и куки-файлов для хранения JWT.
Виды токенов
- Токены доступа (access tokens) обычно представляют собой короткоживущие JWT, подписанные сервером. Они включаются в каждый HTTP-запрос, выполняемый клиентом к серверу. Токены используются для авторизации запросов.
- Токены обновления (refresh tokens) обычно представлены долгоживущими токенами, хранящимися в базе данных и используемыми для получения нового токена доступа при истечении срока действия предыдущего токена.
Где именно следует хранить токены на клиенте?
Существует 2 распространённых способа хранения токенов на клиенте: локальное хранилище браузера и куки-файлы. О том, какой способ лучше, много спорят. Большинство людей склоняется в сторону куки-файлов из-за их лучшей защищённости.
Давайте сравним локальное хранилище и куки-файлы. Наше сравнение основано, преимущественно, на этом материале и на комментариях к нему.
Локальное хранилище
▍Преимущества
Основное преимущество локального хранилища заключается в том, что им удобно пользоваться.
- Работа с локальным хранилищем организована очень удобно, тут используется чистый JavaScript. Если у вашего приложения нет бэкенда, и вы полагаетесь на чужие API, не всегда можно запросить у этих API установку особых куки-файлов для вашего сайта.
- Используя локальное хранилище, удобно работать с API, которые требуют размещать токен доступа в заголовок запроса. Например — так: Authorization Bearer $ .
▍Недостатки
Главный недостаток локального хранилища — это его уязвимость к XSS-атакам.
- При выполнении XSS-атаки злоумышленник может запустить свой JavaScript-код на вашем сайте. Это означает, что атакующий может получить доступ к токену доступа, сохранённому в localStorage .
- Источником XSS-атаки может быть сторонний JavaScript-код, включённый в состав вашего сайта. Это может быть что-то вроде React, Vue, jQuery, скрипта Google Analytics и так далее. В современных условиях почти невозможно разработать сайт, в состав которого не входят библиотеки сторонних разработчиков.
Куки-файлы
▍Преимущества
Главное преимущество куки-файлов заключается в том, что они недоступны из JavaScript. В результате они не так уязвимы к XSS-атакам, как локальное хранилище.
- Если вы используете флаг HttpOnly и защищённые куки-файлы, это означает, что из JavaScript нельзя получить доступ к этим файлам. То есть, если даже атакующий сможет запустить свой код на вашей странице, ему не удастся прочитать токен доступа из куки-файла.
- Куки автоматически отправляются в каждом HTTP-запросе к серверу.
▍Недостатки
В зависимости от конкретных обстоятельств может случиться так, что токены в куки-файлах сохранить не удастся.
- Размер куки-файлов ограничен 4 Кб. Поэтому, если вы используете большие JWT, хранение их в куки-файлах вам не подойдёт.
- Существуют сценарии, при реализации которых вы не можете передавать куки своему API-серверу. Возможно и то, что какой-то API требует размещения токена в заголовке Authorization . В таком случае вы не сможете хранить токены в куки-файлах.
XSS-атаки
Локальное хранилище уязвимо к XSS-атакам из-за того, что с ним очень легко работать, используя JavaScript. Поэтому злоумышленник может получить доступ к токену и воспользоваться им в своих интересах. Однако, хотя HttpOnly-куки и недостижимы из JavaScript, это не означает, что вы, используя куки, защищены от XSS-атак, направленных на кражу токена доступа.
Если атакующий может запускать свой JS-код в вашем приложении, это значит, что он может просто отправить вашему серверу запрос, а токен будет включён в этот запрос автоматически. Такая схема работы просто не так удобна для атакующего, так как он не может прочитать содержимое токена. Но подобное нужно атакующим нечасто. Кроме того, при такой схеме работы злоумышленнику может быть выгоднее атаковать сервер, пользуясь компьютером жертвы, а не собственным.
Куки-файлы и CSRF-атаки
CSRF-атаки — это атаки, в ходе которых пользователя каким-то образом принуждают к выполнению особого запроса. Например, сайт принимает запросы на изменение адреса электронной почты:
POST /email/change HTTP/1.1 Host: site.com Content-Type: application/x-www-form-urlencoded Content-Length: 50 Cookie: session=abcdefghijklmnopqrstu email=myemail.example.com
В такой ситуации атакующий может создать форму со скрытым полем для ввода адреса электронной почты, которая отправляет POST-запрос на https://site.com/email/change . При этом сессионные куки автоматически будут включены в такой запрос.
Правда, от этой угрозы можно легко защититься, использовав атрибут SameSite в заголовке ответа и анти-CSRF токены.
Промежуточные итоги
Хотя и куки-файлы не отличаются полной неуязвимостью к атакам, для хранения токенов лучше всего, всегда, когда это возможно, выбирать именно их, а не localStorage . Почему?
- И локальное хранилище, и куки уязвимы к XSS-атакам, но злоумышленнику будет сложнее совершить атаку в том случае, если используются HttpOnly-куки.
- Куки уязвимы к CSRF-атакам, но риск таких атак можно смягчить, используя атрибут SameSite и анти-CSRF токены.
Использование куки-файлов для хранения токенов OAuth 2.0
Давайте кратко перечислим способы хранения токенов:
- Способ 1: хранение токенов в локальном хранилище. Этот способ подвержен XSS-атакам.
- Способ 2: хранение токенов в HttpOnly-куки. Этот способ подвержен CSRF-атакам, но риск подобных атак может быть смягчён. От XSS-атак этот вариант хранения токенов защищён немного лучше первого.
- Способ 3: хранение токенов обновления в HttpOnly-куки, а токенов доступа — в памяти. Этот способ хранения токенов безопаснее в плане CSRF-атак и немного лучше защищён от XSS-атак.
Почему хранение токена обновления в HttpOnly-куки безопаснее с точки зрения CSRF-атак?
Злоумышленник может создать форму, которая обращается к /refresh_token . В ответ на этот запрос возвращается новый токен доступа. Но атакующий не может прочитать ответ в том случае, если он использует HTML-форму. Для того чтобы не дать атакующему успешно выполнять fetch- или AJAX-запросы и читать ответы, нужно, чтобы CORS-политика сервера авторизации была бы настроена правильно, а именно — так, чтобы сервер не реагировал бы на запросы от неавторизованных веб-сайтов.
Как всё это настроить?
Шаг 1: возврат токена доступа и токена обновления при аутентификации пользователя
После того, как пользователь аутентифицируется, сервер аутентификации возвращает access_token (токен доступа) и refresh_token (токен обновления). Токен доступа будет включён в тело ответа, а токен обновления — в куки.
Вот что нужно использовать для настройки куки-файлов, предназначенных для хранения токенов обновления:
- Флаг HttpOnly — чтобы не дать прочесть токен из JavaScript.
- Флаг secure=true , что приведёт к тому, что данные будут передаваться только по HTTPS.
- Флаг SameSite=strict нужно использовать всегда, когда это возможно, что позволит защититься от CSRF-атак. Этот подход может использоваться только в том случае, если сервер авторизации относится к тому же сайту, что и фронтенд системы. Если это не так, тогда сервер авторизации должен устанавливать CORS-заголовки на бэкенде, или использовать другие методы для того чтобы убедиться в том, что запрос с токеном обновления может быть выполнен только авторизованным веб-сайтом.
Шаг 2: сохранение токена доступа в памяти
Хранение токена доступа в памяти означает, что токен, в коде фронтенда, записывают в переменную. Это, конечно, означает, что токен будет утерян в том случае, если пользователь закроет вкладку, на которой открыт сайт, или обновит страницу. Именно поэтому у нас имеется токен обновления.
Шаг 3: получение нового токена доступа с использованием токена обновления
Если токен доступа оказывается утраченным или недействительным, нужно обратиться к конечной точке /refresh_token . При этом токен обновления, который, на шаге 1, был сохранён в куки-файле, будет включён в запрос. После этого вы получите новый токен доступа, который сможете использовать для выполнения запросов к API.
Всё это значит, что JWT могут быть больше 4 Кб, и то, что их можно помещать в заголовок Authorization .
Итоги
То, о чём мы тут рассказали, должно дать вам базовую информацию о хранении JWT на клиенте, и о том, как сделать ваш проект безопаснее.
Как вы храните JWT на клиенте?

- безопасность
- разработка
Полное руководство по управлению JWT во фронтенд-клиентах (GraphQL)
JWT (JSON Web Token, произносится как ‘jot’ [джот]) становится популярным способом управления аутентификацией. Эта статья ставит целью развенчать мифы о стандарте JWT, рассмотреть его плюсы и минусы, а также познакомиться с лучшими методиками реализации JWT на стороне клиента с учётом безопасности. Несмотря на то, что здесь мы работали над примерами с помощью клиентов GraphQL, используемые принципы применимы к любому другому фронтенд-клиенту.
Знакомство: что такое JWT?
В целях аутентификации JWT выступает в роли токена, выдаваемого сервером. Этот токен содержит относящееся к конкретному пользователю информационное наполнение в формате JSON. Клиенты могут использовать токен при взаимодействии с API (отправляя его как HTML-заголовок), чтобы API могли идентифицировать пользователя и выполнить соответствующее ему действие.
Но разве не может клиент просто создать случайное информационное наполнение и выдать себя за пользователя?
Хороший вопрос! Именно поэтому JWT также содержит подпись, которая создаётся сервером, выдавшим токен (предположим, конечной точкой вашей авторизации в системе). Любой другой сервер, получающий этот токен, может независимо проверить подпись, чтобы убедиться в подлинности информационного наполнения JSON и в том, что это наполнение было создано уполномоченным источником.
Но что если у меня есть действительный и подписанный JWT, а кто-то украдёт его из клиента? Смогут ли они постоянно использовать мой JWT?
Да. Если JWT будет украден, то вор сможет продолжать его использовать. API, принимающий JWT, выполняет самостоятельную проверку, не зависящую от его источника, следовательно сервер API никак не может знать о том, что этот токен был украден. Именно поэтому у JWT есть значение срока годности, которое намеренно создается коротким, и довольно часто его длительность определяется всего в 15 минут. Но при этом также нужно следить за тем, чтобы не произошло утечки JWT.
Из этих двух фактов формируются практически все особенности управления JWT. Т.е. мы стараемся избежать кражи, а если она всё же происходит, то нас спасает короткое время действия токенов.
Именно поэтому очень важно не хранить JWT на клиенте, например в куки или локальном хранилище. Поступая так, вы делаете своё приложение уязвимым для атак CSRF и XSS, которые при помощи вредоносных форм или скриптов могут использовать или украсть ваш токен, благоприятно размещённый в куки или локальном хранилище.
Есть ли у JWT какая-то конкретная структура?
В сериализованном виде JWT выглядит примерно так:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o
Если вы раскодируете этот base64, то получите JSON в виде 3 важных частей: заголовка, информационного наполнения и подписи.
Сериализованная форма будет иметь следующий формат:
[ base64UrlEncode(header) ] . [ base64UrlEncode(payload) ] . [signature ]
JWT не зашифрован. Он закодирован в base64 и подписан. Поэтому любой может декодировать токен и использовать его данные. Подпись JWT используется для проверки легитимности источника этих данных.
Вот упрощённая схема, демонстрирующая, как JWT выдаётся ( /login ), а затем используется для совершения вызова API к другому сервису ( /api ):
Выглядит сложновато. Почему бы мне не придерживаться старых добрых токенов сессии?
Это уже наболевшая тема интернет-форумов. Мы коротко и категорично отвечаем, что бэкенд-разработчикам нравится использовать JWT по следующим причинам:
b) отсутствие необходимости в централизованной базе данных токенов.
В настройках микросервисов каждый из них может независимо проверять действительность полученного от клиента токена. Далее он может декодировать токен и извлечь связанную с ним информацию, не нуждаясь в доступе к централизованной базе данных токенов.
Именно поэтому разработчики API ценят JWT, а мы (с клиентской стороны) должны разобраться, как его использовать. Как бы то ни было, если вы можете обойтись токеном сессии, выданным вашим любимым монолитным фреймворком, то всё у вас в порядке и нет нужды в JWT.
Вход в систему
Теперь, когда у нас есть базовое понимание JWT, давайте создадим простой процесс входа в систему и извлечём JWT. Вот что у нас должно получиться:
С чего же начать?
Процесс входа в систему принципиально не отличается от того, с которым вы сталкиваетесь регулярно. Например, вот форма авторизации, которая отправляет имя пользователя/пароль в конечную точку аутентификации и получает в ответ JWT-токен. Это может быть авторизация с помощью внешнего провайдера, шаг OAuth или OAuth2. Главное, чтобы в ответ на завершающий шаг входа в систему клиент получил JWT-токен.
Сначала мы создадим простую форму авторизации для отправки имени пользователя и пароля серверу авторизации. Сервер выдаст JWT-токен, и мы будет хранить его в памяти. В текущем руководстве мы не будем фокусироваться на сервере аутентификации в бэкенд, но вы запросто можете проверить его в примере репозитория для этой статьи.
Вот как может выглядеть обработчик handleSubmit для кнопки входа в систему:
async function handleSubmit () //.
// Вызов API login
const response = await fetch(`/auth/login`, method: 'POST',
body: JSON.stringify(< username, password >)
>)
//.
// Извлечение из ответа JWT
const < jwt_token >= await response.json()
//.
// Выполнение каких-либо действий с токеном в методе login
await login(< jwt_token >)
>
API login возвращает токен и затем передаёт его в функцию login из /utils/auth , где мы можем решить, что с ним делать.
import < login >from '../utils/auth'
await login(< jwt_token >)
Итак, мы получили токен, где же нам теперь его хранить?
Нам необходимо где-то сохранить полученный JWT-токен, чтобы иметь возможность перенаправлять его нашему API в качестве заголовка. Вы можете соблазниться мыслью поместить его в локальное хранилище (localstorage). Не стоит этого делать, т.к. оно уязвимо для XSS-атак.
Может сохранить его в куки?
Куки, созданная в клиенте для сохранения JWT, также будет уязвима для XSS. Если JWT может быть считан на клиенте из JavaScript вне вашего приложения — то может быть и украден. Вы можете подумать, что поможет куки HttpOnly (создаваемая сервером, а не клиентом), но куки уязвимы к CSRF-атакам. Важно отметить, что HttpOnly, а также чувствительные политики CORS не могут предотвратить CSRF-атаки входа, и использование куки требует надлежащей стратегии по борьбе с ними.
Обратите внимание, что новая спецификация SameSite Cookie, которая получает повышенную поддержку в большинстве браузеров, будет создавать куки на основе подходов, защищённых от CSRF-атак. Это решение может не сработать, если ваши серверы Auth и API размещены на разных доменах, но в противном случае такой вариант должен вполне подойти.
Где же тогда нам его хранить?
На данный момент мы будем хранить токен в памяти (и перейдём к постоянным сеансам в следующем разделе).
let inMemoryToken;
function login (< jwt_token, jwt_token_expiry >, noRedirect) inMemoryToken = token: jwt_token,
expiry: jwt_token_expiry
>;
if (!noRedirect) Router.push('/app')
>
>
Как вы можете видеть, здесь мы храним токен в памяти. Да, он будет обнулён, когда пользователь переключит вкладки, но мы решим этот вопрос позже. Я также объясню, почему установил флаги noRedirect и jwt_token_expiry .
Теперь, когда у нас есть токен, что нам с ним делать?
- Использовать его в нашем клиенте API для передачи в качестве заголовка каждого вызова API.
- Проверять авторизован ли пользователь, посмотрев, определена ли переменная JWT.
- По желанию мы даже можем декодировать JWT на клиенте, чтобы обратиться к данным информационного наполнения. Предположим, нам нужен id или имя пользователя на клиенте, которые мы можем извлечь из JWT.
Как нам проверить, авторизован ли пользователь?
Мы проверяем в utils/auth , установлена ли переменная токена и если нет — перенаправляем на страницу авторизации.
const jwt_token = inMemoryToken;
if (!jwt_token) Router.push('/login')
>
return jwt_token
Настройка клиента
Теперь пришло время настроить наш клиент GraphQL. Замысел в том, чтобы получить токен из установленной нами переменной и, если он там, передавать его нашему клиенту GraphQL.
Предполагая, что ваш GraphQL API принимает JWT-токен аутентификации в качестве заголовка Authorization , вам нужно только настроить клиент на установку заголовка HTTP с использованием JWT-токена из переменной.
Вот как выглядит настройка с клиентом Apollo GraphQL, использующим мидлвар ApolloLink .
let appJWTToken
const httpLink = new HttpLink()
const authMiddleware = new ApolloLink((operation, forward)=> if (appJWTToken) operation.setContext( headers: Authorization: `Bearer $`
>
>);
>
return forward(operation);
>)
const apolloClient = new ApolloClient( link: concat(authMiddleware, httpLink),
cache: new InMemoryCache(),
>);
Как видно из этого кода, если есть токен, то он передаётся каждому запросу в качестве заголовка.
Но что случится, если токена не будет?
Всё зависит от потока выполнения в вашем приложении. К примеру, вы можете перенаправлять пользователя назад на страницу авторизации:
else Router.push('/login')
>
А если токен истекает в процессе его использования?
Предположим, наш токен действителен только 15 минут. В этом случае при его истечении мы, вероятно, получим ошибку от API, отрицающего наш запрос (например, 401: Unauthorized ). Помните, что каждый сервис, умеющий использовать JWT, может независимо верифицировать его и проверить, истёк ли срок действия.
Давайте добавим в наше приложение обработку ошибок, чтобы разобраться с подобными случаями. Мы напишем код, который будет выполняться для каждого ответа API, проверяя его на ошибку. Когда мы получим от API ошибку об истечении/недействительности токена, мы запустим процесс выхода из системы или перенаправимся к процессу входа в неё.
Вот как выглядит код, если мы используем клиент Apollo:
import < onError >from 'apollo-link-error';
const logoutLink = onError((< networkError >) => if (networkError.statusCode === 401) logout();
>)
const apolloClient = new ApolloClient( link: logoutLink.concat(concat(authMiddleware, httpLink)),
cache: new InMemoryCache(),
>);
Вы можете заметить, что такой подход приводит к поистине печальному пользовательскому опыту. Пользователь будет вынужден повторять аутентификацию при каждом истечении токена. Именно поэтому приложения реализуют фоновый процесс обновления JWT, подробнее о котором мы расскажем ниже.
Выход из системы
При использовании JWT “logout” просто стирает токен на стороне клиента, после чего он уже не может быть использован в последующих вызовах API.
Значит вызова API /logout не существует?
Конечная точка logout , по сути, не требуется, т.к. любой микросервиc, принимающий ваш JWT, будет продолжать его принимать. Если ваш сервер аутентификации удаляет JWT, то это не будет иметь значения, поскольку другие сервисы будут продолжать его принимать (поскольку весь смысл JWT в отсутствии централизованной координации).
Что делать, если мне нужно обеспечить невозможность продолжения использования токена?
Поэтому важно определять краткосрочные значения срока действия JWT. По этой же причине нам ещё более важно обеспечить таким образом защиту JWT от кражи. Токен действителен (даже после его удаления на клиенте), но только в течение короткого промежутка времени, что снижает вероятность его использования злоумышленниками.
Дополнительно вы можете добавить процесс обработки чёрных списков. В данном случае можно создать вызов API /logout , и ваш сервер аутентификации будет помещать токены в “недействительный список”. Как бы то ни было, все API сервисы, использующие JWT, теперь должны добавить дополнительный шаг к их верификации, чтобы проверять централизованный “чёрный список”. Это снова вводит центральное состояние и возвращает нас к тому, что мы имели до использования JWT.
Разве наличие чёрного списка не исключает преимущество JWT, гласящее о необязательности центрального хранилища?
В некотором смысле так и есть. Это опциональная предосторожность, к которой вы можете прибегнуть, если беспокоитесь, что ваш токен могут украсть. Но при этом она также повышает число необходимых верификаций.
Что произойдёт, если я авторизован на нескольких вкладках?
Один из способов разрешения этой ситуации заключается во введении в локальном хранилище глобального слушателя событий. При каждом обновлении этого ключа logout в локальном хранилище для одной вкладки, слушатель сработает и на других вкладках, также запустив ‘logout’ и перенаправив пользователей на экран входа в систему.
window.addEventListener('storage', this.syncLogout)
//.
syncLogout (event) if (event.key === 'logout') console.log('logged out from storage!')
Router.push('/login')
>
>
Теперь при выходе нам нужно совершить два действия:
- обнулить токен;
- установить в локальном хранилище элемент logout .
async function logout () inMemoryToken = null;
const url = 'http://localhost:3010/auth/logout'
const response = await fetch(url, method: 'POST',
credentials: 'include',
>)
// для поддержки выхода из системы во всех окнах
window.localStorage.setItem('logout', Date.now())
>
В этом случае при каждом выходе на одной вкладке, слушатель событий будет срабатывать во всех других, перенаправляя их на экран авторизации.
Это работает для вкладок, но как мне принудительно закрыть сессии на разных устройствах?
Эту тему мы рассмотрим в одном из следующих разделов.
Фоновое обновление
Существует две главных проблемы, с которыми могут столкнуться пользователи нашего JWT-приложения:
- Учитывая короткий срок действия JWT, пользователь будет повторно авторизовываться каждые 15 минут, что было бы ужасно неудобно. Ему наверняка было бы комфортнее оставаться в системе длительное время.
- Если пользователь закрывает своё приложение и вновь его открывает, то ему придётся авторизовываться повторно. Его сессия не постоянна, т.к. мы не сохраняем токен на клиенте.
Для решения этих проблем большинство JWT-провайдеров предоставляют токен обновления, который имеет два свойства:
- Он может использоваться для совершения вызова API (например, /refresh_token ) с запросом нового JWT-токена до истечения срока действия текущего.
- Он может быть безопасно сохранён на клиенте в течение нескольких сессий.
Как работает обновляемый токен?
Этот токен выдаётся в процессе аутентификации вместе с JWT. Сервер аутентификации сохраняет его и ассоциирует с конкретным пользователем в своей базе данных, чтобы иметь возможность обрабатывать логику обновления JWT.
На клиенте до момента истечения текущего токена мы подключаем наше приложение, чтобы сделать конечную точку refresh_oken и получить новый JWT.
Как осуществляется безопасное хранение токена обновления на клиенте?
Токен обновления отправляется сервером аутентификации клиенту в виде куки HttpOnly и автоматически пересылается браузером в вызове API /refresh_token .
Т.к. JavaScript на клиентской стороне не может считать или украсть куки HttpOnly , это несколько лучше для противостояния XSS, чем хранение токена в обычной куки или локальном хранилище.
Такой подход защищён от CSRF-атак, т.к. даже несмотря на то, что атака на подписание формы может выполнить API-вызов /refresh_token , атакующий не сможет получить новое возвращаемое значение токена.
Какой же из способов сохранения JWT-сессии в итоге мы считаем лучшим?
Обратите внимание, что хоть этот метод и не устойчив к серьёзным XSS-атакам, объединённый с обычными техниками защиты от XSS, куки HttpOnly является рекомендуемым способом сохранения относящейся к сессии информации. При этом, сохраняя нашу сессию косвенно через токен обновления, мы устраняем прямую уязвимость CSRF, которая присутствует при использовании JWT-токена.
Как же выглядит новый процесс “авторизации”?
Ничего особо не меняется, за исключением того, что токен обновления отправляется вместе с JWT. Давайте ещё раз взглянем на диаграмму процесса авторизации, но теперь уже с функциональностью refresh_token :
- Пользователь авторизуется с вызовом API login.
- Сервер генерирует JWT-токен и refresh_token .
- Сервер устанавливает куки HttpOnly с refresh_token . jwt_token и jwt_token_expiry возвращаются назад клиенту в виде информационного наполнения JSON.
- jwt_token хранится в памяти.
- Отсчёт до следующего фонового обновления начинается на основе jwt_token_expiry .
А как выглядит фоновое обновление?
Вот что происходит:
- Вызов конечной точки /refresh_token .
- Сервер будет считывать куки HttpOnly , и если он найдёт действительный refresh_token , тогда…
- …сервер вернёт новый jwt_token и jwt_token_expiry клиенту, а также установит новую куки токена обновления через заголовок Set-Cookie .
Постоянные сессии
Теперь когда мы можем убедиться, что наши пользователи не вынуждены постоянно авторизовываться, давайте обратим внимание на вторую проблему постоянных сессий.
Вы заметите, что если пользователи закрывают приложение и затем снова его открывают (предположим, закрывая и вновь открывая вкладку браузера), то им будет снова предложено войти в систему.
Приложения обычно спрашивают пользователей, хотят ли те “stay logged in” (оставаться авторизованными) в течение нескольких сессий или сохраняют их авторизованными по умолчанию. Именно это хотим реализовать и мы.
На данный момент мы не можем этого сделать, т.к. JWT находится в памяти и не сохраняется. Ранее мы уже рассматривали, почему не можем хранить JWT непосредственно в куки или локальном хранилище.
Как же можно безопасно сохранять сессии?
Как и говорилось ранее, мы можем сохранять токены обновления и затем использовать их для фонового обновления (т.е. обновлять наш краткосрочные JWT-токены, не прося пользователей авторизовываться повторно). При этом мы также можем использовать их для запроса нового JWT-токена для новой сессии.
Давайте предположим, что пользователь вышел из текущей сессии, закрыв вкладку браузера. Теперь давайте пронаблюдаем процесс, происходящий при последующем посещении пользователем приложения:
- Если мы видим, что в памяти у нас нет JWT, тогда мы запускаем процесс фонового обновления.
- Если токен обновления по-прежнему действителен (или не был пересоздан), тогда мы получаем новый JWT и можем продолжать.
Возможный случай ошибки:
В случае если наш срок действия токена обновления истечёт (предположим, пользователь вернётся в приложение через большой промежуток времени) или будет пересоздан (к примеру, из-за принудительного выхода), тогда клиент получит ошибку 401 для неавторизованного refresh_token. Ещё в одном случае у нас может просто изначально не быть refresh_token , что также приведёт к ошибке, полученной от конечной точки /refresh_token , в результате которой пользователь будет перенаправлен на экран авторизации.
Вот пример кода, показывающий, как мы можем обработать эти ошибки при помощи logoutLink :
const logoutLink = onError((< networkError >) => if (networkError.statusCode === 401) logout();
>)
Принудительный выход или выход из всех сессий на всех устройствах
Теперь, когда пользователи авторизованы постоянно и будут оставаться в системе из сессии в сессию, появляется новая проблема, требующая решения: принудительный выход или выход из всех сессий на всех устройствах.
Реализации токена обновления из предыдущих разделов показывают, что мы можем сохранять сессии и оставаться в системе.
В этом случае простая реализация принудительного выхода просит сервер аутентификации сделать недействительными все токены обновления, ассоциированные с конкретным пользователем.
По сути, это реализация для сервера аутентификации в бэкенде, которая не требует никакой особой обработки на клиенте, за исключением кнопки “Принудительный выход” в приложении.
Отображение серверной части (SSR)
В отображении серверной стороны при работе с JWT-токенами есть дополнительные трудности.
Вот что нам нужно:
- Браузер делает запрос к URL приложения.
- SSR-сервер отображает страницу на основе идентичности пользователя.
- Пользователь получает отображённую страницу, а затем продолжает использовать приложение в качестве SPA (одностраничного приложения).
Откуда SSR сервер узнаёт, авторизован ли пользователь?
Браузер должен отправить SSR-серверу информацию о текущей идентичности пользователя. Единственный способ сделать это — использовать куки.
Мы уже реализовали рабочий процесс токена обновления через куки, поэтому когда мы делаем запрос к SSR-серверу, нам нужно убедиться, что токен обновления будет также отправлен.
Внимание! Для SSR на аутентифицированных страницах очень важно, чтобы домен API аутентификации (и, следовательно, домен куки refresh_token ) совпадал с доменом SSR-сервера. В противном случае наши куки не смогут быть отправлены SSR-серверу.
Вот что делает SSR-сервер:
- При получении запроса на отображение конкретной страницы он захватывает куки refresh_token.
- Далее сервер использует эту куки, чтобы получить новый JWT для пользователя.
- Затем сервер использует этот новый токен и совершает все аутентифицированные запросы GraphQL для извлечения правильных данных.
Может ли пользователь продолжить выполнение аутентифицированных API-запросов, когда SSR-страница будет загружена?
К сожалению, без дополнительных манипуляций сделать это не получится. Как только SSR-сервер возвращает отображённый HTML, единственной оставшейся в браузере идентификацией пользователя является старая куки токена обновления, которая уже была использована SSR-сервером.
Если код нашего приложения попытается использовать этот токен обновления для запроса нового JWT, этот запрос провалится, и пользователь будет выброшен из системы.
Для решения этой проблемы SSR-сервер должен отправить последнюю куки токена обновления после отображения страницы, чтобы браузер мог её использовать.
Весь поток SSR от начала до конца:
Пример кода
Пример кода для этой статьи с полным описанием рабочего приложения и возможностями SSR доступен здесь.
Этот репозиторий также содержит пример бэкенд-кода аутентификации (основанного на Awesome https://github.com/elitan/hasura-backend-plus).
Итоги
Если вы проработаете все описанные выше разделы, ваше приложение получит все современные возможности JWT, имея при этом уверенную защиту от распространённых уязвимостей, которым может быть подвержена реализация JWT.
- Введение в GraphQL: сложные операции и переменные
- Как создать GraphQL-сервер с запросами, мутациями и подписками
- Эмпирический анализ схем GraphQL. Часть 1