Basic Authentication for json-server – DZone Security

Введение

В этой статье мы поговорим о том, как работают JSON Web Tokens, в чем их преимущества, какова их структура и как использовать их для базовой аутентификации и авторизации в Express.

Вам не обязательно иметь опыт работы с JSON Web Tokens (JWT), поскольку мы будем говорить об этом с нуля.

Для раздела, посвященного реализации, будет предпочтительнее, если у вас есть опыт работы с Express, Javascript ES6 и REST-клиентами.

Что такое веб-токены json?

JSON Web Tokens (JWT) были введены в качестве метода безопасного обмена данными между двумя сторонами. Он был представлен в спецификации RFC 7519, разработанной рабочей группой инженеров Интернета (IETF).

Технологии

Для решения используем фреймворк Spring Boot и Spring Web, для него требуется:

  1. Java 8 ;

  2. Apache Maven

Авторизация и валидация будет выполнена силами Spring Security и JsonWebToken (JWT).Для уменьшения кода использую Lombok.

Создание приложения

Переходим к практике. Создаем Spring Boot приложение и реализуем простое REST API для получения данных пользователя и списка пользователей.

1 Создание Web-проекта

Создаем Maven-проект SpringBootSecurityRest. При инициализации, если вы это делаете через Intellij IDEA, добавьте Spring Boot DevTools, Lombok и Spring Web, иначе добавьте зависимости отдельно в pom-файле.

2 Конфигурация pom-xml

После развертывания проекта pom-файл должен выглядеть следующим образом:

  1. Должен быть указан parent-сегмент с подключенным spring-boot-starter-parent;

  2. И установлены зависимости spring-boot-starter-web, spring-boot-devtools и Lombok.

3 Создание ресурса REST

Разделим все классы на слои, создадим в папке com.springbootsecurityrest четыре новые папки:

Spring Security

Простенькое REST API написано и пока оно открыто для всех. Двигаемся дальше, теперь его необходимо защитить, а доступ открыть только авторизованным пользователям. Для этого воспользуемся Spring Security и JWT.

Spring Security это Java/JavaEE framework, предоставляющий механизмы построения систем аутентификации и авторизации, а также другие возможности обеспечения безопасности для корпоративных приложений, созданных с помощью Spring Framework.

JSON Web Token (JWT) — это открытый стандарт (RFC 7519) для создания токенов доступа, основанный на формате JSON. Как правило, используется для передачи данных для аутентификации в клиент-серверных приложениях. Токены создаются сервером, подписываются секретным ключом и передаются клиенту, который в дальнейшем использует данный токен для подтверждения своей личности.

1 Подключаем зависимости

Добавляем новые зависимости в pom-файл.

2 Генерация и хранения токена

Начнем с генерации и хранения токена, для этого создадим папку security и в ней создаем класс JwtTokenRepository с имплементацией интерфейса CsrfTokenRepository (из пакета org.springframework.security.web.csrf).

Интерфейс указывает на необходимость реализовать три метода:

  1. Генерация токена в методе generateToken;

  2. Сохранения токена – saveToken;

  3. Получение токена – loadToken.

Генерируем токен силами Jwt, пример реализации метода.

3 Создание нового фильтра для SpringSecurity

Создаем новый класс JwtCsrfFilter, который является реализацией абстрактного класса OncePerRequestFilter (пакет org.springframework.web.filter). Класс будет выполнять валидацию токена и инициировать создание нового. Если обрабатываемый запрос относится к авторизации (путь /auth/login), то логика не выполняется и запрос отправляется далее для выполнения базовой авторизации.

6 Обработка ошибок

Что бы видеть ошибки авторизации или валидации токена, необходимо подготовить обработчик ошибок. Для этого создаем новый класс GlobalExceptionHandler в корне com.springbootsecurityrest, который является расширением класса ResponseEntityExceptionHandler с реализацией метода handleAuthenticationException.

Метод будет устанавливать статус ответа 401 (UNAUTHORIZED) и возвращать сообщение в формате ErrorInfo.

7 Настройка конфигурационного файла Spring Security.

Все данные подготовили и теперь необходимо настроить конфигурационный файл. В папке com.springbootsecurityrest создаем файл SpringSecurityConfig, который является реализацией абстрактного класса WebSecurityConfigurerAdapter пакета org.springframework.security.config.annotation.web.configuration. Помечаем класс двумя аннотациями: Configuration и EnableWebSecurity.

Authentication failure

As expected, we get an authentication failure.

Authentication failure response

