Представим, что мы разрабатываем страницу с профилем пользователя в социальной сети. У пользователей есть возможность редактировать свои профили. Нам нужно определять, что 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-токен содержит информацию о пользователе и подпись, которая необходима для авторизации. Так как токен подписан секретным ключом в веб-приложении, то его может проверять на подлинность только это веб-приложение.
<!DOCTYPE html>
<html class="h-100" data-bs-theme="light" data-mantine-color-scheme="light" lang="ru" prefix="og: https://ogp.me/ns#">
<head>
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<link crossorigin="true" href="https://cdn.hexlet.io" rel="preconnect">
<link href="https://mc.yandex.ru" rel="preconnect">
<meta content="aa2vrdtq64dub8knuf83lwywit311w" name="facebook-domain-verification">
<link href="/favicon.ico" rel="icon" sizes="any">
<link href="/favicon.svg" rel="icon" type="image/svg+xml">
<link href="/apple-touch-icon.png" rel="apple-touch-icon">
<link href="/manifest.webmanifest" rel="manifest">
<script>
//<![CDATA[
window.gon={};gon.ym_counter="25559621";gon.is_bot=true;gon.applications={};gon.current_user={"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26 16:55:44 UTC","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false};gon.token="9Zt5jnBhP6QF_1y90imM0WPxym4SbSpM9wfYbjp6vZYaSrK5gh-SxLO8eCXeJnymo_jnxBpa1O5K50I6aH1a-A";gon.locale="ru";gon.language="ru";gon.theme="light";gon.rails_env="production";gon.mobile=false;gon.google={"analytics_key":"UA-1360700-51","optimize_key":"GTM-5QDVFPF"};gon.captcha={"google_v3_site_key":"6LenGbgZAAAAAM7HbrDbn5JlizCSzPcS767c9vaY","yandex_site_key":"ysc1_Vyob5ZPPUdPBsu0ykt8bVFdzsfpoVjQChLGl2b4g19647a89","verification_failed":null};gon.social_signin=false;gon.typoreporter_google_form_id="1FAIpQLSeibfGq-KvWQ2Fyru-zkFFRVTLBuzXAHAoEyN1p49FtDmNoNA";
//]]>
</script>
<meta charset="utf-8">
<title>JWT-авторизация на сервере | Веб-разработка на Go</title>
<meta name="description" content="JWT-авторизация на сервере / Веб-разработка на Go: Учимся настраивать JWT-авторизацию на сервере">
<link rel="canonical" href="https://ru.hexlet.io/courses/go-web-development/lessons/auth/theory_unit">
<meta name="robots" content="noarchive">
<meta property="og:title" content="JWT-авторизация на сервере">
<meta property="og:title" content="Веб-разработка на Go">
<meta property="og:description" content="JWT-авторизация на сервере / Веб-разработка на Go: Учимся настраивать JWT-авторизацию на сервере">
<meta property="og:url" content="https://ru.hexlet.io/courses/go-web-development/lessons/auth/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="YMJkmAnyoagfc-yHrzLK93ZGGgD9UkPaTHyFGD2tisOPE6-v-4wMyKkwyB-jPTqAtk83qvVlvXjxnB9Mb6ptrQ" />
<script src="/vite/assets/inertia-INZxX8jp.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/preload-helper-BJ4cLWpC.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-nkZBEvfU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ahoy-DrlRQ-1D.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/analytics-6pOtQ3OW.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Surface-DL2bpZA-.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/extends-C-EagtpE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/inheritsLoose-BBd-DCVI.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/objectWithoutPropertiesLoose-DRHXDhjp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/index.esm-DAqKOkZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Button-CGPUux8l.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/CloseButton-D1euiPao.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Group-BX48WcuU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Loader-BQEY8g6v.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Modal-Cy3HByv7.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/OptionalPortal-1Hza5P2w.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Stack-CtjJzfw4.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Textarea-Ck64llAy.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/DirectionProvider-Dc9zdUke.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/events-DJQOhap0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-reduced-motion-D2owz4wa.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-disclosure-zKtK5W1r.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-hotkeys-Cnc_Rwkb.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/random-id-DOQyszCZ.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/exports-C_MrNx_T.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<link rel="stylesheet" href="/vite/assets/application-BqhCP46M.js" />
<script src="/vite/assets/application-Df9RExpe.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/autocomplete-VMNbxKGl.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/createPopper-C3aM9r1M.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/js.cookie-D1-O8zkX.js" as="script" crossorigin="anonymous"><link rel="stylesheet" href="/vite/assets/application-C8HjmMaq.css" media="screen" />
<script>
window.ym = function(){(ym.a=ym.a||[]).push(arguments)};
window.addEventListener('load', function() {
setTimeout(function() {
ym.l = 1*new Date();
ym(window.gon.ym_counter, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true
});
// Загружаем скрипт
var k = document.createElement('script');
k.async = 1;
k.src = 'https://mc.yandex.ru/metrika/tag.js';
document.head.appendChild(k);
ym(window.gon.ym_counter, 'getClientID', function(clientID) {
window.ymClientId = clientID;
});
}, 1500);
});
</script>
<!-- Google Tag Manager - deferred -->
<script>
// dataLayer stub сразу — пуши работают до загрузки скрипта
window.dataLayer = window.dataLayer || [];
// Сам скрипт — отложенно после load
window.addEventListener('load', function() {
setTimeout(function() {
dataLayer.push({'gtm.start': new Date().getTime(), event: 'gtm.js'});
var j = document.createElement('script');
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-WK88TH';
document.head.appendChild(j);
}, 1500);
});
</script>
<!-- End Google Tag Manager -->
</head>
<body>
<noscript>
<div>
<img alt="" src="https://mc.yandex.ru/watch/25559621" style="position:absolute; left:-9999px;">
</div>
</noscript>
<header class="sticky-top bg-body">
<nav class="navbar navbar-expand-lg">
<div class="container-xxl">
<a class="navbar-brand" href="/"><img alt="Логотип Хекслета" height="24" src="https://ru.hexlet.io/vite/assets/logo_ru_light-BpiEA1LT.svg" width="96">
</a><button aria-controls="collapsable" aria-expanded="false" aria-label="Меню" class="navbar-toggler border-0 mb-0 mt-1" data-bs-target="#collapsable" data-bs-toggle="collapse">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="collapsable">
<ul class="navbar-nav mb-lg-0 mt-lg-1">
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
Все курсы
<span class="bi bi-chevron-down align-middle ms-1"></span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item d-flex py-2" href="/courses"><div class="fw-bold me-auto">Все что есть</div>
<div class="text-muted">117</div>
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные категории</b>
</li>
<li>
<a class="dropdown-item py-2" href="/courses_devops">Курсы по DevOps
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_data_analytics">Курсы по аналитике данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_programming">Курсы по программированию
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_testing">Курсы по тестированию
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные курсы</b>
</li>
<li>
<a class="dropdown-item py-2" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/go">Go-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/java">Java-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/python">Python-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/qa-auto-engineer-java">Автоматизатор тестирования на Java
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/data-analytics">Аналитик данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/frontend">Фронтенд-разработчик
</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
О Хекслете
<span class="bi bi-chevron-down align-middle"></span>
</button>
<ul class="dropdown-menu bg-body">
<li>
<a class="dropdown-item py-2" href="/pages/about">О нас
</a></li>
<li>
<a class="dropdown-item py-2" href="/blog">Блог
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/hse-research" role="button">Результаты (Исследование)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://career.hexlet.io" role="button">Хекслет Карьера
</span></li>
<li>
<a class="dropdown-item py-2" href="/testimonials">Отзывы студентов
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://t.me/hexlet_help_bot" role="button">Поддержка (В ТГ)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/referal-program/?promo_creative=priglasite-druzei&promo_name=referal-program&promo_position=promo_position&promo_start=010724&promo_type=link" role="button">Реферальная программа
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/certificate" role="button">Подарочные сертификаты
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://hh.ru/employer/4307094" role="button">Вакансии
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://b2b.hexlet.io" data-target="_blank" role="button">Компаниям
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexly.ru/" data-target="_blank" role="button">Колледж
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexlyschool.ru/" data-target="_blank" role="button">Частная школа
</span></li>
</ul>
</li>
<li><a class="nav-link" href="/subscription/new">Подписка</a></li>
</ul>
<ul class="navbar-nav flex-lg-row align-items-lg-center gap-2 ms-auto">
<li>
<a class="nav-link" aria-label="Переключить тему" href="/theme/switch?new_theme=dark"><span aria-hidden="true" class="bi bi-moon"></span>
</a></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="/u/new" role="button"><span>Регистрация</span>
</span></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="https://ru.hexlet.io/session/new" role="button"><span>Вход</span>
</span></li>
</ul>
</div>
</div>
</nav>
</header>
<div class="x-container-xxxl">
</div>
<main class="mb-6 min-vh-100 h-100">
<link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDAzNywicHVyIjoiYmxvYl9pZCJ9fQ==--c9a507d1b30c26185c312c95f68af4f0d8122afa/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Programmer-bro.png"/><link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6Mzk2NSwicHVyIjoiYmxvYl9pZCJ9fQ==--84278a1852c9c6fb13b80a69f395bac6e47a422e/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Cloud%20sync-bro.png"/><link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzY2MywicHVyIjoiYmxvYl9pZCJ9fQ==--7e08550d68b7c2ad01e51109dfcf0861158d2e58/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Code%20review-amico.png"/><link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDg4MywicHVyIjoiYmxvYl9pZCJ9fQ==--9e90f48f058ab07777e0e79ecfcf2980f79fa277/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Code%20typing-cuate.png"/><link rel="preload" as="image" href="/vite/assets/development-BVihs_d5.png"/><div id="app" data-page="{"component":"web/courses/lessons/theory_unit","props":{"errors":{},"locale":"ru","language":"ru","httpsHost":"https://ru.hexlet.io","host":"ru.hexlet.io","colorScheme":"light","auth":{"user":{"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26T16:55:44.658Z","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false}},"cloudflareTurnstileSiteKey":"0x4AAAAAAA15KmeFXzd2H0Xo","vkIdClientId":"51586979","yandexIdClientId":"88d071f1d3384eb4bd1deb37910235c7","formAuthToken":"oNGcJiwwNjqHGaZZwaktJVW02Hm-yyPse6aSUnMaZd1PAFcR3k6bWjFagsHNpt1Slb3107b83U7GRggGIR2Csw","topics":[{"id":88641,"title":"В тестах нет проверки на StatusConflict","plain_title":"В тестах нет проверки на StatusConflict ","creator":{"public_name":"Andrey","id":438142,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Mamtsev","id":294764,"is_tutor":true},"id":177000,"body":"Спасибо, добавил","topic_id":88641}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"JWT-авторизация на сервере","entity_url":null,"active":true}},{"id":85586,"title":"Нерабочая ссылка:\nhttps://highload.today/blogs/jwt-avtorizatsiya/#1","plain_title":"Нерабочая ссылка: https://highload.today/blogs/jwt-avtorizatsiya/#1 ","creator":{"public_name":"Alex Браганец","id":609169,"is_tutor":false},"comments":[{"creator":{"public_name":"Nikolai Gagarinov","id":104929,"is_tutor":true},"id":172665,"body":"Или вот эту можно посмотреть https://habr.com/ru/post/340146/","topic_id":85586},{"creator":{"public_name":"Nikolai Gagarinov","id":104929,"is_tutor":true},"id":172587,"body":"Добрый день.\n\nОпишите, пожалуйста, что именно не работает? Какое сообщение выводится, в каком браузере открываете ссылку и где находитесь географически, используете ли прокси/впн?","topic_id":85586},{"creator":{"public_name":"Nikolai Gagarinov","id":104929,"is_tutor":true},"id":172664,"body":"Спасибо, что сообщили, посмотрим альтернативную статью или ресурс.\nПока статью можно посмотреть [в кеше](https://webcache.googleusercontent.com/search?q=cache:AAoQ4p6T9pkJ:https://highload.today/blogs/jwt-avtorizatsiya/&cd=4&hl=ru&ct=clnk&gl=ru) гугла","topic_id":85586},{"creator":{"public_name":"Alex Браганец","id":609169,"is_tutor":false},"id":172650,"body":"Не переходит по ссылке.\n\nSafari Версия 16.3 (18614.4.6.1.6)\n\nSafari не может открыть страницу «https://highload.today/blogs/jwt-avtorizatsiya/#1», так как Safari не удается безопасно подключиться к серверу «highload.today».\n\nGoogle Chrome Версия 111.0.5563.64 (Официальная сборка), (x86_64)\n\nДоступ на сайт с ru сегмента ограничен по политическим мотивам. \nЧерез VPN только.\n\n","topic_id":85586}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"JWT-авторизация на сервере","entity_url":null,"active":true}},{"id":98950,"title":"Постоянно ошибки внутренние Хекслета\n\n```\nError Trace:\t/usr/src/app/main_test.go:122\n\t\t\t/usr/src/app/main_test.go:27\nError: Received unexpected error:\n\t Post \"http://localhost:8080/signup\": dial tcp 127.0.0.1:8080: connect: connection refused\n```","plain_title":"Постоянно ошибки внутренние Хекслета Error Trace: /usr/src/app/main_test.go:122 /usr/src/app/main_test.go:27 Error: Received unexpected error: Post \"http://localhost:8080/signup\": dial tcp 127.0.0.1:8080: connect: connection refused ","creator":{"public_name":"Александр Ржаницын","id":53329,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Gagarinov","id":75907,"is_tutor":true},"id":190301,"body":"**Александр Ржаницын**, здравствуйте. Подскажите, в какой момент возникают ошибки? Можете прислать ссылку на свое решение?","topic_id":98950}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"JWT-авторизация на сервере","entity_url":null,"active":true}},{"id":83587,"title":"\"хэшируется с помощью Base64Url-кодирования\" - серьезно?","plain_title":"\"хэшируется с помощью Base64Url-кодирования\" - серьезно? ","creator":{"public_name":"Имя Фамилия","id":288556,"is_tutor":false},"comments":[{"creator":{"public_name":"Ivan Mamtsev","id":294764,"is_tutor":true},"id":169643,"body":"Добрый день, подскажите в чем ваш вопрос?","topic_id":83587},{"creator":{"public_name":"Сергей","id":697931,"is_tutor":false},"id":183013,"body":"https://github.com/gofiber/jwt - с мая месяца не поддерживается","topic_id":83587},{"creator":{"public_name":"Ivan Mamtsev","id":294764,"is_tutor":true},"id":183248,"body":"Обновили теорию","topic_id":83587}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"JWT-авторизация на сервере","entity_url":null,"active":true}}],"lesson":{"exercise":{"id":2361,"slug":"go_web_development_auth_exercise","name":null,"state":"active","kind":"exercise","language":"golang","locale":"ru","has_web_view":true,"has_test_view":false,"reviewable":true,"readme":"В этой практике вас предстоит реализовать систему аутентификации и авторизации с использованием jwt-токенов. \n\nСхема работы веб-приложения следующая:\n\n## app/solution.go\n\n1. Пользователь отправляет HTTP-запрос на регистрацию с логином и паролем:\n\n```bash\nPOST /signup\n\n{\"email\": \"john@doe.com\", \"password\": \"qwerty123\"}\n```\n\nВеб-приложение хранит созданных пользователей в переменной `users map[string]User`, где ключ — это электронная почта пользователя, а значение — структура со всеми данными пользователя. Если пользователь с таким email уже существует, то возвращается HTTP-ответ со статусом *409 Conflict*. В случае успеха веб-приложение сохраняет пользователя в оперативной памяти и возвращает HTTP-ответ со статусом *200 OK*.\n\n2. После успешной регистрации пользователь может пройти аутентификацию. Для этого он отправляет следующий HTTP-запрос с логином и паролем:\n\n```bash\nPOST /signin\n\n{\"email\": \"john@doe.com\", \"password\": \"qwerty123\"}\n```\n\nЕсли пользователь отправил некорректные учетные данные, то веб-приложение возвращает HTTP-ответ со статусом *422 Unprocessable Entity*. В случае успеха веб-приложение генерирует JWT-токен для этого пользователя и возвращает HTTP-ответ со статусом *200 OK* и JWT-токеном в теле:\n\n```bash\nHTTP/1.1 200 OK\n\n{\"jwt_token\": \"xxxxx.yyyyy.zzzzz\"}\n```\n\nДля подписи JWT-токена необходимо использовать секретный ключ, который хранится в переменной `secretKey string`.\n\n3. Когда пользователь прошел аутентификацию он может запросить данные своего профиля. Для авторизации пользователь передает JWT-токен в заголовке HTTP-запроса *Authentication*:\n\n```bash\nGET /profile\nAuthorization: Bearer xxxxx.yyyyy.zzzzz\n```\n\nВеб-приложение авторизует пользователя по JWT-токену и возвращает данные его профиля. Если не передать JWT-токен или передать некорректный, то веб-приложение возвращает HTTP-ответ со статусом *401 Unauthorized*.\n","prepared_readme":"В этой практике вас предстоит реализовать систему аутентификации и авторизации с использованием jwt-токенов. \n\nСхема работы веб-приложения следующая:\n\n## app/solution.go\n\n1. Пользователь отправляет HTTP-запрос на регистрацию с логином и паролем:\n\n```bash\nPOST /signup\n\n{\"email\": \"john@doe.com\", \"password\": \"qwerty123\"}\n```\n\nВеб-приложение хранит созданных пользователей в переменной `users map[string]User`, где ключ — это электронная почта пользователя, а значение — структура со всеми данными пользователя. Если пользователь с таким email уже существует, то возвращается HTTP-ответ со статусом *409 Conflict*. В случае успеха веб-приложение сохраняет пользователя в оперативной памяти и возвращает HTTP-ответ со статусом *200 OK*.\n\n2. После успешной регистрации пользователь может пройти аутентификацию. Для этого он отправляет следующий HTTP-запрос с логином и паролем:\n\n```bash\nPOST /signin\n\n{\"email\": \"john@doe.com\", \"password\": \"qwerty123\"}\n```\n\nЕсли пользователь отправил некорректные учетные данные, то веб-приложение возвращает HTTP-ответ со статусом *422 Unprocessable Entity*. В случае успеха веб-приложение генерирует JWT-токен для этого пользователя и возвращает HTTP-ответ со статусом *200 OK* и JWT-токеном в теле:\n\n```bash\nHTTP/1.1 200 OK\n\n{\"jwt_token\": \"xxxxx.yyyyy.zzzzz\"}\n```\n\nДля подписи JWT-токена необходимо использовать секретный ключ, который хранится в переменной `secretKey string`.\n\n3. Когда пользователь прошел аутентификацию он может запросить данные своего профиля. Для авторизации пользователь передает JWT-токен в заголовке HTTP-запроса *Authentication*:\n\n```bash\nGET /profile\nAuthorization: Bearer xxxxx.yyyyy.zzzzz\n```\n\nВеб-приложение авторизует пользователя по JWT-токену и возвращает данные его профиля. Если не передать JWT-токен или передать некорректный, то веб-приложение возвращает HTTP-ответ со статусом *401 Unauthorized*.\n","has_solution":true,"entity_name":"JWT-авторизация на сервере"},"units":[{"id":7225,"name":"theory","url":"/courses/go-web-development/lessons/auth/theory_unit"},{"id":7463,"name":"quiz","url":"/courses/go-web-development/lessons/auth/quiz_unit"},{"id":8222,"name":"exercise","url":"/courses/go-web-development/lessons/auth/exercise_unit"}],"links":[{"id":423674,"name":"JWT","url":"https://jwt.io/"},{"id":423675,"name":"Fiber JWT Middleware","url":"https://github.com/gofiber/contrib/tree/main/jwt"},{"id":423676,"name":"JWT-авторизация и сессии","url":"https://habr.com/ru/post/340146/\n"}],"ordered_units":[{"id":7225,"name":"theory","url":"/courses/go-web-development/lessons/auth/theory_unit"},{"id":7463,"name":"quiz","url":"/courses/go-web-development/lessons/auth/quiz_unit"},{"id":8222,"name":"exercise","url":"/courses/go-web-development/lessons/auth/exercise_unit"}],"id":3236,"slug":"auth","state":"approved","name":"JWT-авторизация на сервере","course_order":150,"goal":"Учимся настраивать JWT-авторизацию на сервере","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"Представим, что мы разрабатываем страницу с профилем пользователя в социальной сети. У пользователей есть возможность редактировать свои профили. Нам нужно определять, что HTTP-запрос на изменение профиля пришел от владельца профиля. Если не сделать такую проверку, то злоумышленники смогут изменять данные профилей других пользователей.\n\nВ этом уроке мы разберем тему JWT-авторизации и ее реализацию в Go. Это важная тема, потому что авторизация — это ключевая часть безопасности любого веб-приложения.\n\n## Аутентификация и авторизация\n\nПредставим, что мы хотим войти в свой аккаунт социальной сети. Веб-сайту нужно понять, что мы владеем этим аккаунтом. Для этого нам нужно предоставить корректные учетные данные. Обычно это логин и пароль. Веб-сайт проверяет эти данные и, если они верны, мы входим в свой аккаунт. Этот процесс называется **аутентификацией**:\n\n\n\nДопустим, после успешной аутентификации мы заходим на страницу со своим профилем. Так как мы уже были аутентифицированы, мы можем отправить запрос на редактирование данных профиля. Веб-сайт проверяет, что запрос пришел от владельца профиля, и позволяет изменять данные. Процесс проверки прав на осуществление действий называется **авторизацией**:\n\n\n\nТаким образом, аутентификация — это процесс проверки учетных данных, а авторизация — это процесс проверки прав на осуществление действий. В примере социальной сети аутентификация происходит при логине, а авторизация при каждом запросе после аутентификации.\n\nПроцесс авторизации происходит часто, и если допустить ошибку в системе авторизации, то злоумышленники смогут осуществлять действия от имени других пользователей. Например, сделать покупку от их имени или узнать конфиденциальную информацию. Поэтому важно знать безопасные и проверенные способы авторизации.\n\nДва самых популярных способа авторизации:\n\n* С помощью сессий\n* С помощью токенов\n\nУ каждого из этих способов есть свои уникальности, преимущества и недостатки. Ознакомиться более детально с ними можно в дополнительных материалах к уроку. Мы же рассмотрим на практике одну из реализаций авторизации с помощью токенов — JWT.\n\n## JWT-авторизация\n\n**JWT (JSON Web Token)** — это специальный формат токена, который позволяет безопасно передавать данные между клиентом и сервером. Например, клиентом может быть веб-браузер или мобильное приложение, сервером — сервер с Go веб-приложением.\n\nJWT-токен состоит из трех частей, которые разделены точкой:\n\n* **Header** или **заголовок** — информация о токене, тип токена и алгоритм шифрования\n* **Payload** или **полезные данные** — данные, которые мы хотим передать в токене. Например, имя пользователя, его роль, истекает ли токен. Эти данные представлены в виде JSON-объекта\n* **Signature** или **подпись** — подпись токена, которая позволяет проверить, что токен не был изменен\n\nОбычный токен имеет формат:\n\n```bash\nxxxxx.yyyyy.zzzzz\n```\n\nРассмотрим пример реального токена с разбором каждой части. Предположим, наше веб-приложение сгенерировало следующий JWT-токен:\n\n```bash\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c\n```\n\n### Header\n\nЗаголовок обычно состоит из JSON-объекта с двумя свойствами:\n\n* Тип токена, который в нашем случае — *JWT*\n* Алгоритм шифрования, который в нашем случае — *HMAC SHA256*\n\nДалее этот JSON-объект хэшируется с помощью *Base64Url-кодирования*, чтобы представить его в виде компактной строки.\n\nТаким образом, в нашем примере заголовок JWT-токена имеет следующее значение:\n\n```json\n{\n \"alg\": \"HS256\",\n \"typ\": \"JWT\"\n}\n```\n\n### Payload\n\nВторая часть токена — это полезная нагрузка в виде JSON-объекта. Она содержит различные данные об авторизованном пользователе. Значение этой части JWT-токена различно в каждом веб-приложении. Мы можем записать здесь любые публичные данные, которые могут быть полезны при авторизации.\n\nКак и заголовок JWT-токена, полезная нагрузка хэшируется с помощью *Base64Url-кодирования* для представления в виде компактной строки.\n\nВ нашем примере полезная нагрузка JWT-токена имеет следующее значение:\n\n```json\n{\n \"sub\": \"1234567890\",\n \"name\": \"John Doe\",\n \"iat\": 1516239022\n}\n```\n\nНазвания некоторых полей могут показаться непонятными с первого взгляда. Например, поле *sub* означает идентификатор пользователя, а поле *iat* — время создания токена. При составлении полей полезной нагрузки рекомендуется учитывать имена из документации [IANA (Internet Assigned Numbers Authority)](https://www.iana.org/assignments/jwt/jwt.xhtml). Это поможет избежать конфликтов имен с общепринятыми нормами. Поэтому мы использовали название *sub*, вместо привычного *user_id*.\n\nОсновная причина, почему названия полей в полезной нагрузке JWT-токена пишутся сокращенно, — это уменьшение размера токена после шифрования.\n\n### Signature\n\nЧтобы создать подпись, мы должны взять закодированный заголовок, закодированную полезную нагрузку, секретную строку и зашифровать эти данные. При этом нужно использовать алгоритм шифрования из заголовка JWT-токена.\n\nВ нашем примере для создания подписи используется алгоритм шифрования *HMAC SHA256* и секретная строка \"your-256-bit-secret\":\n\n```bash\nHMACSHA256(\n base64UrlEncode(header) + \".\" +\n base64UrlEncode(payload),\n your-256-bit-secret\n)\n```\n\nПодпись используются, чтобы проверить, что сообщение не было изменено при передаче. Она также позволяет подтвердить, что отправитель JWT-токена является тем, кем он представляется.\n\n### Собираем все части JWT-токена\n\nВ результате генерации JWT-токена получаются три Base64-URL-закодированные строки, которые разделены точкой. Значение JWT-токена является компактным и легко передается в HTTP-запросах.\n\nЕсли вы хотите попрактиковаться с JWT, можно использовать онлайн инструмент [jwt.io Debugger](https://jwt.io) для декодирования, проверки и генерации JWT-токенов.\n\nМы разобрали, из чего состоит JWT-токен. Теперь рассмотрим алгоритм работы с JWT-токеном в веб-приложениях.\n\n### Алгоритм работы с JWT-токеном\n\nПроцесс аутентификации и авторизации с JWT-токеном между веб-браузером и веб-приложением выглядит следующим образом:\n\n1. Веб-браузер отправляет запрос веб-приложению с логином и паролем\n2. Веб-приложение проверяет логин и пароль, и если они верны, то генерирует JWT-токен и отправляет его веб-браузеру. При генерации JWT-токена веб-приложение ставит подпись секретным ключом, который хранится только в веб-приложении\n3. Веб-браузер сохраняет JWT-токен и отправляет его вместе с каждым запросом в веб-приложение\n4. Веб-приложение проверяет JWT-токен и если он верный, то выполняет действие от имени авторизованного пользователя\n\nБезопасность коммуникации между веб-браузером и веб-приложением заключается в том, что токены генерируются и подписываются только со стороны веб-приложения. Злоумышленник не сможет подделать токен, так как не знает секретный ключ, который используется для подписи токена.\n\nПодпись токена происходит с помощью шифрования. С помощью подписи веб-приложение проверяет, что токен действительно был сгенерирован им. Шифрование может осуществляться различными алгоритмами. Например, алгоритмом `HS256` — HMAC с SHA-256.\n\nМы рассмотрели основы JWT-авторизации и поняли, что веб-приложение должно генерировать JWT-токены и подписывать их секретным ключом, который хранится только в веб-приложении. Рассмотрим, как это можно реализовать в Go-приложении.\n\n## JWT-авторизация в Go веб-приложении\n\nРеализуем аутентификацию и авторизацию в социальной сети, которая написана на Go с использованием микрофреймворка Fiber. Для этого реализуем следующие функции:\n\n* Регистрация пользователя\n* Вход в аккаунт — аутентификация\n* Получение информации о своем аккаунте — только для авторизованных пользователей\n\nЧтобы упростить работу, мы не будем описывать многие важные части в системах аутентификации пользователей. Например, мы не будем проверять HTTP-запросы, использовать базы данных или хэшировать хранимые пароли для безопасности. Вы можете улучшить эти части веб-приложения самостоятельно.\n\n### Регистрация аккаунта\n\nНачнем разработку социальной сети с функции регистрации аккаунта. Когда пользователь заходит на веб-сайт, он видит форму регистрации с тремя полями: имя, электронная почта и пароль. После заполнения формы пользователь нажимает кнопку «Зарегистрироваться», и веб-браузер отправляет HTTP-запрос `POST /register` в наше веб-приложение. Мы реализуем обработчик этого HTTP-запроса следующим образом:\n\n```go\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/gofiber/fiber/v2\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc main() {\n\tapp := fiber.New()\n\n\tauthHandler := &AuthHandler{&AuthStorage{map[string]User{}}}\n\n\tapp.Post(\"/register\", authHandler.Register)\n\n\tlogrus.Fatal(app.Listen(\":80\"))\n}\n\ntype (\n\t// Обработчик HTTP-запросов на регистрацию и аутентификацию пользователей\n\tAuthHandler struct {\n\t\tstorage *AuthStorage\n\t}\n\n\t// Хранилище зарегистрированных пользователей\n\t// Данные хранятся в оперативной памяти\n\tAuthStorage struct {\n\t\tusers map[string]User\n\t}\n\n\t// Структура данных с информацией о пользователе\n\tUser struct {\n\t\tEmail string\n\t\tName string\n\t\tpassword string\n\t}\n)\n\n// Структура HTTP-запроса на регистрацию пользователя\ntype RegisterRequest struct {\n\tEmail string `json:\"email\"`\n\tName string `json:\"name\"`\n\tPassword string `json:\"password\"`\n}\n\n// Обработчик HTTP-запросов на регистрацию пользователя\nfunc (h *AuthHandler) Register(c *fiber.Ctx) error {\n\tregReq := RegisterRequest{}\n\tif err := c.BodyParser(&regReq); err != nil {\n\t\treturn fmt.Errorf(\"body parser: %w\", err)\n\t}\n\n\t// Проверяем, что пользователь с таким email еще не зарегистрирован\n\tif _, exists := h.storage.users[regReq.Email]; exists {\n\t\treturn errors.New(\"the user already exists\")\n\t}\n\n\t// Сохраняем в память нового зарегистрированного пользователя\n\th.storage.users[regReq.Email] = User{\n\t\tEmail: regReq.Email,\n\t\tName: regReq.Name,\n\t\tpassword: regReq.Password,\n\t}\n\n\treturn c.SendStatus(fiber.StatusCreated)\n}\n```\n\nКогда приходит HTTP-запрос на регистрацию нового пользователя, веб-приложение проверяет, что в хранилище еще не существует пользователя с такой электронной почтой. Если пользователь с такой электронной почтой уже есть, то веб-приложение возвращает ошибку. В обратном случае все данные пользователя сохраняются в оперативной памяти веб-приложения.\n\nПосле регистрации пользователь может войти в свой аккаунт, и далее мы реализуем эту возможность.\n\n### Вход в аккаунт\n\nКогда пользователь заходит на страницу входа в аккаунт, он видит форму с двумя полями: электронная почта и пароль. Эти поля являются учетными данными пользователя. После заполнения формы пользователь нажимает кнопку «Войти», и веб-браузер отправляет HTTP-запрос `POST /login` в наше веб-приложение. Обработчик этого запроса будет выглядеть следующим образом:\n\n```go\n// Структура HTTP-запроса на вход в аккаунт\ntype LoginRequest struct {\n\tEmail string `json:\"email\"`\n\tPassword string `json:\"password\"`\n}\n\n// Структура HTTP-ответа на вход в аккаунт\n// В ответе содержится JWT-токен авторизованного пользователя\ntype LoginResponse struct {\n\tAccessToken string `json:\"access_token\"`\n}\n\nvar (\n\terrBadCredentials = errors.New(\"email or password is incorrect\")\n)\n\n// Секретный ключ для подписи JWT-токена\n// Необходимо хранить в безопасном месте\nvar jwtSecretKey = []byte(\"very-secret-key\")\n\n// Обработчик HTTP-запросов на вход в аккаунт\nfunc (h *AuthHandler) Login(c *fiber.Ctx) error {\n\tregReq := LoginRequest{}\n\tif err := c.BodyParser(&regReq); err != nil {\n\t\treturn fmt.Errorf(\"body parser: %w\", err)\n\t}\n\n\t// Ищем пользователя в памяти приложения по электронной почте\n\tuser, exists := h.storage.users[regReq.Email]\n\t// Если пользователь не найден, возвращаем ошибку\n\tif !exists {\n\t\treturn errBadCredentials\n\t}\n\t// Если пользователь найден, но у него другой пароль, возвращаем ошибку\n\tif user.password != regReq.Password {\n\t\treturn errBadCredentials\n\t}\n\n\t// Генерируем JWT-токен для пользователя,\n\t// который он будет использовать в будущих HTTP-запросах\n\n\t// Генерируем полезные данные, которые будут храниться в токене\n\tpayload := jwt.MapClaims{\n\t\t\"sub\": user.Email,\n\t\t\"exp\": time.Now().Add(time.Hour * 72).Unix(),\n\t}\n\n\t// Создаем новый JWT-токен и подписываем его по алгоритму HS256\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)\n\n\tt, err := token.SignedString(jwtSecretKey)\n\tif err != nil {\n\t\tlogrus.WithError(err).Error(\"JWT token signing\")\n\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t}\n\n\treturn c.JSON(LoginResponse{AccessToken: t})\n}\n```\n\nКогда приходит HTTP-запрос на вход в аккаунт, веб-приложение проверяет, что в хранилище существует пользователь с такой электронной почтой и паролем. Если учетные данные некорректны, то веб-приложение возвращает ошибку. В обратном случае пользователь успешно прошел аутентификацию, и веб-приложение генерирует и возвращает JWT-токен. Его пользователь будет использовать в будущих HTTP-запросах, чтобы авторизоваться.\n\nЧтобы сгенерировать JWT-токен, мы используем библиотеку [jwt-go](https://github.com/golang-jwt/jwt). Благодаря этому все тонкости формирования и шифрования токена скрыты от нас. Все, что от нас требуется, — это указать секретный ключ для подписи токена и полезные данные, которые будут храниться в токене.\n\nВ полезных данных JWT-токена мы записываем электронную почту пользователя как идентификатор. Также записываем время, когда этот токен будет недействителен. В нашем примере время жизни JWT-токена составляет 72 часа. Когда это время истечет, пользователь должен будет войти в аккаунт заново.\n\nТеперь в нашей социальной сети пользователи могут регистрироваться и аутентифицироваться — входить в свои аккаунты. Далее реализуем функцию получения информации о своем аккаунте, которая будет доступна только авторизованным пользователям.\n\n### Получение информации о своем аккаунте для авторизованных пользователей\n\nКогда пользователь прошел аутентификацию, он получил JWT-токен, который будет использовать в последующих HTTP-запросах для авторизации. Есть разные способы передавать токен в HTTP-запросе. Для этого можно использовать заголовок *Authorization*, параметр запроса или даже куки веб-браузера. Мы будем использовать заголовок *Authorization*, так как это наиболее распространенный способ передачи JWT-токена в HTTP-запросе.\n\nКогда пользователь заходит на свою страницу, веб-браузер отправляет HTTP-запрос `GET /profile` в наше веб-приложение. Обработчик этого запроса выглядит следующим образом:\n\n```go\npackage main\n\nimport (\n\t...\n\tjwtware \"github.com/gofiber/contrib/jwt\"\n\tjwt \"github.com/golang-jwt/jwt/v5\"\n\t...\n)\n\nconst (\n contextKeyUser = \"user\"\n)\n\nfunc main() {\n app := fiber.New()\n\n ...\n\n // Группа обработчиков, которые требуют авторизации\n authorizedGroup := app.Group(\"\")\n\tauthorizedGroup.Use(jwtware.New(jwtware.Config{\n\t SigningKey: jwtware.SigningKey{\n\t Key: jwtSecretKey,\n\t },\n\t ContextKey: contextKeyUser,\n\t}))\n authorizedGroup.Get(\"/profile\", userHandler.Profile)\n\n logrus.Fatal(app.Listen(\":80\"))\n}\n\n// Структура HTTP-ответа с информацией о пользователе\ntype ProfileResponse struct {\n\tEmail string `json:\"email\"`\n\tName string `json:\"name\"`\n}\n\nfunc jwtPayloadFromRequest(c *fiber.Ctx) (jwt.MapClaims, bool) {\n\tjwtToken, ok := c.Context().Value(contextKeyUser).(*jwt.Token)\n\tif !ok {\n\t\tlogrus.WithFields(logrus.Fields{\n\t\t\t\"jwt_token_context_value\": c.Context().Value(contextKeyUser),\n\t\t}).Error(\"wrong type of JWT token in context\")\n\t\treturn nil, false\n\t}\n\n\tpayload, ok := jwtToken.Claims.(jwt.MapClaims)\n\tif !ok {\n\t\tlogrus.WithFields(logrus.Fields{\n\t\t\t\"jwt_token_claims\": jwtToken.Claims,\n\t\t}).Error(\"wrong type of JWT token claims\")\n\t\treturn nil, false\n\t}\n\n\treturn payload, true\n}\n\n// Обработчик HTTP-запросов на получение информации о пользователе\nfunc (h *UserHandler) Profile(c *fiber.Ctx) error {\n\tjwtPayload, ok := jwtPayloadFromRequest(c)\n\tif !ok {\n\t\treturn c.SendStatus(fiber.StatusUnauthorized)\n\t}\n\n\tuserInfo, ok := h.storage.users[jwtPayload[\"sub\"].(string)]\n\tif !ok {\n\t\treturn errors.New(\"user not found\")\n\t}\n\n\treturn c.JSON(ProfileResponse{\n\t\tEmail: userInfo.Email,\n\t\tName: userInfo.Name,\n\t})\n}\n```\n\nТак как проверка авторизации может происходить во многих HTTP-обработчиках, мы вынесли ее на уровень посредников. В микрофреймворке Fiber для этого есть готовый посредник — [jwtware](https://github.com/gofiber/jwt).\n\nПри инициализации посредника мы указали два свойства:\n\n* *SigningKey* — секретный ключ JWT-токена\n* *ContextKey* — название поля, по которому хранится объект JWT-токена авторизованного пользователя. Этот объект можно использовать в любом обработчике группы `authorizedGroup`\n\nКогда приходит HTTP-запрос на получение информации о пользователе, веб-приложение проверяет, что в заголовке *Authorization* указан корректный JWT-токен. Если проверка прошла успешно, веб-приложение ищет пользователя в хранилище по электронной почте, которая записана в полезной нагрузке JWT-токена. Если пользователь был найден, то авторизация пройдена, и мы возвращаем в HTTP-ответе информацию об этом пользователе.\n\nМы реализовали все части аутентификации и авторизации нашей социальной сети. Теперь соберем все вместе и проверим, что веб-приложение работает.\n\n### Проверяем веб-приложение\n\nПолный код веб-приложения выглядит следующим образом:\n\n```go\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/gofiber/fiber/v2\"\n\tjwtware \"github.com/gofiber/contrib/jwt\"\n\tjwt \"github.com/golang-jwt/jwt/v5\"\n\t\"github.com/sirupsen/logrus\"\n\t\"time\"\n)\n\nconst (\n\tcontextKeyUser = \"user\"\n)\n\nfunc main() {\n\tapp := fiber.New()\n\n\tauthStorage := &AuthStorage{map[string]User{}}\n\tauthHandler := &AuthHandler{storage: authStorage}\n\tuserHandler := &UserHandler{storage: authStorage}\n\n\t// Группа обработчиков, которые доступны неавторизованным пользователям\n\tpublicGroup := app.Group(\"\")\n\tpublicGroup.Post(\"/register\", authHandler.Register)\n\tpublicGroup.Post(\"/login\", authHandler.Login)\n\n\t// Группа обработчиков, которые требуют авторизации\n\tauthorizedGroup := app.Group(\"\")\n\tauthorizedGroup.Use(jwtware.New(jwtware.Config{\n\t\tSigningKey: jwtware.SigningKey{\n\t\tKey: jwtSecretKey,\n\t\t},\n\t\tContextKey: contextKeyUser,\n\t}))\n\tauthorizedGroup.Get(\"/profile\", userHandler.Profile)\n\n\tlogrus.Fatal(app.Listen(\":80\"))\n}\n\ntype (\n\t// Обработчик HTTP-запросов на регистрацию и аутентификацию пользователей\n\tAuthHandler struct {\n\t\tstorage *AuthStorage\n\t}\n\n\t// Хранилище зарегистрированных пользователей\n\t// Данные хранятся в оперативной памяти\n\tAuthStorage struct {\n\t\tusers map[string]User\n\t}\n\n\t// Структура данных с информацией о пользователе\n\tUser struct {\n\t\tEmail string\n\t\tName string\n\t\tpassword string\n\t}\n)\n\n// Структура HTTP-запроса на регистрацию пользователя\ntype RegisterRequest struct {\n\tEmail string `json:\"email\"`\n\tName string `json:\"name\"`\n\tPassword string `json:\"password\"`\n}\n\n// Обработчик HTTP-запросов на регистрацию пользователя\nfunc (h *AuthHandler) Register(c *fiber.Ctx) error {\n\tregReq := RegisterRequest{}\n\tif err := c.BodyParser(&regReq); err != nil {\n\t\treturn fmt.Errorf(\"body parser: %w\", err)\n\t}\n\n\t// Проверяем, что пользователь с таким email еще не зарегистрирован\n\tif _, exists := h.storage.users[regReq.Email]; exists {\n\t\treturn errors.New(\"the user already exists\")\n\t}\n\n\t// Сохраняем в память нового зарегистрированного пользователя\n\th.storage.users[regReq.Email] = User{\n\t\tEmail: regReq.Email,\n\t\tName: regReq.Name,\n\t\tpassword: regReq.Password,\n\t}\n\n\treturn c.SendStatus(fiber.StatusCreated)\n}\n\n// Структура HTTP-запроса на вход в аккаунт\ntype LoginRequest struct {\n\tEmail string `json:\"email\"`\n\tPassword string `json:\"password\"`\n}\n\n// Структура HTTP-ответа на вход в аккаунт\n// В ответе содержится JWT-токен авторизованного пользователя\ntype LoginResponse struct {\n\tAccessToken string `json:\"access_token\"`\n}\n\nvar (\n\terrBadCredentials = errors.New(\"email or password is incorrect\")\n)\n\n// Секретный ключ для подписи JWT-токена\n// Необходимо хранить в безопасном месте\nvar jwtSecretKey = []byte(\"very-secret-key\")\n\n// Обработчик HTTP-запросов на вход в аккаунт\nfunc (h *AuthHandler) Login(c *fiber.Ctx) error {\n\tregReq := LoginRequest{}\n\tif err := c.BodyParser(&regReq); err != nil {\n\t\treturn fmt.Errorf(\"body parser: %w\", err)\n\t}\n\n\t// Ищем пользователя в памяти приложения по электронной почте\n\tuser, exists := h.storage.users[regReq.Email]\n\t// Если пользователь не найден, возвращаем ошибку\n\tif !exists {\n\t\treturn errBadCredentials\n\t}\n\t// Если пользователь найден, но у него другой пароль, возвращаем ошибку\n\tif user.password != regReq.Password {\n\t\treturn errBadCredentials\n\t}\n\n\t// Генерируем JWT-токен для пользователя,\n\t// который он будет использовать в будущих HTTP-запросах\n\n\t// Генерируем полезные данные, которые будут храниться в токене\n\tpayload := jwt.MapClaims{\n\t\t\"sub\": user.Email,\n\t\t\"exp\": time.Now().Add(time.Hour * 72).Unix(),\n\t}\n\n\t// Создаем новый JWT-токен и подписываем его по алгоритму HS256\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)\n\n\tt, err := token.SignedString(jwtSecretKey)\n\tif err != nil {\n\t\tlogrus.WithError(err).Error(\"JWT token signing\")\n\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t}\n\n\treturn c.JSON(LoginResponse{AccessToken: t})\n}\n\n// Обработчик HTTP-запросов, которые связаны с пользователем\ntype UserHandler struct {\n\tstorage *AuthStorage\n}\n\n// Структура HTTP-ответа с информацией о пользователе\ntype ProfileResponse struct {\n\tEmail string `json:\"email\"`\n\tName string `json:\"name\"`\n}\n\nfunc jwtPayloadFromRequest(c *fiber.Ctx) (jwt.MapClaims, bool) {\n\tjwtToken, ok := c.Context().Value(contextKeyUser).(*jwt.Token)\n\tif !ok {\n\t\tlogrus.WithFields(logrus.Fields{\n\t\t\t\"jwt_token_context_value\": c.Context().Value(contextKeyUser),\n\t\t}).Error(\"wrong type of JWT token in context\")\n\t\treturn nil, false\n\t}\n\n\tpayload, ok := jwtToken.Claims.(jwt.MapClaims)\n\tif !ok {\n\t\tlogrus.WithFields(logrus.Fields{\n\t\t\t\"jwt_token_claims\": jwtToken.Claims,\n\t\t}).Error(\"wrong type of JWT token claims\")\n\t\treturn nil, false\n\t}\n\n\treturn payload, true\n}\n\n// Обработчик HTTP-запросов на получение информации о пользователе\nfunc (h *UserHandler) Profile(c *fiber.Ctx) error {\n\tjwtPayload, ok := jwtPayloadFromRequest(c)\n\tif !ok {\n\t\treturn c.SendStatus(fiber.StatusUnauthorized)\n\t}\n\n\tuserInfo, ok := h.storage.users[jwtPayload[\"sub\"].(string)]\n\tif !ok {\n\t\treturn errors.New(\"user not found\")\n\t}\n\n\treturn c.JSON(ProfileResponse{\n\t\tEmail: userInfo.Email,\n\t\tName: userInfo.Name,\n\t})\n}\n```\n\nЗапускаем веб-приложение и отправляем запрос на регистрацию нового пользователя:\n\n```bash\ncurl --location --request POST 'http://localhost/register' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n \"email\": \"john@doe.com\",\n \"name\": \"John\",\n \"password\": \"pickles\"\n}'\n```\n\nВ ответ получаем:\n\n```bash\nHTTP/1.1 201 Created\n```\n\nЭто означает, что такого пользователя нет в хранилище, и он был успешно создан.\n\nТеперь попробуем пройти аутентификацию этого пользователя:\n\n```bash\ncurl --location --request POST 'http://localhost/login' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n \"email\": \"john@doe.com\",\n \"password\": \"pickles\"\n}'\n```\n\nВ ответ приходит:\n\n```bash\nHTTP/1.1 200 OK\n\n{\"access_token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Njg5NTEwMTcsInN1YiI6ImpvaG5AZG9lLmNvbSJ9.Q3k6yMFYtuzPyjoZYpIHibJQPey29QWmlHfwS2A3keM\"}\n```\n\nМы указали корректные учетные данные пользователя, поэтому аутентификация прошла успешно. В ответ веб-приложение вернуло JWT-токен, который мы будем использовать для авторизации при получении информации о пользователе.\n\nОтправляем запрос на получение информации о пользователе:\n\n```bash\ncurl -v 'http://localhost/profile' \\\n--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Njg5NTE0NDEsInN1YiI6ImpvaG5AZG9lLmNvbSJ9.e4yIoGzQC8ckcRISBjt4g18S2VEBiHrRhXG7N39-7qI'\n```\n\nВ ответ приходит:\n\n```bash\nHTTP/1.1 200 OK\n\n{\"email\":\"john@doe.com\",\"name\":\"John\"}\n```\n\nТак как мы передали JWT-токен в заголовке HTTP-запроса *Authorization*, веб-приложение авторизовало нас как пользователя *john@doe.com*, и вернуло информацию о нашем аккаунте.\n\nВ последнем запросе есть один интересный момент. Мы поставили перед значением токена слово *Bearer*:\n\n```bash\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Njg5NTE0NDEsInN1YiI6ImpvaG5AZG9lLmNvbSJ9.e4yIoGzQC8ckcRISBjt4g18S2VEBiHrRhXG7N39-7qI\n```\n\n**Bearer** переводится как носитель. Он дает веб-приложению понять, что в заголовке *Authorization* передан токен для авторизации. Принято считать, что это слово означает фразу: «Дай доступ к носителю этого токена». Если не указать слово *Bearer* перед значением, то авторизация не пройдет даже с корректным значением токена.\n\nМы реализовали функцию регистрации, аутентификации и получение информации об аккаунте авторизованного пользователя. Для авторизации мы использовали JWT-токен, который генерируются при входе в аккаунт на 72 часа и передается в заголовке HTTP-запроса *Authorization*.\n\n## Выводы\n\n* Аутентификация — это процесс проверки подлинности пользователя, который пытается получить доступ к веб-приложению\n* Авторизация — это процесс проверки прав пользователя на какое-либо действие в веб-приложении\n* Один из способов реализации системы авторизации — это JWT-токен. Токен генерируется и подписывается веб-приложением после успешной аутентификации и передается во всех последующих HTTP-запросах в заголовке *Authorization*\n* JWT-токен содержит информацию о пользователе и подпись, которая необходима для авторизации. Так как токен подписан секретным ключом в веб-приложении, то его может проверять на подлинность только это веб-приложение.\n"},"lessonMember":null,"courseMember":null,"course":{"start_lesson":{"exercise":null,"units":[{"id":7205,"name":"theory","url":"/courses/go-web-development/lessons/intro/theory_unit"}],"links":[{"id":423640,"name":"Установка Go","url":"https://go.dev/doc/install"},{"id":423641,"name":"Анатомия веб-сервиса от Highload","url":"https://highload.guide/blog/inside-webserver.html"},{"id":423642,"name":"Вытесняющий планировщик Go","url":"https://habr.com/ru/post/502506/"},{"id":423643,"name":"Сравнение производительности веб-сервера на Go с другими языками","url":"https://www.toptal.com/back-end/server-side-io-performance-node-php-java-go"},{"id":423644,"name":"Communicating sequential processes (теория о шаблонах взаимодействия в конкурентных системах)","url":"https://en.wikipedia.org/wiki/Communicating_sequential_processes"},{"id":423645,"name":"Конкурентность — Асинхронность","url":"https://habr.com/ru/post/319350/"}],"ordered_units":[{"id":7205,"name":"theory","url":"/courses/go-web-development/lessons/intro/theory_unit"}],"id":3218,"slug":"intro","state":"approved","name":"Введение","course_order":100,"goal":"Знакомимся с целями и задачами курса","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"На сегодняшний день интернет стал неотъемлемой частью жизни каждого из нас. Мы заказываем продукты, вещи, лекарства в интернете, смотрим фильмы в онлайн-кинотеатрах и общаемся с друзьями в социальных сетях. И этот список можно продолжать бесконечно. При этом в основе каждого из этих примеров лежит веб-приложение, которое выполняет определенную функцию.\n\nВеб-приложение — это программное обеспечение, в котором пользователь взаимодействует с веб-сервером при помощи веб-браузера. Например, Google — это веб-приложение для поиска информации в интернете, а Youtube — это веб-приложение для просмотра видео.\n\nВеб-приложения разделяются на два типа:\n\n* **Frontend (клиентская часть веб-приложения)** — отвечает за отображение информации пользователю и взаимодействие с ним. Чаще подразумевается взаимодействие в веб-браузере\n* **Backend (серверная часть веб-приложения)** — отвечает за управление данными и бизнес-логикой веб-приложения\n\nВ этом курсе мы сосредоточимся на backend, а именно на разработке веб-приложений на языке Go.\n\n## Почему именно Go\n\nGolang — это достаточно молодой язык программирования. Его разработали в Google с учетом следующих требований:\n\n* **Простота**. Язык Go должен быть простым в изучении и использовании. Чтобы быстро разрабатывать программное обеспечение, программисту нужно понимать, как работает язык и как его использовать\n* **Производительность**. Язык Go должен быть быстрым в работе. Веб-приложения компании Google — одни из самых высоконагруженных в мире. Поэтому им важно использовать язык программирования, который не уступит по производительности низкоуровневым языкам, таким как \"Си\"\n* **Параллелизм**. С современными веб-приложениями работают тысячи пользователей в один момент времени. Язык Go должен предоставлять удобные инструменты для эффективной параллельной обработки множества HTTP-запросов\n\nЭти требования были учтены при разработке языка Go. По этим причинам на сегодняшний день он активно используется при разработке социальных сетей, финтех и блокчейн платформ.\n\nВ таких проектах часто присутствуют большие нагрузки, обработка данных в режиме реального времени и микросервисная архитектура. И именно Go позволяет решить эти вопросы достаточно просто и эффективно.\n\n## Цели курса\n\nВ этом курсе мы научимся разрабатывать веб-приложения на языке Go с использованием микрофреймворка Fiber. Мы узнаем, как:\n\n* работать со стандартной библиотекой HTTP в Golang\n* использовать логирование в приложениях\n* читать запросы и отправлять ответы с микрофреймворком Fiber\n* описывать роутинг в Fiber веб-приложениях\n* сериализовать и десериализовать данные в JSON в Golang\n* строить слой хранения данных в Golang-приложении\n* проверять HTTP-запросы в Go\n* использовать middleware при обработке HTTP-запросов в Go\n* настраивать JWT-авторизацию на сервере\n* работать с шаблонами HTML-страниц в Go-приложениях\n* обрабатывать, логировать и возвращать ошибки клиенту\n"},"id":319,"slug":"go-web-development","challenges_count":3,"name":"Веб-разработка на Go","allow_indexing":true,"state":"approved","course_state":"finished","pricing_type":"paid","description":"На этом курсе вы изучите основы веб-разработки на Go. Вы узнаете, как обрабатывать запросы и формировать ответ, как работать сессиями, обрабатывать ошибки, что такое CRUD и как правильно работать с сущностями. В итоге вы научитесь создавать полноценные сайты на фреймворке Fiber, строить архитектуру веб-приложений и доставлять их до сервера. Веб-разработка на Fiber пригодится, если вы решите детально изучить принципы создания современных веб-приложений и создавать производительные приложения, которые обрабатывают тысячи запросов в секунду.","kind":"basic","updated_at":"2026-01-20T11:43:55.334Z","language":"golang","duration_cache":57240,"skills":["Разрабатывать веб-приложения на Go","Использовать микрофреймворк Fiber","Строить систему JWT-аутентификации на Go"],"keywords":[],"lessons_count":13,"cover":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NjkyMCwicHVyIjoiYmxvYl9pZCJ9fQ==--77dee61425670e714a2a183b8687d836b504fb63/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fZmlsbCI6WzYwMCw0MDBdfSwicHVyIjoidmFyaWF0aW9uIn19--6067466c2912ca31a17eddee04b8cf2a38c6ad17/image.png"},"recommendedLandings":[{"stack":{"id":63,"slug":"go-web-development","title":"Веб-разработка на Go","audience":"for_programmers","start_type":"anytime","pricing_model":"subscription","priority":"medium","kind":"track","state":"published","stack_state":"finished","order":1250,"duration_in_months":1},"id":113,"slug":"go-web-development","title":"Веб-разработка на Go","subtitle":"Навык создания веб-приложений на Go, необходимый для получения оффера на позицию Go-разработчика","subtitle_for_lists":"Изучите Go и разработку веб-приложений","locale":"ru","current":true,"duration_in_months_text":"1 месяц","stack_slug":"go-web-development","price_text":"от 3 900 ₽","duration_text":"1 месяц","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDAzNywicHVyIjoiYmxvYl9pZCJ9fQ==--c9a507d1b30c26185c312c95f68af4f0d8122afa/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Programmer-bro.png"},{"stack":{"id":225,"slug":"devops-engineer-from-scratch","title":"DevOps-инженер с нуля","audience":"for_beginners","start_type":"weekly","pricing_model":"purchase","priority":"high","kind":"profession","state":"published","stack_state":"not_finished","order":50,"duration_in_months":14},"id":355,"slug":"devops-engineer-from-scratch","title":"DevOps-инженер с нуля","subtitle":"Полное погружение в DevOps: весь стек от Linux до Kubernetes","subtitle_for_lists":"Полное погружение в DevOps: весь стек от Linux до Kubernetes","locale":"ru","current":true,"duration_in_months_text":"14 месяцев","stack_slug":"devops-engineer-from-scratch","price_text":"от 6 792 ₽","duration_text":"14 месяцев","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6Mzk2NSwicHVyIjoiYmxvYl9pZCJ9fQ==--84278a1852c9c6fb13b80a69f395bac6e47a422e/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Cloud%20sync-bro.png"},{"stack":{"id":229,"slug":"go","title":"GO-разработчик","audience":"for_programmers","start_type":"weekly","pricing_model":"purchase","priority":"high","kind":"profession","state":"published","stack_state":"not_finished","order":70,"duration_in_months":6},"id":363,"slug":"go","title":"Go-разработчик","subtitle":"Изучите Go, работу с БД, HTTP, конкурентность, горутины, многопоточность ","subtitle_for_lists":"Изучите Go, работу с БД, HTTP, конкурентность, горутины, многопоточность ","locale":"ru","current":true,"duration_in_months_text":"6 месяцев","stack_slug":"go","price_text":"от 4 509 ₽","duration_text":"6 месяцев","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzY2MywicHVyIjoiYmxvYl9pZCJ9fQ==--7e08550d68b7c2ad01e51109dfcf0861158d2e58/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Code%20review-amico.png"},{"stack":{"id":461,"slug":"go-from-scratch","title":"Go-разработчик с нуля","audience":"for_beginners","start_type":"weekly","pricing_model":"purchase","priority":"high","kind":"profession","state":"published","stack_state":"not_finished","order":250,"duration_in_months":9},"id":590,"slug":"go-from-scratch","title":"Go-разработчик с нуля","subtitle":"Изучите фреймворк Gin, горутины, многопоточность и работа с API","subtitle_for_lists":"Изучите фреймворк Gin, горутины, многопоточность и работа с API","locale":"ru","current":true,"duration_in_months_text":"9 месяцев","stack_slug":"go-from-scratch","price_text":null,"duration_text":"9 месяцев","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDg4MywicHVyIjoiYmxvYl9pZCJ9fQ==--9e90f48f058ab07777e0e79ecfcf2980f79fa277/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Code%20typing-cuate.png"}],"lessonMemberUnit":null,"accessToLearnUnitExists":false,"accessToCourseExists":false},"url":"/courses/go-web-development/lessons/auth/theory_unit","version":"0b0c6d4ebbd40fd58630a0dd89cc25544ccdf24e","encryptHistory":false,"clearHistory":false}"><style data-mantine-styles="true">:root, :host{--mantine-font-family: Arial, sans-serif;--mantine-font-family-headings: Arial, sans-serif;--mantine-heading-font-weight: normal;--mantine-radius-default: 0rem;--mantine-primary-color-filled: var(--mantine-color-indigo-filled);--mantine-primary-color-filled-hover: var(--mantine-color-indigo-filled-hover);--mantine-primary-color-light: var(--mantine-color-indigo-light);--mantine-primary-color-light-hover: var(--mantine-color-indigo-light-hover);--mantine-primary-color-light-color: var(--mantine-color-indigo-light-color);--mantine-spacing-xxl: calc(4rem * var(--mantine-scale));--mantine-font-size-xs: 12px;--mantine-font-size-sm: 14px;--mantine-font-size-md: 16px;--mantine-font-size-lg: clamp(16.0000px, calc(15.2727px + 0.2273vw), 18.0000px);--mantine-font-size-xl: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-display-3: clamp(32.0000px, calc(26.1818px + 1.8182vw), 48.0000px);--mantine-font-size-display-2: clamp(36.0000px, calc(25.8182px + 3.1818vw), 64.0000px);--mantine-font-size-display-1: clamp(40.0000px, calc(25.4545px + 4.5455vw), 80.0000px);--mantine-font-size-h1: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-font-size-h2: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-font-size-h3: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-font-size-h4: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-font-size-h5: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-h6: 1rem;--mantine-primary-color-0: var(--mantine-color-indigo-0);--mantine-primary-color-1: var(--mantine-color-indigo-1);--mantine-primary-color-2: var(--mantine-color-indigo-2);--mantine-primary-color-3: var(--mantine-color-indigo-3);--mantine-primary-color-4: var(--mantine-color-indigo-4);--mantine-primary-color-5: var(--mantine-color-indigo-5);--mantine-primary-color-6: var(--mantine-color-indigo-6);--mantine-primary-color-7: var(--mantine-color-indigo-7);--mantine-primary-color-8: var(--mantine-color-indigo-8);--mantine-primary-color-9: var(--mantine-color-indigo-9);--mantine-color-red-0: #ffeaea;--mantine-color-red-1: #fed4d4;--mantine-color-red-2: #f4a7a8;--mantine-color-red-3: #ec7878;--mantine-color-red-4: #e55050;--mantine-color-red-5: #e03131;--mantine-color-red-6: #e02829;--mantine-color-red-7: #c71a1c;--mantine-color-red-8: #b21218;--mantine-color-red-9: #9c0411;--mantine-color-violet-0: #fce9ff;--mantine-color-violet-1: #f1cfff;--mantine-color-violet-2: #e09bff;--mantine-color-violet-3: #d16fff;--mantine-color-violet-4: #be37fe;--mantine-color-violet-5: #b51afe;--mantine-color-violet-6: #b009ff;--mantine-color-violet-7: #9b00e4;--mantine-color-violet-8: #8a00cc;--mantine-color-violet-9: #7800b3;--mantine-color-indigo-0: #edecff;--mantine-color-indigo-1: #d6d5fe;--mantine-color-indigo-2: #aaa9f4;--mantine-color-indigo-3: #7b79eb;--mantine-color-indigo-4: #5451e4;--mantine-color-indigo-5: #3b37e0;--mantine-color-indigo-6: #2d2adf;--mantine-color-indigo-7: #1f1ec7;--mantine-color-indigo-8: #1819b2;--mantine-color-indigo-9: #0c149e;--mantine-color-cyan-0: #dffdff;--mantine-color-cyan-1: #caf5ff;--mantine-color-cyan-2: #99e8ff;--mantine-color-cyan-3: #64daff;--mantine-color-cyan-4: #3ccffe;--mantine-color-cyan-5: #24c8fe;--mantine-color-cyan-6: #00c2ff;--mantine-color-cyan-7: #00ade4;--mantine-color-cyan-8: #009acd;--mantine-color-cyan-9: #0085b5;--mantine-color-green-0: #e9fdec;--mantine-color-green-1: #d7f6dc;--mantine-color-green-2: #b0eab9;--mantine-color-green-3: #86df94;--mantine-color-green-4: #62d574;--mantine-color-green-5: #4ccf5f;--mantine-color-green-6: #3fcc54;--mantine-color-green-7: #2fb344;--mantine-color-green-8: #25a03b;--mantine-color-green-9: #138a2e;--mantine-color-yellow-0: #fff7e2;--mantine-color-yellow-1: #ffeecd;--mantine-color-yellow-2: #ffdc9c;--mantine-color-yellow-3: #ffc966;--mantine-color-yellow-4: #feb93a;--mantine-color-yellow-5: #feae1e;--mantine-color-yellow-6: #ffa90f;--mantine-color-yellow-8: #ca8200;--mantine-color-yellow-9: #af7000;--mantine-h1-font-size: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-h1-font-weight: normal;--mantine-h2-font-size: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-h2-font-weight: normal;--mantine-h3-font-size: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-h3-font-weight: normal;--mantine-h4-font-size: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-h4-font-weight: normal;--mantine-h5-font-size: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-h5-font-weight: normal;--mantine-h6-font-size: 1rem;--mantine-h6-font-weight: normal;}
:root[data-mantine-color-scheme="dark"], :host([data-mantine-color-scheme="dark"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-dark-filled: var(--mantine-color-dark-5);--mantine-color-dark-filled-hover: var(--mantine-color-dark-6);--mantine-color-dark-light: rgba(105, 105, 105, 0.15);--mantine-color-dark-light-hover: rgba(105, 105, 105, 0.2);--mantine-color-dark-light-color: var(--mantine-color-dark-0);--mantine-color-dark-outline: var(--mantine-color-dark-1);--mantine-color-dark-outline-hover: rgba(184, 184, 184, 0.05);--mantine-color-gray-filled: var(--mantine-color-gray-5);--mantine-color-gray-filled-hover: var(--mantine-color-gray-6);--mantine-color-gray-light: rgba(222, 226, 230, 0.15);--mantine-color-gray-light-hover: rgba(222, 226, 230, 0.2);--mantine-color-gray-light-color: var(--mantine-color-gray-0);--mantine-color-gray-outline: var(--mantine-color-gray-1);--mantine-color-gray-outline-hover: rgba(241, 243, 245, 0.05);--mantine-color-red-filled: var(--mantine-color-red-5);--mantine-color-red-filled-hover: var(--mantine-color-red-6);--mantine-color-red-light: rgba(236, 120, 120, 0.15);--mantine-color-red-light-hover: rgba(236, 120, 120, 0.2);--mantine-color-red-light-color: var(--mantine-color-red-0);--mantine-color-red-outline: var(--mantine-color-red-1);--mantine-color-red-outline-hover: rgba(254, 212, 212, 0.05);--mantine-color-pink-filled: var(--mantine-color-pink-5);--mantine-color-pink-filled-hover: var(--mantine-color-pink-6);--mantine-color-pink-light: rgba(250, 162, 193, 0.15);--mantine-color-pink-light-hover: rgba(250, 162, 193, 0.2);--mantine-color-pink-light-color: var(--mantine-color-pink-0);--mantine-color-pink-outline: var(--mantine-color-pink-1);--mantine-color-pink-outline-hover: rgba(255, 222, 235, 0.05);--mantine-color-grape-filled: var(--mantine-color-grape-5);--mantine-color-grape-filled-hover: var(--mantine-color-grape-6);--mantine-color-grape-light: rgba(229, 153, 247, 0.15);--mantine-color-grape-light-hover: rgba(229, 153, 247, 0.2);--mantine-color-grape-light-color: var(--mantine-color-grape-0);--mantine-color-grape-outline: var(--mantine-color-grape-1);--mantine-color-grape-outline-hover: rgba(243, 217, 250, 0.05);--mantine-color-violet-filled: var(--mantine-color-violet-5);--mantine-color-violet-filled-hover: var(--mantine-color-violet-6);--mantine-color-violet-light: rgba(209, 111, 255, 0.15);--mantine-color-violet-light-hover: rgba(209, 111, 255, 0.2);--mantine-color-violet-light-color: var(--mantine-color-violet-0);--mantine-color-violet-outline: var(--mantine-color-violet-1);--mantine-color-violet-outline-hover: rgba(241, 207, 255, 0.05);--mantine-color-indigo-filled: var(--mantine-color-indigo-5);--mantine-color-indigo-filled-hover: var(--mantine-color-indigo-6);--mantine-color-indigo-light: rgba(123, 121, 235, 0.15);--mantine-color-indigo-light-hover: rgba(123, 121, 235, 0.2);--mantine-color-indigo-light-color: var(--mantine-color-indigo-0);--mantine-color-indigo-outline: var(--mantine-color-indigo-1);--mantine-color-indigo-outline-hover: rgba(214, 213, 254, 0.05);--mantine-color-blue-filled: var(--mantine-color-blue-5);--mantine-color-blue-filled-hover: var(--mantine-color-blue-6);--mantine-color-blue-light: rgba(116, 192, 252, 0.15);--mantine-color-blue-light-hover: rgba(116, 192, 252, 0.2);--mantine-color-blue-light-color: var(--mantine-color-blue-0);--mantine-color-blue-outline: var(--mantine-color-blue-1);--mantine-color-blue-outline-hover: rgba(208, 235, 255, 0.05);--mantine-color-cyan-filled: var(--mantine-color-cyan-5);--mantine-color-cyan-filled-hover: var(--mantine-color-cyan-6);--mantine-color-cyan-light: rgba(100, 218, 255, 0.15);--mantine-color-cyan-light-hover: rgba(100, 218, 255, 0.2);--mantine-color-cyan-light-color: var(--mantine-color-cyan-0);--mantine-color-cyan-outline: var(--mantine-color-cyan-1);--mantine-color-cyan-outline-hover: rgba(202, 245, 255, 0.05);--mantine-color-teal-filled: var(--mantine-color-teal-5);--mantine-color-teal-filled-hover: var(--mantine-color-teal-6);--mantine-color-teal-light: rgba(99, 230, 190, 0.15);--mantine-color-teal-light-hover: rgba(99, 230, 190, 0.2);--mantine-color-teal-light-color: var(--mantine-color-teal-0);--mantine-color-teal-outline: var(--mantine-color-teal-1);--mantine-color-teal-outline-hover: rgba(195, 250, 232, 0.05);--mantine-color-green-filled: var(--mantine-color-green-5);--mantine-color-green-filled-hover: var(--mantine-color-green-6);--mantine-color-green-light: rgba(134, 223, 148, 0.15);--mantine-color-green-light-hover: rgba(134, 223, 148, 0.2);--mantine-color-green-light-color: var(--mantine-color-green-0);--mantine-color-green-outline: var(--mantine-color-green-1);--mantine-color-green-outline-hover: rgba(215, 246, 220, 0.05);--mantine-color-lime-filled: var(--mantine-color-lime-5);--mantine-color-lime-filled-hover: var(--mantine-color-lime-6);--mantine-color-lime-light: rgba(192, 235, 117, 0.15);--mantine-color-lime-light-hover: rgba(192, 235, 117, 0.2);--mantine-color-lime-light-color: var(--mantine-color-lime-0);--mantine-color-lime-outline: var(--mantine-color-lime-1);--mantine-color-lime-outline-hover: rgba(233, 250, 200, 0.05);--mantine-color-yellow-filled: var(--mantine-color-yellow-5);--mantine-color-yellow-filled-hover: var(--mantine-color-yellow-6);--mantine-color-yellow-light: rgba(255, 201, 102, 0.15);--mantine-color-yellow-light-hover: rgba(255, 201, 102, 0.2);--mantine-color-yellow-light-color: var(--mantine-color-yellow-0);--mantine-color-yellow-outline: var(--mantine-color-yellow-1);--mantine-color-yellow-outline-hover: rgba(255, 238, 205, 0.05);--mantine-color-orange-filled: var(--mantine-color-orange-5);--mantine-color-orange-filled-hover: var(--mantine-color-orange-6);--mantine-color-orange-light: rgba(255, 192, 120, 0.15);--mantine-color-orange-light-hover: rgba(255, 192, 120, 0.2);--mantine-color-orange-light-color: var(--mantine-color-orange-0);--mantine-color-orange-outline: var(--mantine-color-orange-1);--mantine-color-orange-outline-hover: rgba(255, 232, 204, 0.05);--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-9) 0%, var(--mantine-color-cyan-7) 100%);--app-color-surface: #2e2e2e;}
:root[data-mantine-color-scheme="light"], :host([data-mantine-color-scheme="light"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-red-light: rgba(224, 40, 41, 0.1);--mantine-color-red-light-hover: rgba(224, 40, 41, 0.12);--mantine-color-red-outline-hover: rgba(224, 40, 41, 0.05);--mantine-color-violet-light: rgba(176, 9, 255, 0.1);--mantine-color-violet-light-hover: rgba(176, 9, 255, 0.12);--mantine-color-violet-outline-hover: rgba(176, 9, 255, 0.05);--mantine-color-indigo-light: rgba(45, 42, 223, 0.1);--mantine-color-indigo-light-hover: rgba(45, 42, 223, 0.12);--mantine-color-indigo-outline-hover: rgba(45, 42, 223, 0.05);--mantine-color-cyan-light: rgba(0, 194, 255, 0.1);--mantine-color-cyan-light-hover: rgba(0, 194, 255, 0.12);--mantine-color-cyan-outline-hover: rgba(0, 194, 255, 0.05);--mantine-color-green-light: rgba(63, 204, 84, 0.1);--mantine-color-green-light-hover: rgba(63, 204, 84, 0.12);--mantine-color-green-outline-hover: rgba(63, 204, 84, 0.05);--mantine-color-yellow-light: rgba(255, 169, 15, 0.1);--mantine-color-yellow-light-hover: rgba(255, 169, 15, 0.12);--mantine-color-yellow-outline-hover: rgba(255, 169, 15, 0.05);--app-color-surface: #f1f3f5;--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-5) 100%);}</style><style data-mantine-styles="classes">@media (max-width: 35.99375em) {.mantine-visible-from-xs {display: none !important;}}@media (min-width: 36em) {.mantine-hidden-from-xs {display: none !important;}}@media (max-width: 47.99375em) {.mantine-visible-from-sm {display: none !important;}}@media (min-width: 48em) {.mantine-hidden-from-sm {display: none !important;}}@media (max-width: 61.99375em) {.mantine-visible-from-md {display: none !important;}}@media (min-width: 62em) {.mantine-hidden-from-md {display: none !important;}}@media (max-width: 74.99375em) {.mantine-visible-from-lg {display: none !important;}}@media (min-width: 75em) {.mantine-hidden-from-lg {display: none !important;}}@media (max-width: 87.99375em) {.mantine-visible-from-xl {display: none !important;}}@media (min-width: 88em) {.mantine-hidden-from-xl {display: none !important;}}</style><div style="position:absolute;top:0rem" class=""></div><div style="max-width:var(--container-size-xl);height:100%;min-height:0rem" class=""><style data-mantine-styles="inline">.__m__-_R_5ub_{--grid-gutter:0rem;}</style><div style="height:100%;min-height:0rem" class="m_410352e9 mantine-Grid-root __m__-_R_5ub_"><div class="m_dee7bd2f mantine-Grid-inner" style="height:100%"><style data-mantine-styles="inline">.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:91.66666666666667%;--col-max-width:91.66666666666667%;}@media(min-width: 48em){.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:83.33333333333334%;--col-max-width:83.33333333333334%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem;display:flex" class="m_96bdd299 mantine-Grid-col __m__-_R_rdub_"><style data-mantine-styles="inline">.__m__-_R_6qrdub_{margin-top:0rem;padding-inline:var(--mantine-spacing-xs);width:100%;}@media(min-width: 48em){.__m__-_R_6qrdub_{margin-top:var(--mantine-spacing-xl);width:80%;}}@media(min-width: 62em){.__m__-_R_6qrdub_{padding-inline:var(--mantine-spacing-xl);}}</style><div style="margin-inline:auto;max-width:var(--mantine-breakpoint-xl)" class="__m__-_R_6qrdub_"><div style="color:var(--mantine-color-dimmed)" class="m_4451eb3a mantine-Center-root" data-inline="true"><div style="--ti-size:var(--ti-size-xs);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;margin-inline-end:calc(0.125rem * var(--mantine-scale));color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="xs"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-lock "><path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6"></path><path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path><path d="M8 11v-4a4 4 0 1 1 8 0v4"></path></svg></div><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Веб-разработка на Go</p></div><h1 style="--title-fw:var(--mantine-h1-font-weight);--title-lh:var(--mantine-h1-line-height);--title-fz:var(--mantine-h1-font-size);margin-bottom:var(--mantine-spacing-xl)" class="m_8a5d1357 mantine-Title-root" data-order="1">Теория: JWT-авторизация на сервере</h1><script type="application/ld+json">{"@context":"https://schema.org","@type":"LearningResource","name":"JWT-авторизация на сервере","inLanguage":"ru","isPartOf":{"@type":"LearningResource","name":"Веб-разработка на Go"},"isAccessibleForFree":"False","hasPart":{"@type":"WebPageElement","isAccessibleForFree":"False","cssSelector":".paywalled"}}</script><div class=""><div style="--alert-color:var(--mantine-color-indigo-light-color);margin-bottom:var(--mantine-spacing-lg);font-size:var(--mantine-font-size-lg)" class="m_66836ed3 mantine-Alert-root" id="mantine-_R_remqrdub_" role="alert" aria-describedby="mantine-_R_remqrdub_-body" aria-labelledby="mantine-_R_remqrdub_-title"><div class="m_a5d60502 mantine-Alert-wrapper"><div class="m_667f2a6a mantine-Alert-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-rocket "><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3"></path><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3"></path><path d="M14 9a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path></svg></div><div class="m_667c2793 mantine-Alert-body"><div class="m_6a03f287 mantine-Alert-title"><span id="mantine-_R_remqrdub_-title" class="m_698f4f23 mantine-Alert-label">Полный доступ к материалам</span></div><div id="mantine-_R_remqrdub_-body" class="m_7fa78076 mantine-Alert-message"><div style="--group-gap:var(--mantine-spacing-md);--group-align:center;--group-justify:space-between;--group-wrap:wrap" class="m_4081bf90 mantine-Group-root"><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Зарегистрируйтесь и получите доступ к этому и десяткам других курсов</p><a style="--button-height:var(--button-height-xs);--button-padding-x:var(--button-padding-x-xs);--button-fz:var(--mantine-font-size-xs);--button-bg:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-hover:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-color:var(--mantine-color-white);--button-bd:none" class="mantine-focus-auto mantine-active m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root" data-variant="gradient" data-size="xs" href="/u/new"><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">Зарегистрироваться</span></span></a></div></div></div></div></div><div class="paywalled m_d08caa0 mantine-Typography-root"><p>Представим, что мы разрабатываем страницу с профилем пользователя в социальной сети. У пользователей есть возможность редактировать свои профили. Нам нужно определять, что HTTP-запрос на изменение профиля пришел от владельца профиля. Если не сделать такую проверку, то злоумышленники смогут изменять данные профилей других пользователей.</p>
<p>В этом уроке мы разберем тему JWT-авторизации и ее реализацию в Go. Это важная тема, потому что авторизация — это ключевая часть безопасности любого веб-приложения.</p>
<h2 id="heading-2-1">Аутентификация и авторизация</h2>
<p>Представим, что мы хотим войти в свой аккаунт социальной сети. Веб-сайту нужно понять, что мы владеем этим аккаунтом. Для этого нам нужно предоставить корректные учетные данные. Обычно это логин и пароль. Веб-сайт проверяет эти данные и, если они верны, мы входим в свой аккаунт. Этот процесс называется <strong>аутентификацией</strong>:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6Njk2MCwicHVyIjoiYmxvYl9pZCJ9fQ==--c56973ba52f8eba755f539aaae063c24b2b99cb2/auth1.jpg" alt="set" loading="lazy"/></p>
<p>Допустим, после успешной аутентификации мы заходим на страницу со своим профилем. Так как мы уже были аутентифицированы, мы можем отправить запрос на редактирование данных профиля. Веб-сайт проверяет, что запрос пришел от владельца профиля, и позволяет изменять данные. Процесс проверки прав на осуществление действий называется <strong>авторизацией</strong>:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6Njk2MSwicHVyIjoiYmxvYl9pZCJ9fQ==--ebb51da709b39055d4e64c5d5f88b6d6e7c6f4a9/auth2.jpg" alt="set" loading="lazy"/></p>
<p>Таким образом, аутентификация — это процесс проверки учетных данных, а авторизация — это процесс проверки прав на осуществление действий. В примере социальной сети аутентификация происходит при логине, а авторизация при каждом запросе после аутентификации.</p>
<p>Процесс авторизации происходит часто, и если допустить ошибку в системе авторизации, то злоумышленники смогут осуществлять действия от имени других пользователей. Например, сделать покупку от их имени или узнать конфиденциальную информацию. Поэтому важно знать безопасные и проверенные способы авторизации.</p>
<p>Два самых популярных способа авторизации:</p>
<ul>
<li>С помощью сессий</li>
<li>С помощью токенов</li>
</ul>
<p>У каждого из этих способов есть свои уникальности, преимущества и недостатки. Ознакомиться более детально с ними можно в дополнительных материалах к уроку. Мы же рассмотрим на практике одну из реализаций авторизации с помощью токенов — JWT.</p>
<h2 id="heading-2-2">JWT-авторизация</h2>
<p><strong>JWT (JSON Web Token)</strong> — это специальный формат токена, который позволяет безопасно передавать данные между клиентом и сервером. Например, клиентом может быть веб-браузер или мобильное приложение, сервером — сервер с Go веб-приложением.</p>
<p>JWT-токен состоит из трех частей, которые разделены точкой:</p>
<ul>
<li><strong>Header</strong> или <strong>заголовок</strong> — информация о токене, тип токена и алгоритм шифрования</li>
<li><strong>Payload</strong> или <strong>полезные данные</strong> — данные, которые мы хотим передать в токене. Например, имя пользователя, его роль, истекает ли токен. Эти данные представлены в виде JSON-объекта</li>
<li><strong>Signature</strong> или <strong>подпись</strong> — подпись токена, которая позволяет проверить, что токен не был изменен</li>
</ul>
<p>Обычный токен имеет формат:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">xxxxx.yyyyy.zzzzz</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Рассмотрим пример реального токена с разбором каждой части. Предположим, наше веб-приложение сгенерировало следующий JWT-токен:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<h3 id="heading-3-3">Header</h3>
<p>Заголовок обычно состоит из JSON-объекта с двумя свойствами:</p>
<ul>
<li>Тип токена, который в нашем случае — <em>JWT</em></li>
<li>Алгоритм шифрования, который в нашем случае — <em>HMAC SHA256</em></li>
</ul>
<p>Далее этот JSON-объект хэшируется с помощью <em>Base64Url-кодирования</em>, чтобы представить его в виде компактной строки.</p>
<p>Таким образом, в нашем примере заголовок JWT-токена имеет следующее значение:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">{
"alg": "HS256",
"typ": "JWT"
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<h3 id="heading-3-4">Payload</h3>
<p>Вторая часть токена — это полезная нагрузка в виде JSON-объекта. Она содержит различные данные об авторизованном пользователе. Значение этой части JWT-токена различно в каждом веб-приложении. Мы можем записать здесь любые публичные данные, которые могут быть полезны при авторизации.</p>
<p>Как и заголовок JWT-токена, полезная нагрузка хэшируется с помощью <em>Base64Url-кодирования</em> для представления в виде компактной строки.</p>
<p>В нашем примере полезная нагрузка JWT-токена имеет следующее значение:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Названия некоторых полей могут показаться непонятными с первого взгляда. Например, поле <em>sub</em> означает идентификатор пользователя, а поле <em>iat</em> — время создания токена. При составлении полей полезной нагрузки рекомендуется учитывать имена из документации <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://www.iana.org/assignments/jwt/jwt.xhtml" rel="noopener noreferrer" target="_blank">IANA (Internet Assigned Numbers Authority)</a>. Это поможет избежать конфликтов имен с общепринятыми нормами. Поэтому мы использовали название <em>sub</em>, вместо привычного <em>user_id</em>.</p>
<p>Основная причина, почему названия полей в полезной нагрузке JWT-токена пишутся сокращенно, — это уменьшение размера токена после шифрования.</p>
<h3 id="heading-3-5">Signature</h3>
<p>Чтобы создать подпись, мы должны взять закодированный заголовок, закодированную полезную нагрузку, секретную строку и зашифровать эти данные. При этом нужно использовать алгоритм шифрования из заголовка JWT-токена.</p>
<p>В нашем примере для создания подписи используется алгоритм шифрования <em>HMAC SHA256</em> и секретная строка "your-256-bit-secret":</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Подпись используются, чтобы проверить, что сообщение не было изменено при передаче. Она также позволяет подтвердить, что отправитель JWT-токена является тем, кем он представляется.</p>
<h3 id="heading-3-6">Собираем все части JWT-токена</h3>
<p>В результате генерации JWT-токена получаются три Base64-URL-закодированные строки, которые разделены точкой. Значение JWT-токена является компактным и легко передается в HTTP-запросах.</p>
<p>Если вы хотите попрактиковаться с JWT, можно использовать онлайн инструмент <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://jwt.io" rel="noopener noreferrer" target="_blank">jwt.io Debugger</a> для декодирования, проверки и генерации JWT-токенов.</p>
<p>Мы разобрали, из чего состоит JWT-токен. Теперь рассмотрим алгоритм работы с JWT-токеном в веб-приложениях.</p>
<h3 id="heading-3-7">Алгоритм работы с JWT-токеном</h3>
<p>Процесс аутентификации и авторизации с JWT-токеном между веб-браузером и веб-приложением выглядит следующим образом:</p>
<ol>
<li>Веб-браузер отправляет запрос веб-приложению с логином и паролем</li>
<li>Веб-приложение проверяет логин и пароль, и если они верны, то генерирует JWT-токен и отправляет его веб-браузеру. При генерации JWT-токена веб-приложение ставит подпись секретным ключом, который хранится только в веб-приложении</li>
<li>Веб-браузер сохраняет JWT-токен и отправляет его вместе с каждым запросом в веб-приложение</li>
<li>Веб-приложение проверяет JWT-токен и если он верный, то выполняет действие от имени авторизованного пользователя</li>
</ol>
<p>Безопасность коммуникации между веб-браузером и веб-приложением заключается в том, что токены генерируются и подписываются только со стороны веб-приложения. Злоумышленник не сможет подделать токен, так как не знает секретный ключ, который используется для подписи токена.</p>
<p>Подпись токена происходит с помощью шифрования. С помощью подписи веб-приложение проверяет, что токен действительно был сгенерирован им. Шифрование может осуществляться различными алгоритмами. Например, алгоритмом <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">HS256</code> — HMAC с SHA-256.</p>
<p>Мы рассмотрели основы JWT-авторизации и поняли, что веб-приложение должно генерировать JWT-токены и подписывать их секретным ключом, который хранится только в веб-приложении. Рассмотрим, как это можно реализовать в Go-приложении.</p>
<h2 id="heading-2-8">JWT-авторизация в Go веб-приложении</h2>
<p>Реализуем аутентификацию и авторизацию в социальной сети, которая написана на Go с использованием микрофреймворка Fiber. Для этого реализуем следующие функции:</p>
<ul>
<li>Регистрация пользователя</li>
<li>Вход в аккаунт — аутентификация</li>
<li>Получение информации о своем аккаунте — только для авторизованных пользователей</li>
</ul>
<p>Чтобы упростить работу, мы не будем описывать многие важные части в системах аутентификации пользователей. Например, мы не будем проверять HTTP-запросы, использовать базы данных или хэшировать хранимые пароли для безопасности. Вы можете улучшить эти части веб-приложения самостоятельно.</p>
<h3 id="heading-3-9">Регистрация аккаунта</h3>
<p>Начнем разработку социальной сети с функции регистрации аккаунта. Когда пользователь заходит на веб-сайт, он видит форму регистрации с тремя полями: имя, электронная почта и пароль. После заполнения формы пользователь нажимает кнопку «Зарегистрироваться», и веб-браузер отправляет HTTP-запрос <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">POST /register</code> в наше веб-приложение. Мы реализуем обработчик этого HTTP-запроса следующим образом:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">package main
import (
"errors"
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
)
func main() {
app := fiber.New()
authHandler := &AuthHandler{&AuthStorage{map[string]User{}}}
app.Post("/register", authHandler.Register)
logrus.Fatal(app.Listen(":80"))
}
type (
// Обработчик HTTP-запросов на регистрацию и аутентификацию пользователей
AuthHandler struct {
storage *AuthStorage
}
// Хранилище зарегистрированных пользователей
// Данные хранятся в оперативной памяти
AuthStorage struct {
users map[string]User
}
// Структура данных с информацией о пользователе
User struct {
Email string
Name string
password string
}
)
// Структура HTTP-запроса на регистрацию пользователя
type RegisterRequest struct {
Email string `json:"email"`
Name string `json:"name"`
Password string `json:"password"`
}
// Обработчик HTTP-запросов на регистрацию пользователя
func (h *AuthHandler) Register(c *fiber.Ctx) error {
regReq := RegisterRequest{}
if err := c.BodyParser(&regReq); err != nil {
return fmt.Errorf("body parser: %w", err)
}
// Проверяем, что пользователь с таким email еще не зарегистрирован
if _, exists := h.storage.users[regReq.Email]; exists {
return errors.New("the user already exists")
}
// Сохраняем в память нового зарегистрированного пользователя
h.storage.users[regReq.Email] = User{
Email: regReq.Email,
Name: regReq.Name,
password: regReq.Password,
}
return c.SendStatus(fiber.StatusCreated)
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Когда приходит HTTP-запрос на регистрацию нового пользователя, веб-приложение проверяет, что в хранилище еще не существует пользователя с такой электронной почтой. Если пользователь с такой электронной почтой уже есть, то веб-приложение возвращает ошибку. В обратном случае все данные пользователя сохраняются в оперативной памяти веб-приложения.</p>
<p>После регистрации пользователь может войти в свой аккаунт, и далее мы реализуем эту возможность.</p>
<h3 id="heading-3-10">Вход в аккаунт</h3>
<p>Когда пользователь заходит на страницу входа в аккаунт, он видит форму с двумя полями: электронная почта и пароль. Эти поля являются учетными данными пользователя. После заполнения формы пользователь нажимает кнопку «Войти», и веб-браузер отправляет HTTP-запрос <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">POST /login</code> в наше веб-приложение. Обработчик этого запроса будет выглядеть следующим образом:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">// Структура HTTP-запроса на вход в аккаунт
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
// Структура HTTP-ответа на вход в аккаунт
// В ответе содержится JWT-токен авторизованного пользователя
type LoginResponse struct {
AccessToken string `json:"access_token"`
}
var (
errBadCredentials = errors.New("email or password is incorrect")
)
// Секретный ключ для подписи JWT-токена
// Необходимо хранить в безопасном месте
var jwtSecretKey = []byte("very-secret-key")
// Обработчик HTTP-запросов на вход в аккаунт
func (h *AuthHandler) Login(c *fiber.Ctx) error {
regReq := LoginRequest{}
if err := c.BodyParser(&regReq); err != nil {
return fmt.Errorf("body parser: %w", err)
}
// Ищем пользователя в памяти приложения по электронной почте
user, exists := h.storage.users[regReq.Email]
// Если пользователь не найден, возвращаем ошибку
if !exists {
return errBadCredentials
}
// Если пользователь найден, но у него другой пароль, возвращаем ошибку
if user.password != regReq.Password {
return errBadCredentials
}
// Генерируем JWT-токен для пользователя,
// который он будет использовать в будущих HTTP-запросах
// Генерируем полезные данные, которые будут храниться в токене
payload := jwt.MapClaims{
"sub": user.Email,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
// Создаем новый JWT-токен и подписываем его по алгоритму HS256
token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
t, err := token.SignedString(jwtSecretKey)
if err != nil {
logrus.WithError(err).Error("JWT token signing")
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(LoginResponse{AccessToken: t})
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Когда приходит HTTP-запрос на вход в аккаунт, веб-приложение проверяет, что в хранилище существует пользователь с такой электронной почтой и паролем. Если учетные данные некорректны, то веб-приложение возвращает ошибку. В обратном случае пользователь успешно прошел аутентификацию, и веб-приложение генерирует и возвращает JWT-токен. Его пользователь будет использовать в будущих HTTP-запросах, чтобы авторизоваться.</p>
<p>Чтобы сгенерировать JWT-токен, мы используем библиотеку <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://github.com/golang-jwt/jwt" rel="noopener noreferrer" target="_blank">jwt-go</a>. Благодаря этому все тонкости формирования и шифрования токена скрыты от нас. Все, что от нас требуется, — это указать секретный ключ для подписи токена и полезные данные, которые будут храниться в токене.</p>
<p>В полезных данных JWT-токена мы записываем электронную почту пользователя как идентификатор. Также записываем время, когда этот токен будет недействителен. В нашем примере время жизни JWT-токена составляет 72 часа. Когда это время истечет, пользователь должен будет войти в аккаунт заново.</p>
<p>Теперь в нашей социальной сети пользователи могут регистрироваться и аутентифицироваться — входить в свои аккаунты. Далее реализуем функцию получения информации о своем аккаунте, которая будет доступна только авторизованным пользователям.</p>
<h3 id="heading-3-11">Получение информации о своем аккаунте для авторизованных пользователей</h3>
<p>Когда пользователь прошел аутентификацию, он получил JWT-токен, который будет использовать в последующих HTTP-запросах для авторизации. Есть разные способы передавать токен в HTTP-запросе. Для этого можно использовать заголовок <em>Authorization</em>, параметр запроса или даже куки веб-браузера. Мы будем использовать заголовок <em>Authorization</em>, так как это наиболее распространенный способ передачи JWT-токена в HTTP-запросе.</p>
<p>Когда пользователь заходит на свою страницу, веб-браузер отправляет HTTP-запрос <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">GET /profile</code> в наше веб-приложение. Обработчик этого запроса выглядит следующим образом:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">package main
import (
...
jwtware "github.com/gofiber/contrib/jwt"
jwt "github.com/golang-jwt/jwt/v5"
...
)
const (
contextKeyUser = "user"
)
func main() {
app := fiber.New()
...
// Группа обработчиков, которые требуют авторизации
authorizedGroup := app.Group("")
authorizedGroup.Use(jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{
Key: jwtSecretKey,
},
ContextKey: contextKeyUser,
}))
authorizedGroup.Get("/profile", userHandler.Profile)
logrus.Fatal(app.Listen(":80"))
}
// Структура HTTP-ответа с информацией о пользователе
type ProfileResponse struct {
Email string `json:"email"`
Name string `json:"name"`
}
func jwtPayloadFromRequest(c *fiber.Ctx) (jwt.MapClaims, bool) {
jwtToken, ok := c.Context().Value(contextKeyUser).(*jwt.Token)
if !ok {
logrus.WithFields(logrus.Fields{
"jwt_token_context_value": c.Context().Value(contextKeyUser),
}).Error("wrong type of JWT token in context")
return nil, false
}
payload, ok := jwtToken.Claims.(jwt.MapClaims)
if !ok {
logrus.WithFields(logrus.Fields{
"jwt_token_claims": jwtToken.Claims,
}).Error("wrong type of JWT token claims")
return nil, false
}
return payload, true
}
// Обработчик HTTP-запросов на получение информации о пользователе
func (h *UserHandler) Profile(c *fiber.Ctx) error {
jwtPayload, ok := jwtPayloadFromRequest(c)
if !ok {
return c.SendStatus(fiber.StatusUnauthorized)
}
userInfo, ok := h.storage.users[jwtPayload["sub"].(string)]
if !ok {
return errors.New("user not found")
}
return c.JSON(ProfileResponse{
Email: userInfo.Email,
Name: userInfo.Name,
})
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Так как проверка авторизации может происходить во многих HTTP-обработчиках, мы вынесли ее на уровень посредников. В микрофреймворке Fiber для этого есть готовый посредник — <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://github.com/gofiber/jwt" rel="noopener noreferrer" target="_blank">jwtware</a>.</p>
<p>При инициализации посредника мы указали два свойства:</p>
<ul>
<li><em>SigningKey</em> — секретный ключ JWT-токена</li>
<li><em>ContextKey</em> — название поля, по которому хранится объект JWT-токена авторизованного пользователя. Этот объект можно использовать в любом обработчике группы <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">authorizedGroup</code></li>
</ul>
<p>Когда приходит HTTP-запрос на получение информации о пользователе, веб-приложение проверяет, что в заголовке <em>Authorization</em> указан корректный JWT-токен. Если проверка прошла успешно, веб-приложение ищет пользователя в хранилище по электронной почте, которая записана в полезной нагрузке JWT-токена. Если пользователь был найден, то авторизация пройдена, и мы возвращаем в HTTP-ответе информацию об этом пользователе.</p>
<p>Мы реализовали все части аутентификации и авторизации нашей социальной сети. Теперь соберем все вместе и проверим, что веб-приложение работает.</p>
<h3 id="heading-3-12">Проверяем веб-приложение</h3>
<p>Полный код веб-приложения выглядит следующим образом:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">package main
import (
"errors"
"fmt"
"github.com/gofiber/fiber/v2"
jwtware "github.com/gofiber/contrib/jwt"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/sirupsen/logrus"
"time"
)
const (
contextKeyUser = "user"
)
func main() {
app := fiber.New()
authStorage := &AuthStorage{map[string]User{}}
authHandler := &AuthHandler{storage: authStorage}
userHandler := &UserHandler{storage: authStorage}
// Группа обработчиков, которые доступны неавторизованным пользователям
publicGroup := app.Group("")
publicGroup.Post("/register", authHandler.Register)
publicGroup.Post("/login", authHandler.Login)
// Группа обработчиков, которые требуют авторизации
authorizedGroup := app.Group("")
authorizedGroup.Use(jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{
Key: jwtSecretKey,
},
ContextKey: contextKeyUser,
}))
authorizedGroup.Get("/profile", userHandler.Profile)
logrus.Fatal(app.Listen(":80"))
}
type (
// Обработчик HTTP-запросов на регистрацию и аутентификацию пользователей
AuthHandler struct {
storage *AuthStorage
}
// Хранилище зарегистрированных пользователей
// Данные хранятся в оперативной памяти
AuthStorage struct {
users map[string]User
}
// Структура данных с информацией о пользователе
User struct {
Email string
Name string
password string
}
)
// Структура HTTP-запроса на регистрацию пользователя
type RegisterRequest struct {
Email string `json:"email"`
Name string `json:"name"`
Password string `json:"password"`
}
// Обработчик HTTP-запросов на регистрацию пользователя
func (h *AuthHandler) Register(c *fiber.Ctx) error {
regReq := RegisterRequest{}
if err := c.BodyParser(&regReq); err != nil {
return fmt.Errorf("body parser: %w", err)
}
// Проверяем, что пользователь с таким email еще не зарегистрирован
if _, exists := h.storage.users[regReq.Email]; exists {
return errors.New("the user already exists")
}
// Сохраняем в память нового зарегистрированного пользователя
h.storage.users[regReq.Email] = User{
Email: regReq.Email,
Name: regReq.Name,
password: regReq.Password,
}
return c.SendStatus(fiber.StatusCreated)
}
// Структура HTTP-запроса на вход в аккаунт
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
// Структура HTTP-ответа на вход в аккаунт
// В ответе содержится JWT-токен авторизованного пользователя
type LoginResponse struct {
AccessToken string `json:"access_token"`
}
var (
errBadCredentials = errors.New("email or password is incorrect")
)
// Секретный ключ для подписи JWT-токена
// Необходимо хранить в безопасном месте
var jwtSecretKey = []byte("very-secret-key")
// Обработчик HTTP-запросов на вход в аккаунт
func (h *AuthHandler) Login(c *fiber.Ctx) error {
regReq := LoginRequest{}
if err := c.BodyParser(&regReq); err != nil {
return fmt.Errorf("body parser: %w", err)
}
// Ищем пользователя в памяти приложения по электронной почте
user, exists := h.storage.users[regReq.Email]
// Если пользователь не найден, возвращаем ошибку
if !exists {
return errBadCredentials
}
// Если пользователь найден, но у него другой пароль, возвращаем ошибку
if user.password != regReq.Password {
return errBadCredentials
}
// Генерируем JWT-токен для пользователя,
// который он будет использовать в будущих HTTP-запросах
// Генерируем полезные данные, которые будут храниться в токене
payload := jwt.MapClaims{
"sub": user.Email,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
// Создаем новый JWT-токен и подписываем его по алгоритму HS256
token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
t, err := token.SignedString(jwtSecretKey)
if err != nil {
logrus.WithError(err).Error("JWT token signing")
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(LoginResponse{AccessToken: t})
}
// Обработчик HTTP-запросов, которые связаны с пользователем
type UserHandler struct {
storage *AuthStorage
}
// Структура HTTP-ответа с информацией о пользователе
type ProfileResponse struct {
Email string `json:"email"`
Name string `json:"name"`
}
func jwtPayloadFromRequest(c *fiber.Ctx) (jwt.MapClaims, bool) {
jwtToken, ok := c.Context().Value(contextKeyUser).(*jwt.Token)
if !ok {
logrus.WithFields(logrus.Fields{
"jwt_token_context_value": c.Context().Value(contextKeyUser),
}).Error("wrong type of JWT token in context")
return nil, false
}
payload, ok := jwtToken.Claims.(jwt.MapClaims)
if !ok {
logrus.WithFields(logrus.Fields{
"jwt_token_claims": jwtToken.Claims,
}).Error("wrong type of JWT token claims")
return nil, false
}
return payload, true
}
// Обработчик HTTP-запросов на получение информации о пользователе
func (h *UserHandler) Profile(c *fiber.Ctx) error {
jwtPayload, ok := jwtPayloadFromRequest(c)
if !ok {
return c.SendStatus(fiber.StatusUnauthorized)
}
userInfo, ok := h.storage.users[jwtPayload["sub"].(string)]
if !ok {
return errors.New("user not found")
}
return c.JSON(ProfileResponse{
Email: userInfo.Email,
Name: userInfo.Name,
})
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Запускаем веб-приложение и отправляем запрос на регистрацию нового пользователя:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">curl --location --request POST 'http://localhost/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "john@doe.com",
"name": "John",
"password": "pickles"
}'</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>В ответ получаем:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">HTTP/1.1 201 Created</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Это означает, что такого пользователя нет в хранилище, и он был успешно создан.</p>
<p>Теперь попробуем пройти аутентификацию этого пользователя:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">curl --location --request POST 'http://localhost/login' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "john@doe.com",
"password": "pickles"
}'</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>В ответ приходит:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">HTTP/1.1 200 OK
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Njg5NTEwMTcsInN1YiI6ImpvaG5AZG9lLmNvbSJ9.Q3k6yMFYtuzPyjoZYpIHibJQPey29QWmlHfwS2A3keM"}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Мы указали корректные учетные данные пользователя, поэтому аутентификация прошла успешно. В ответ веб-приложение вернуло JWT-токен, который мы будем использовать для авторизации при получении информации о пользователе.</p>
<p>Отправляем запрос на получение информации о пользователе:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">curl -v 'http://localhost/profile' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Njg5NTE0NDEsInN1YiI6ImpvaG5AZG9lLmNvbSJ9.e4yIoGzQC8ckcRISBjt4g18S2VEBiHrRhXG7N39-7qI'</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>В ответ приходит:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">HTTP/1.1 200 OK
{"email":"john@doe.com","name":"John"}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Так как мы передали JWT-токен в заголовке HTTP-запроса <em>Authorization</em>, веб-приложение авторизовало нас как пользователя <em><a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="mailto:john@doe.com">john@doe.com</a></em>, и вернуло информацию о нашем аккаунте.</p>
<p>В последнем запросе есть один интересный момент. Мы поставили перед значением токена слово <em>Bearer</em>:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Njg5NTE0NDEsInN1YiI6ImpvaG5AZG9lLmNvbSJ9.e4yIoGzQC8ckcRISBjt4g18S2VEBiHrRhXG7N39-7qI</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p><strong>Bearer</strong> переводится как носитель. Он дает веб-приложению понять, что в заголовке <em>Authorization</em> передан токен для авторизации. Принято считать, что это слово означает фразу: «Дай доступ к носителю этого токена». Если не указать слово <em>Bearer</em> перед значением, то авторизация не пройдет даже с корректным значением токена.</p>
<p>Мы реализовали функцию регистрации, аутентификации и получение информации об аккаунте авторизованного пользователя. Для авторизации мы использовали JWT-токен, который генерируются при входе в аккаунт на 72 часа и передается в заголовке HTTP-запроса <em>Authorization</em>.</p>
<h2 id="heading-2-13">Выводы</h2>
<ul>
<li>Аутентификация — это процесс проверки подлинности пользователя, который пытается получить доступ к веб-приложению</li>
<li>Авторизация — это процесс проверки прав пользователя на какое-либо действие в веб-приложении</li>
<li>Один из способов реализации системы авторизации — это JWT-токен. Токен генерируется и подписывается веб-приложением после успешной аутентификации и передается во всех последующих HTTP-запросах в заголовке <em>Authorization</em></li>
<li>JWT-токен содержит информацию о пользователе и подпись, которая необходима для авторизации. Так как токен подписан секретным ключом в веб-приложении, то его может проверять на подлинность только это веб-приложение.</li>
</ul></div><div style="margin-block:var(--mantine-spacing-xl)" class=""><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md)" class="m_8a5d1357 mantine-Title-root" data-order="2">Рекомендуемые программы</h2><style data-mantine-styles="inline">.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xs);--carousel-slide-size:70%;}@media(min-width: 36em){.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xl);--carousel-slide-size:50%;}}</style><div style="--carousel-control-size:calc(2.5rem * var(--mantine-scale));--carousel-controls-offset:var(--mantine-spacing-sm);margin-bottom:var(--mantine-spacing-lg);padding-block:var(--mantine-spacing-sm);background:var(--app-color-surface)" class="m_17884d0f mantine-Carousel-root responsiveClassName" data-orientation="horizontal" data-include-gap-in-size="true"><div class="m_39bc3463 mantine-Carousel-controls" data-orientation="horizontal"><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="previous" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="next" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(-90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button></div><div class="m_a2dae653 mantine-Carousel-viewport" data-type="media"><div class="m_fcd81474 mantine-Carousel-container __m__-_R_2mremqrdub_" data-orientation="horizontal"><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/go-web-development?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">1 месяц</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Для продвинутых</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Веб-разработка на Go</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Изучите Go и разработку веб-приложений</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDAzNywicHVyIjoiYmxvYl9pZCJ9fQ==--c9a507d1b30c26185c312c95f68af4f0d8122afa/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Programmer-bro.png" alt="Веб-разработка на Go" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 3 900 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/devops-engineer-from-scratch?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">14 месяцев</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">С нуля</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">DevOps-инженер с нуля</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Полное погружение в DevOps: весь стек от Linux до Kubernetes</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6Mzk2NSwicHVyIjoiYmxvYl9pZCJ9fQ==--84278a1852c9c6fb13b80a69f395bac6e47a422e/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Cloud%20sync-bro.png" alt="DevOps-инженер с нуля" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 6 792 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/go?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">6 месяцев</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Для продвинутых</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Go-разработчик</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Изучите Go, работу с БД, HTTP, конкурентность, горутины, многопоточность </p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzY2MywicHVyIjoiYmxvYl9pZCJ9fQ==--7e08550d68b7c2ad01e51109dfcf0861158d2e58/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Code%20review-amico.png" alt="Go-разработчик" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 4 509 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/go-from-scratch?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">9 месяцев</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">С нуля</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Go-разработчик с нуля</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Изучите фреймворк Gin, горутины, многопоточность и работа с API</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDg4MywicHVyIjoiYmxvYl9pZCJ9fQ==--9e90f48f058ab07777e0e79ecfcf2980f79fa277/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Code%20typing-cuate.png" alt="Go-разработчик с нуля" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root"></p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md);font-size:var(--mantine-font-size-h3)" class="m_8a5d1357 mantine-Title-root" data-order="2" data-responsive="true">Каталог</h2><p style="margin-bottom:auto" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Полный список доступных курсов по разным направлениям</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="/vite/assets/development-BVihs_d5.png" alt="Orientation"/></div></div></div></a></div></div></div></div></div></div></div></div></div><style data-mantine-styles="inline">.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:8.333333333333334%;--col-max-width:8.333333333333334%;}@media(min-width: 48em){.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:16.666666666666668%;--col-max-width:16.666666666666668%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem" class="m_96bdd299 mantine-Grid-col __m__-_R_1bdub_"><div style="margin-inline:var(--mantine-spacing-xs)" class="mantine-visible-from-sm"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-lg);text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/go-web-development/lessons/auth/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label"><span style="margin-inline-end:var(--mantine-spacing-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Дальше</span>→</span></span></a><a style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Навигация по теме</span><span class="m_57492dcc mantine-NavLink-description">Теория</span></div><span class="m_690090b5 mantine-NavLink-section" data-position="right"></span></a><div style="margin-block:var(--mantine-spacing-lg)" class="m_3eebeb36 mantine-Divider-root" data-orientation="horizontal" role="separator"></div><div style="margin-block:var(--mantine-spacing-lg)" class=""><div style="justify-content:space-between;margin-bottom:calc(0.1875rem * var(--mantine-scale));color:var(--mantine-color-dimmed);font-size:var(--mantine-font-size-xs)" class="m_8bffd616 mantine-Flex-root __m__-_R_qimrbdub_"><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Завершено</p><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">0 / 13</p></div><div style="--progress-size:var(--progress-size-sm)" class="m_db6d6462 mantine-Progress-root" data-size="sm"><div style="--progress-section-size:0%;--progress-section-color:var(--mantine-color-gray-filled)" class="m_2242eb65 mantine-Progress-section" role="progressbar" aria-valuemax="100" aria-valuemin="0" aria-valuenow="0" aria-valuetext="0%"></div></div></div><button style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root" type="button"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Обсуждения (архив)</span><span class="m_57492dcc mantine-NavLink-description"></span></div></button><div style="--toc-bg:var(--mantine-color-blue-light);--toc-color:var(--mantine-color-blue-light-color);--toc-size:var(--mantine-font-size-sm);--toc-radius:var(--mantine-radius-sm);margin-top:var(--mantine-spacing-xl)" class="m_bcaa9990 mantine-TableOfContents-root" data-variant="light" data-size="sm"></div></div><div class="mantine-hidden-from-sm"><div style="--stack-gap:0rem;--stack-align:stretch;--stack-justify:flex-start" class="m_6d731127 mantine-Stack-root"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-xs);padding:0rem;text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/go-web-development/lessons/auth/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">→</span></span></a><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" data-disabled="true" type="button" disabled=""><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></span></button><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto mantine-active m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" type="button"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></span></button></div></div></div></div></div></div></div>
</main>
<footer class="bg-dark fw-light text-light px-3 py-5">
<div class="row small">
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 mb-3">Хекслет</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/about">О нас</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/testimonials">Отзывы</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://b2b.hexlet.io" role="button">Корпоративное обучение</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/blog">Блог</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/qna">Вопросы и ответы</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/glossary">Глоссарий</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://help.hexlet.io" data-target="_blank" role="button">Справка</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" target="_blank" rel="noopener noreferrer" href="/map">Карта сайта</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 fw-normal mb-3">Направления</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_devops">DevOps
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_data_analytics">Аналитика
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_backend_development">Бэкенд
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_programming">Программирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_testing">Тестирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_front_end_dev">Фронтенд
</a></li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Профессии</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/go">Go-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/java">Java-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python">Python-разработчик </a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/data-analytics">Аналитик данных</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/qa-engineer">Инженер по ручному тестированию</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php">РНР-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/frontend">Фронтенд-разработчик</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Навыки</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python-django-developer">Django</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/docker">Docker</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php-laravel-developer">Laravel</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/postman">Postman</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-react-developer">React</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-rest-api">REST API в Node.js</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/spring-boot">Spring Boot</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/typescript">Typescript</a>
</li>
</ul>
</div>
</div>
<hr>
<div class="row">
<div class="col-12 col-sm-4 col-md-2">
<div class="fs-4">
<ul class="list-unstyled d-flex">
<li class="me-3">
<a aria-label="Telegram" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://t.me/hexlet_ru"><span class="bi bi-telegram"></span>
</a></li>
<li>
<a aria-label="Youtube" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://www.youtube.com/user/HexletUniversity"><span class="bi bi-youtube"></span>
</a></li>
</ul>
</div>
<div class="mb-2 d-flex flex-column">
<a class="link-light text-decoration-none" rel="nofollow" href="mailto:support@hexlet.io">support@hexlet.io</a>
<a class="link-light text-decoration-none py-2" target="_blank" href="https://t.me/hexlet_help_bot">t.me/hexlet_help_bot</a>
</div>
<ul class="list-unstyled d-flex">
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://hexlet.io/locale/switch?new_locale=en" data-target="_self" role="button"><span class="my-auto">EN</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 opacity-100 external-link" rel="nofollow" data-href="https://ru.hexlet.io/locale/switch?new_locale=ru" data-target="_self" role="button"><span class="my-auto">RU</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://kz.hexlet.io/locale/switch?new_locale=kz" data-target="_self" role="button"><span class="my-auto">KZ</span>
</span></li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<ul class="list-unstyled fs-4">
<li class="mb-3">
<a class="link-light text-decoration-none" href="tel:8%20800%20100%2022%2047">8 800 100 22 47</a>
<span class="d-block opacity-50 small">бесплатно по РФ</span>
</li>
<li>
<a class="link-light text-decoration-none" href="tel:%2B7%20495%20085%2021%2062">+7 495 085 21 62</a>
<span class="d-block opacity-50 small">бесплатно по Москве</span>
</li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<div class="small mb-3">Образовательные услуги оказываются на основании Л035-01298-77/01989008 от 14.03.2025</div>
<ul class="list-unstyled small">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/legal">Правовая информация</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/offer">Оферта</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/license">Лицензия</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/contacts">Контакты</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-12 col-md-4 small">
<div class="mb-2">
<div>ООО «<a href="/" class="text-decoration-none link-light">Хекслет Рус</a>»</div>
<div>108813 г. Москва, вн.тер.г. поселение Московский,</div>
<div>г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3</div>
<div>ОГРН 1217300010476</div>
<div>ИНН 7325174845</div>
</div>
<hr>
<div>АНО ДПО «<a href="/" class="text-decoration-none link-light">Учебный центр «Хекслет</a>»</div>
<div>119331 г. Москва, вн. тер. г. муниципальный округ</div>
<div>Ломоносовский, пр-кт Вернадского, д. 29</div>
<div>ОГРН 1247700712390</div>
<div>ИНН 7736364948</div>
</div>
</div>
</footer>
<div id="root-assistant-offcanvas"></div>
<script src="/vite/assets/assistant-CdBlNCiQ.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-nkZBEvfU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/MarkdownBlock-DbyKWoR_.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/shiki-V011pkdv.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-XR8Qr8kR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dist-GCHh59xr.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useIsomorphicEffect-HJ6VK0D3.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-KSp6QbZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/classnames-l6ipYlLR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/debounce-jMQ_Cf4f.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v67327c56f0bb4ef8b305cae61679db8f1769101564043" integrity="sha512-rdcWY47ByXd76cbCFzznIcEaCN71jqkWBBqlwhF1SY7KubdLKZiEGeP7AyieKZlGP9hbY/MhGrwXzJC/HulNyg==" data-cf-beacon='{"version":"2024.11.0","token":"d11015b65d11429ea6b4a2ef37dd7e0b","server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body>
</html>