Веб-разработка на Go
Теория: JWT-авторизация на сервере
Представим, что мы разрабатываем страницу с профилем пользователя в социальной сети. У пользователей есть возможность редактировать свои профили. Нам нужно определять, что HTTP-запрос на изменение профиля пришел от владельца профиля. Если не сделать такую проверку, то злоумышленники смогут изменять данные профилей других пользователей.
В этом уроке мы разберем тему JWT-авторизации и ее реализацию в Go. Это важная тема, потому что авторизация — это ключевая часть безопасности любого веб-приложения.
Аутентификация и авторизация
Представим, что мы хотим войти в свой аккаунт социальной сети. Веб-сайту нужно понять, что мы владеем этим аккаунтом. Для этого нам нужно предоставить корректные учетные данные. Обычно это логин и пароль. Веб-сайт проверяет эти данные и, если они верны, мы входим в свой аккаунт. Этот процесс называется аутентификацией:
Допустим, после успешной аутентификации мы заходим на страницу со своим профилем. Так как мы уже были аутентифицированы, мы можем отправить запрос на редактирование данных профиля. Веб-сайт проверяет, что запрос пришел от владельца профиля, и позволяет изменять данные. Процесс проверки прав на осуществление действий называется авторизацией:
Таким образом, аутентификация — это процесс проверки учетных данных, а авторизация — это процесс проверки прав на осуществление действий. В примере социальной сети аутентификация происходит при логине, а авторизация при каждом запросе после аутентификации.
Процесс авторизации происходит часто, и если допустить ошибку в системе авторизации, то злоумышленники смогут осуществлять действия от имени других пользователей. Например, сделать покупку от их имени или узнать конфиденциальную информацию. Поэтому важно знать безопасные и проверенные способы авторизации.
Два самых популярных способа авторизации:
- С помощью сессий
- С помощью токенов
У каждого из этих способов есть свои уникальности, преимущества и недостатки. Ознакомиться более детально с ними можно в дополнительных материалах к уроку. Мы же рассмотрим на практике одну из реализаций авторизации с помощью токенов — JWT.
JWT-авторизация
JWT (JSON Web Token) — это специальный формат токена, который позволяет безопасно передавать данные между клиентом и сервером. Например, клиентом может быть веб-браузер или мобильное приложение, сервером — сервер с Go веб-приложением.
JWT-токен состоит из трех частей, которые разделены точкой:
- Header или заголовок — информация о токене, тип токена и алгоритм шифрования
- Payload или полезные данные — данные, которые мы хотим передать в токене. Например, имя пользователя, его роль, истекает ли токен. Эти данные представлены в виде JSON-объекта
- Signature или подпись — подпись токена, которая позволяет проверить, что токен не был изменен
Обычный токен имеет формат:
Рассмотрим пример реального токена с разбором каждой части. Предположим, наше веб-приложение сгенерировало следующий JWT-токен:
Header
Заголовок обычно состоит из JSON-объекта с двумя свойствами:
- Тип токена, который в нашем случае — JWT
- Алгоритм шифрования, который в нашем случае — HMAC SHA256
Далее этот JSON-объект хэшируется с помощью Base64Url-кодирования, чтобы представить его в виде компактной строки.
Таким образом, в нашем примере заголовок JWT-токена имеет следующее значение:
Payload
Вторая часть токена — это полезная нагрузка в виде JSON-объекта. Она содержит различные данные об авторизованном пользователе. Значение этой части JWT-токена различно в каждом веб-приложении. Мы можем записать здесь любые публичные данные, которые могут быть полезны при авторизации.
Как и заголовок JWT-токена, полезная нагрузка хэшируется с помощью Base64Url-кодирования для представления в виде компактной строки.
В нашем примере полезная нагрузка JWT-токена имеет следующее значение:
Названия некоторых полей могут показаться непонятными с первого взгляда. Например, поле sub означает идентификатор пользователя, а поле iat — время создания токена. При составлении полей полезной нагрузки рекомендуется учитывать имена из документации IANA (Internet Assigned Numbers Authority). Это поможет избежать конфликтов имен с общепринятыми нормами. Поэтому мы использовали название sub, вместо привычного user_id.
Основная причина, почему названия полей в полезной нагрузке JWT-токена пишутся сокращенно, — это уменьшение размера токена после шифрования.
Signature
Чтобы создать подпись, мы должны взять закодированный заголовок, закодированную полезную нагрузку, секретную строку и зашифровать эти данные. При этом нужно использовать алгоритм шифрования из заголовка JWT-токена.
В нашем примере для создания подписи используется алгоритм шифрования HMAC SHA256 и секретная строка "your-256-bit-secret":
Подпись используются, чтобы проверить, что сообщение не было изменено при передаче. Она также позволяет подтвердить, что отправитель JWT-токена является тем, кем он представляется.
Собираем все части JWT-токена
В результате генерации JWT-токена получаются три Base64-URL-закодированные строки, которые разделены точкой. Значение JWT-токена является компактным и легко передается в HTTP-запросах.
Если вы хотите попрактиковаться с JWT, можно использовать онлайн инструмент jwt.io Debugger для декодирования, проверки и генерации JWT-токенов.
Мы разобрали, из чего состоит JWT-токен. Теперь рассмотрим алгоритм работы с JWT-токеном в веб-приложениях.
Алгоритм работы с JWT-токеном
Процесс аутентификации и авторизации с JWT-токеном между веб-браузером и веб-приложением выглядит следующим образом:
- Веб-браузер отправляет запрос веб-приложению с логином и паролем
- Веб-приложение проверяет логин и пароль, и если они верны, то генерирует JWT-токен и отправляет его веб-браузеру. При генерации JWT-токена веб-приложение ставит подпись секретным ключом, который хранится только в веб-приложении
- Веб-браузер сохраняет JWT-токен и отправляет его вместе с каждым запросом в веб-приложение
- Веб-приложение проверяет JWT-токен и если он верный, то выполняет действие от имени авторизованного пользователя
Безопасность коммуникации между веб-браузером и веб-приложением заключается в том, что токены генерируются и подписываются только со стороны веб-приложения. Злоумышленник не сможет подделать токен, так как не знает секретный ключ, который используется для подписи токена.
Подпись токена происходит с помощью шифрования. С помощью подписи веб-приложение проверяет, что токен действительно был сгенерирован им. Шифрование может осуществляться различными алгоритмами. Например, алгоритмом HS256 — HMAC с SHA-256.
Мы рассмотрели основы JWT-авторизации и поняли, что веб-приложение должно генерировать JWT-токены и подписывать их секретным ключом, который хранится только в веб-приложении. Рассмотрим, как это можно реализовать в Go-приложении.
JWT-авторизация в Go веб-приложении
Реализуем аутентификацию и авторизацию в социальной сети, которая написана на Go с использованием микрофреймворка Fiber. Для этого реализуем следующие функции:
- Регистрация пользователя
- Вход в аккаунт — аутентификация
- Получение информации о своем аккаунте — только для авторизованных пользователей
Чтобы упростить работу, мы не будем описывать многие важные части в системах аутентификации пользователей. Например, мы не будем проверять HTTP-запросы, использовать базы данных или хэшировать хранимые пароли для безопасности. Вы можете улучшить эти части веб-приложения самостоятельно.
Регистрация аккаунта
Начнем разработку социальной сети с функции регистрации аккаунта. Когда пользователь заходит на веб-сайт, он видит форму регистрации с тремя полями: имя, электронная почта и пароль. После заполнения формы пользователь нажимает кнопку «Зарегистрироваться», и веб-браузер отправляет HTTP-запрос POST /register в наше веб-приложение. Мы реализуем обработчик этого HTTP-запроса следующим образом:
Когда приходит HTTP-запрос на регистрацию нового пользователя, веб-приложение проверяет, что в хранилище еще не существует пользователя с такой электронной почтой. Если пользователь с такой электронной почтой уже есть, то веб-приложение возвращает ошибку. В обратном случае все данные пользователя сохраняются в оперативной памяти веб-приложения.
После регистрации пользователь может войти в свой аккаунт, и далее мы реализуем эту возможность.
Вход в аккаунт
Когда пользователь заходит на страницу входа в аккаунт, он видит форму с двумя полями: электронная почта и пароль. Эти поля являются учетными данными пользователя. После заполнения формы пользователь нажимает кнопку «Войти», и веб-браузер отправляет HTTP-запрос POST /login в наше веб-приложение. Обработчик этого запроса будет выглядеть следующим образом:
Когда приходит HTTP-запрос на вход в аккаунт, веб-приложение проверяет, что в хранилище существует пользователь с такой электронной почтой и паролем. Если учетные данные некорректны, то веб-приложение возвращает ошибку. В обратном случае пользователь успешно прошел аутентификацию, и веб-приложение генерирует и возвращает JWT-токен. Его пользователь будет использовать в будущих HTTP-запросах, чтобы авторизоваться.
Чтобы сгенерировать JWT-токен, мы используем библиотеку jwt-go. Благодаря этому все тонкости формирования и шифрования токена скрыты от нас. Все, что от нас требуется, — это указать секретный ключ для подписи токена и полезные данные, которые будут храниться в токене.
В полезных данных JWT-токена мы записываем электронную почту пользователя как идентификатор. Также записываем время, когда этот токен будет недействителен. В нашем примере время жизни JWT-токена составляет 72 часа. Когда это время истечет, пользователь должен будет войти в аккаунт заново.
Теперь в нашей социальной сети пользователи могут регистрироваться и аутентифицироваться — входить в свои аккаунты. Далее реализуем функцию получения информации о своем аккаунте, которая будет доступна только авторизованным пользователям.
Получение информации о своем аккаунте для авторизованных пользователей
Когда пользователь прошел аутентификацию, он получил JWT-токен, который будет использовать в последующих HTTP-запросах для авторизации. Есть разные способы передавать токен в HTTP-запросе. Для этого можно использовать заголовок Authorization, параметр запроса или даже куки веб-браузера. Мы будем использовать заголовок Authorization, так как это наиболее распространенный способ передачи JWT-токена в HTTP-запросе.
Когда пользователь заходит на свою страницу, веб-браузер отправляет HTTP-запрос GET /profile в наше веб-приложение. Обработчик этого запроса выглядит следующим образом:
Так как проверка авторизации может происходить во многих HTTP-обработчиках, мы вынесли ее на уровень посредников. В микрофреймворке Fiber для этого есть готовый посредник — jwtware.
При инициализации посредника мы указали два свойства:
- SigningKey — секретный ключ JWT-токена
- ContextKey — название поля, по которому хранится объект JWT-токена авторизованного пользователя. Этот объект можно использовать в любом обработчике группы
authorizedGroup
Когда приходит HTTP-запрос на получение информации о пользователе, веб-приложение проверяет, что в заголовке Authorization указан корректный JWT-токен. Если проверка прошла успешно, веб-приложение ищет пользователя в хранилище по электронной почте, которая записана в полезной нагрузке JWT-токена. Если пользователь был найден, то авторизация пройдена, и мы возвращаем в HTTP-ответе информацию об этом пользователе.
Мы реализовали все части аутентификации и авторизации нашей социальной сети. Теперь соберем все вместе и проверим, что веб-приложение работает.
Проверяем веб-приложение
Полный код веб-приложения выглядит следующим образом:
Запускаем веб-приложение и отправляем запрос на регистрацию нового пользователя:
В ответ получаем:
Это означает, что такого пользователя нет в хранилище, и он был успешно создан.
Теперь попробуем пройти аутентификацию этого пользователя:
В ответ приходит:
Мы указали корректные учетные данные пользователя, поэтому аутентификация прошла успешно. В ответ веб-приложение вернуло JWT-токен, который мы будем использовать для авторизации при получении информации о пользователе.
Отправляем запрос на получение информации о пользователе:
В ответ приходит:
Так как мы передали JWT-токен в заголовке HTTP-запроса Authorization, веб-приложение авторизовало нас как пользователя john@doe.com, и вернуло информацию о нашем аккаунте.
В последнем запросе есть один интересный момент. Мы поставили перед значением токена слово Bearer:
Bearer переводится как носитель. Он дает веб-приложению понять, что в заголовке Authorization передан токен для авторизации. Принято считать, что это слово означает фразу: «Дай доступ к носителю этого токена». Если не указать слово Bearer перед значением, то авторизация не пройдет даже с корректным значением токена.
Мы реализовали функцию регистрации, аутентификации и получение информации об аккаунте авторизованного пользователя. Для авторизации мы использовали JWT-токен, который генерируются при входе в аккаунт на 72 часа и передается в заголовке HTTP-запроса Authorization.
Выводы
- Аутентификация — это процесс проверки подлинности пользователя, который пытается получить доступ к веб-приложению
- Авторизация — это процесс проверки прав пользователя на какое-либо действие в веб-приложении
- Один из способов реализации системы авторизации — это JWT-токен. Токен генерируется и подписывается веб-приложением после успешной аутентификации и передается во всех последующих HTTP-запросах в заголовке Authorization
- JWT-токен содержит информацию о пользователе и подпись, которая необходима для авторизации. Так как токен подписан секретным ключом в веб-приложении, то его может проверять на подлинность только это веб-приложение.