That was useful and a bit of fun, for your next steps I expect you may want to try another Passport Strategy like Google or LinkedIn authentication!

Passport’s pluggable authentication greatly simplifies a complex, arduous, and oft times error-prone process.

I hope you’ve enjoyed reading this article as much as I have writing it and am looking forward to your feedback.

Authentication response

Next, we’ll mess with the password and try again.

Authentication test

If all went well, you should see the following response:

Automatic logout

Немало вопросов на практике возникает с automatic logout. Может показаться, что задачу можно решить путем манипуляции сроков действия access- и refresh-токенов. То есть после обращения пользователя к бэкенду система выдает ему эти токены со сроком действия, который нужен четко для автоматического логаута. И тогда если токен просрочен, то логаут состоится. Ниже на иллюстрации приведена именно такая схема:

Best practices в jwt

И в завершении приведу best practices при использовании этого вида токенов, которых следует придерживаться:

Configuring npm packages

In our earlier Zero Code example, we were able to start the json-server with staticassets in our project hierarchy. As we add custom code, we’ll need a way of letting npmknow about our project dependencies. This is accomplished by adding package.json.

Configuring server.json

Our customized rendition of json-server allows for additional middleware products in our json-server implementation. To add the Basic Authentication support, we start with the sample code described in the Module section of the json-server website and add our references to Passport.

Похожее:  Как зарегистрироваться и войти в Мобильный банк Центр Инвест

Passport strategy

Passport strategies are generally of the following form:

Payload или полезные данные

Вторым блоком идет eyJ1c2VyX2lkIjoxLCJleHAiOjE1ODEzNTcwMzl9

Это есть полезные данные, так же закодированные в Base64. После раскодирования получим:

Regression testing

If all went well, you should see a familiar response to the all wines request from json-server.

Start json-server

Since we now have a custom json-server implementation, we start it using the script (above) that we added earlier in our package.json.

When the json-server starts you see the message JSON Server is running on 3000.

Summary

In my last DZone article on Zero Code REST with json-server I showed you how to stand up a REST server with minimal effort and no coding.

While this approach may work well in many situations, you often find that you’ll need to extend capabilities, so you’ll soon roll up your sleeves and start bending some code. The fun stuff!

This article will share an approach for adding authentication to a json-server using a simple, unobtrusive Express middleware component called Passport.

Где хранить токены

Представим ситуацию: пользователь авторизовался и получил 2 токена:

access – живет 3 мин;

refresh – 5 мин.

refresh мы сохраняем в cookie пользователя.

access держим в памяти, и при каждом обновлении страницы, выполняем запрос refresh для получения новой пары такенов. Либо есть вариант когда мы записываем access в cookie.

Не стоит хранить access или refresh токен в localstore, это не безопасно.

Устанавливаем заголовки для set-cookie:

sameSite: true, SameSite=strict (для предотвращения CSRF-атак)

Главная проблема localstorage

Не храните jwt в локальном хранилище, js скрипты на нашем сайте легко могут получить данные из LocalStorage нашего приложения, у локального хранилища нет никаких способов защиты, то есть если мы храним секретные данные в локальном хранилище, информация доступна всем.

Пример инъекции в js

Допустим, у нас на сайте есть скрипт

Заголовок

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 – это первая часть токена – есть заголовок. Она закодирована в Base64 и если её раскодировать, получим строку:

Зачем 2 токена?

Представим ситуацию, когда у нас каким-то образом украли Access токен. Да, это уже плохо и где-то у нас брешь в безопасности. Злоумышленник в этом случае сможет им воспользоваться не более чем на 15-30 минут. После чего токен “протухнет” и перестанет быть актуальным. Ведь нужен второй токен для продления.

Если украли Refresh токен, то без Access токена (который недоступен в JS) продлить ничего нельзя и он оказывается просто бесполезным.

Использование jwt с express

В этом уроке мы создадим простое веб-приложение на основе микросервиса для управления книгами в библиотеке с двумя сервисами. Один сервис будет отвечать за аутентификацию пользователей, а другой – за управление книгами.

Будет два типа пользователей – администраторы и члены библиотеки. Администраторы смогут просматривать и добавлять новые книги, в то время как пользователи смогут только просматривать их. В идеале они также смогут редактировать или удалять книги. Но чтобы максимально упростить эту статью, мы не будем вдаваться в такие подробности.

Чтобы начать работу, в терминале инициализируйте пустой проект Node.js с настройками по умолчанию:

Затем установим фреймворк Express:

Обновление токена

На данном этапе наше приложение обрабатывает аутентификацию и авторизацию для книжного сервиса, хотя в дизайне есть серьезный недостаток – срок действия JWT-токена никогда не истекает.

Если этот токен украдут, то доступ к учетной записи будет у них навсегда, а реальный пользователь не сможет отозвать доступ.

Чтобы устранить эту возможность, давайте обновим обработчик запроса на вход, чтобы срок действия токена истекал через определенный период. Мы можем сделать это, передав свойство expiresIn в качестве опции для подписи JWT.

Когда мы истекает срок действия токена, у нас также должна быть стратегия генерации нового токена на случай истечения срока действия. Для этого мы создадим отдельный JWT-токен, называемый refresh-токеном, который можно использовать для генерации нового.

Сначала создайте секрет маркера обновления и пустой массив для хранения маркеров обновления:

Когда пользователь входит в систему, вместо генерации одного токена генерируют как токены обновления, так и аутентификации:

app.post('/login', (req, res) => {
    // read username and password from request body
    const { username, password } = req.body;

    // filter user from the users array by username and password
    const user = users.find(u => { return u.username === username && u.password === password });

    if (user) {
        // generate an access token
        const accessToken = jwt.sign({ username: user.username, role: user.role }, accessTokenSecret, { expiresIn: '20m' });
        const refreshToken = jwt.sign({ username: user.username, role: user.role }, refreshTokenSecret);

        refreshTokens.push(refreshToken);

        res.json({
            accessToken,
            refreshToken
        });
    } else {
        res.send('Username or password incorrect');
    }
});

А теперь давайте создадим обработчик запроса, который генерирует новые токены на основе обновленных токенов:

Но и с этим есть проблема. Если токен refresh будет украден у пользователя, кто-то может использовать его для генерации любого количества новых токенов.

Чтобы избежать этого, давайте реализуем простую функцию выхода из системы:

Похожее:  Добавление аутентификации в ваше приложение с помощью Flask-Login | DigitalOcean

Когда пользователь запрашивает выйти из системы, мы удалите токен обновления из нашего массива. Он гарантирует, чКогда пользователь попросит выйти из системы, мы удалим маркер обновления из нашего массива. Это гарантирует, что когда пользователь выйдет из системы, никто не сможет использовать маркер обновления для создания нового маркера аутентификации.

Подбор ключа

При использовании шифрования алгоритмом HS256 мы используем секретный ключ. Злоумышленник может узнать наш ключ с помощью подбора.  

Рекомендации для защиты от атаки подбора ключей:

  1. SECRET_KEY – рекомендуется хранить в env переменных;

  1. Использовать ключ большой длины: большие и малые буквы, цифры и спецсимволы;

  1. Периодическая смена ключа. При такой процедуре пользователю придется заново проходить процедуру аутентификации, но это поможет сделать систему более безопасной.

Постскриптум

В своей реализации Refresh токена использовал общую длину 24 знака. Первые 6 знаков – это дата его “протухания”, следующие 12 знаков – случайно сгенерированные данные. И в конце 6 знаков – это часть Access токена последней части сигнатуры.

Дату протухания внедрил прям в токен с той целью, чтобы не хранить эту информацию где-то в другом месте, например, в базе данных.

Дата содержит год, месяц, день, час и минуты. Хранится в ASCII

Кодирование даты на Golang:

// приводим к целочисленному числу uint32. Итого 32 бита.
// расчет простой: год 12 бит, месяц 4 бита, день 5 бит и т.д. Таким образом в аккурат умещаемся в 32 бита или 4 байта.
date := uint32(year<<20) | uint32(month<<16) | uint32(day<<11) | uint32(hour<<6) | uint32(minute)

// в цикле кодируем байты в ASCII. 1 знак это шесть бит. Итого и получаем шесть знаков даты по таблице ASCII – печатные знаки.
for n := 0; n < 6; n {
b6Bit = byte(date>>i) & 0x3F
sBuilder.WriteByte(byte8bitToASCII(b6Bit))

}

Всю реализацию на Go можно изучить на Github-е

Преимущество использования jwt перед традиционными методами

Как мы уже говорили ранее, JWT может содержать всю информацию о самом пользователе, в отличие от аутентификации на основе сеанса.

Это очень полезно для масштабирования веб-приложений, например, веб-приложений с микросервисами. Сегодня архитектура современного веб-приложения выглядит примерно так:

Все эти сервисы могут быть одним и тем же сервисом, который будет перенаправляться балансировщиком нагрузки в соответствии с использованием ресурсов (CPU или Memory Usage) каждого сервера, или некоторыми различными сервисами, такими как аутентификация и т.д.

Если мы используем традиционные методы авторизации, такие как cookies, нам придется использовать общую базу данных, например Redis, для обмена сложной информацией между серверами или внутренними службами. Но если мы поделимся секретом между микросервисами, мы можем просто использовать JWT, и тогда для авторизации пользователей не потребуется никаких других внешних ресурсов.

Проблемы использования токенов

Как и с любой технологией, в работе с токенами есть некоторые ограничения и проблемы. Их надо осознавать и правильно обращаться с JWT при использовании в проектах. Я уже отмечал, что токен может быть проверен ресурсным сервером независимо ни от чего, достаточно лишь секретной строки.

Но отсюда следует проблема: если выдать пользователю новую пару токенов, а старая пара еще валидна, то средств ее заэкспайрить нет.

Ведь сам по себе токен — это строка, которая нигде не хранится. Пока он валидный, им можно пользоваться. В итоге юзер может получить вторую пару, третью, четвертую — а предыдущие еще будут работоспособны.

Проверка jwt  токена на подлинность

получаем токен { header.payload.signature }

Создаем сигнатуру из header и payload 

newSignature = HMACSHA256( header   "."   payload, SECRET_KEY)

сравниваем получившиеся сигнатуру с сигнатурой из токена:

if(newSignature  === signature) { токен валидный и не был подделан }

Проверка токена

Для проверка токена необходимо проделать ту же операцию.

Берем склейку заголовок данные, кодируем с помощью алгоритма HMAC-SHA256 и нашего приватного ключа. А далее берем сигнатуру с токена и сверяем с результатом кодирования. Если результаты совпадают – значит данные подтверждены и можно быть уверенным, что они не были подменены.

Различные типовые задачи с использованием jwt

Сегодня в проектах приходится решать задачи, которые выходят за рамки описываемых токенов, но при этом они базируются на их использовании. Первая подобная задача — lockout-механизм. Он предполагает блокирование пользователя после нескольких безуспешных попыток аутентификации.

Я не буду подробно останавливаться на этой задаче, она достаточно простая. Если пользователь несколько раз подряд ввел корректные имя или email, но неправильный пароль, надо заблокировать этого юзера на определенное время. Для этого на сервере нужно записать, допустим, в базу данных или промежуточное хранилище количество безуспешных попыток и задать блокировку доступа в случае превышении этого показателя.

Более интересные задачи — Logout и Only one active device, которые в принципе связаны между собой.

Logout предполагает, что после нажатия кнопки логаута во фронтенд-приложении надо с подтверждением выхода еще и инвалидировать сами токены, сделать их недействительными.

Сервис книг

После этого давайте создадим файл books.js для нашего сервиса книг.

Начнем с импорта необходимых библиотек и настройки приложения Express:

После настройки, чтобы имитировать базу данных, давайте просто создадим массив книг:

const books = [
    {
        "author": "Chinua Achebe",
        "country": "Nigeria",
        "language": "English",
        "pages": 209,
        "title": "Things Fall Apart",
        "year": 1958
    },
    {
        "author": "Hans Christian Andersen",
        "country": "Denmark",
        "language": "Danish",
        "pages": 784,
        "title": "Fairy tales",
        "year": 1836
    },
    {
        "author": "Dante Alighieri",
        "country": "Italy",
        "language": "Italian",
        "pages": 928,
        "title": "The Divine Comedy",
        "year": 1315
    },
];

Теперь мы можем создать очень простой обработчик запроса для получения всех книг из базы данных:

Похожее:  Authentication using Python requests - GeeksforGeeks

Поскольку наши книги должны быть видны только аутентифицированным пользователям. Мы должны создать промежуточное ПО для аутентификации.

Перед этим создайте секрет маркера доступа для подписания JWT, как и раньше:

Этот токен должен быть тем же самым, который используется в службе аутентификации. Благодаря тому, что секрет между ними общий, мы можем аутентифицироваться с помощью службы аутентификации, а затем авторизовать пользователей в службе book.

На этом этапе давайте создадим промежуточное ПО Express, которое будет обрабатывать процесс аутентификации:

В этом промежуточном ПО мы считываем значение заголовка authorization. Поскольку заголовок авторизации имеет значение в формате Bearer [JWT_TOKEN], мы разделили значение пробелом и выделили токен.

Затем мы проверили токен с помощью JWT. После проверки мы присоединяем объект пользователя к запросу и продолжаем. В противном случае мы отправим клиенту ошибку.

Мы можем настроить это промежуточное ПО в обработчике запроса GET следующим образом:

Давайте загрузим сервер и проверим, все ли работает правильно:

Сигнатура

Последняя часть токена – наиболее важная. У нас это E4FNMef6tkjIsf7paNrWZnB88c3WyIfjONzAeEd4wF0

Как вы уже могли заметить – первые данные передаются практически в открытом виде и раскодировать их может любой. Но шифровать их нет необходимости. Цель токена – подтвердить, что эти данные не были изменены. Вот для этих целей и выступает сигнатура. И чтобы её сгенерировать нужен приватный ключ.

Она получается примерно следующим образом:

Служба аутентификации

Затем создадим файл под названием auth.js, который будет нашей службой аутентификации:

В идеале мы должны использовать базу данных для хранения информации о пользователях. Но для простоты давайте создадим массив пользователей, который мы будем использовать для их аутентификации.

Сравнение сессий и jwt

Наверняка кто-то скажет: «Ура, вы переоткрыли механизм сессий», и в этом есть доля правды. Токен нужен для подтверждения аутентификации пользователя. А идентификатор сесии — это признак пользовательской сессии: находится ли сейчас юзер в системе, может ли он работать с ней, или сессия завершена.

В интернете вам может попасться такая немного саркастическая картинка:

Структура jwt

Давайте поговорим о структуре JWT через токен образец:

Как видно на изображении, в этом JWT есть три раздела, каждый из которых разделен точкой.

Примечание: Кодирование Base64 – это один из способов обеспечения сохранности данных, поскольку оно не сжимает и не шифрует данные, а просто кодирует их в понятном большинству систем виде. Вы можете прочитать любой текст в кодировке Base64, просто декодировав его.

Первый раздел JWT – это заголовок, который представляет собой строку в кодировке Base64. Если вы расшифруете заголовок, он будет выглядеть примерно так:

Раздел заголовка содержит алгоритм хэширования, который был использован для генерации знака и тип токена.

Второй раздел – полезная нагрузка, содержащая объект JSON, который был отправлен пользователю. Поскольку он закодирован только в Base64, его может легко декодировать любой.

Рекомендуется не включать в JWT какие-либо конфиденциальные данные, такие как пароли или личная информация.

Обычно тело JWT выглядит примерно так, хотя это не обязательно соблюдается:

В большинстве случаев свойство sub будет содержать идентификатор пользователя, свойство iat, которое сокращенно называется issued at, – это временная метка, когда токен был выпущен.

Вы также можете увидеть некоторые общие свойства, такие как eat или exp – время истечения срока действия токена.

Последний раздел – это подпись токена. Она генерируется путем хэширования строки base64UrlEncode(header) “.”. base64UrlEncode(payload) secret с использованием алгоритма, указанного в разделе заголовка.

Секрет – это случайная строка, которую должен знать только сервер. Ни один хэш не может быть преобразован обратно в исходный текст, и даже небольшое изменение исходной строки приведет к другому хэшу. Таким образом, секрет не может быть подвергнут обратному инжинирингу.

Когда эта подпись отправляется обратно на сервер, он может проверить, что клиент не изменил никаких деталей в объекте.

Шифрование

Выделяют 2 типа шифрования: симметричное и асимметричное.

Симметричное шифрование – способ шифрования, в котором для шифрования и дешифрования применяется один и тот же криптографический ключ. Проблемой данного способа является передача ключа. Необходимо исключить возможность кражи при передаче.

Асимметричное шифрование – процесс, при котором для шифрования данных используется публичный ключ, а для дешифровки – приватный. 

Вспомним «Вариант 2» задачи в начале статьи. 

Когда передаем замок без ключа из пункта А в пункт Б, в пункте Б мы получаем замок (публичный ключ) которым можно только зашифровать, то есть закрыть сундук.

При помощи его в пункте Б шифруем свой ключ (положим один из дубликатов ключей в сундук и закроем замком, который получили из пункта А) отправляем обратно в пункт А. 

В пункте А откроем сундук ключом (расшифруем данные приватным ключом).

Теперь в пунктах А и Б есть одинаковые ключи, и можно проводить симметричное шифрование, использовать 1 замок и дубликаты ключей с каждой стороны.

Заключение

В этой статье мы познакомили вас с JWT и тем, как реализовать JWT с помощью Express. Я надеюсь, что теперь у вас есть часть хороших знаний о том, как работает JWT и как реализовать его в вашем проекте.

Как всегда, исходный код доступен на GitHub.

1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (1 оценок, среднее: 4,00 из 5)
Загрузка...

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector