зачем нужно виртуальное окружение python
Python. Урок 17. Виртуальные окружения
Что такое виртуальное окружение и зачем оно нужно?
Во-первых : различные приложения могут использовать одну и туже библиотеку, но при этом требуемые версии могут отличаться.
Во-вторых : может возникнуть необходимость в том, чтобы запретить вносить изменения в приложение на уровне библиотек, т.е. вы установили приложение и хотите, чтобы оно работало независимо от того обновляются у вас библиотеки или нет. Как вы понимаете, если оно будет использовать библиотеки из глобального хранилища ( /usr/lib/pythonXX/site-packages ), то, со временем, могут возникнуть проблемы.
Для решения данных вопросов используется подход, основанный на построении виртуальных окружений – своего рода песочниц, в рамках которых запускается приложение со своими библиотеками, обновление и изменение которых не затронет другие приложение, использующие те же библиотеки.
ПО позволяющее создавать виртуальное окружение в Python
Программное обеспечение, которое позволяет создавать виртуальные окружения в Python можно разделить на те, что входят в стандартную библиотеку Python и не входят в нее. Сделаем краткий обзор доступных инструментов (хороший пост на эту тем есть на stackoverflow ).
Virtualenvwrapper – это обертка для virtualenv позволяющая хранить все изолированные окружения в одном месте, создавать их, копировать и удалять. Предоставляет удобный способ переключения между окружениями и возможность расширять функционал за счет plug-in ’ов.
virtualenv
Установка virtualenv
Virtualenv можно установить с использованием менеджера pip (ссылка на статью), либо скачать исходные коды проекта и установить приложение вручную.
Установка с использованием pip.
Для установки virtualenv откройте консоль и введите следующую команду:
Установка из исходного кода проекта.
В этом случае, вам нужно будет выполнить чуть большее количество действий.
Введите в консоли следующий набор команд:
X.X – это версия приложения, ее вам нужно знать заранее.
Создание виртуального окружения
Виртуальное окружение создается следующей командой:
PRG1 в данном случае – это имя окружения.
Активация виртуального окружения
Для активации виртуального окружения воспользуйтесь командой (для Linux ):
для Windows команда будет выглядеть так:
Команда source выполняет bash- скрипт без запуска второго bash- процесса.
Если команда выполнилась успешно, то вы увидите, что перед приглашением в командной строке появилась дополнительная надпись, совпадающая с именем виртуального окружения.
Если вы создадите виртуальное окружение с ключем –system-site-packages :
то в рамках окружения PRG1 вы будите иметь доступ к глобальному хранилищу пакетов:
Деактивация виртуального окружения
venv
Создание виртуального окружения
Для создания виртуального окружения с именем PRG2 с помощью venv выполните следующую команду:
Активация виртуального окружения
Активация виртуального окружения в Linux выполняется командой:
Деактивация виртуального окружения
Деактивация выполняется командой deactivate (работает как в Windows, так и в Linux )
Полезные ссылки
P.S.
Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas. На нашем сайте вы можете найти вводные уроки по этой теме. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.
Виртуальное окружение Python (venv)
Location — путь до ваших глобальных пакетов.
В большинстве случаев, устанавливать пакеты глобально — плохая идея 🙅♂️ Почему? Рассмотрим простой пример:
Решение данной проблемы — создание виртуального окружения (virtual environment).
Основная цель виртуального окружения Python — создание изолированной среды для python-проектов
Это означает, что каждый проект может иметь свои собственные зависимости, независимо от других проектов.
Настройка виртуального окружения
Устанавливать venv не нужно — он входит в стандартную библиотеку Python
Создание
Для создания виртуального окружения, перейдите в директорию своего проекта и выполните:
В результате будет создан каталог venv/ содержащий копию интерпретатора Python, стандартную библиотеку и другие вспомогательные файлы.
Новые пакеты будут устанавливаться в venv/lib/python3.x/site-packages/
Активация
Чтобы начать пользоваться виртуальным окружением, необходимо его активировать:
source выполняет bash-скрипт без запуска дополнительного bash-процесса.
Проверить успешность активации можно по приглашению оболочки. Она будет выглядеть так:
Также новый путь до библиотек можно увидеть выполнив команду:
Интересный факт: в виртуальном окружении вместо команды python3 и pip3, можно использовать python и pip
Автоматическая активация
В некоторых случаях, процесс активации виртуального окружения может показаться неудобным (про него можно банально забыть 🤷♀️).
На практике, для автоматической активации перед запуском скрипта, создают скрипт-обертку на bash :
Теперь можно установить права на исполнение и запустить нашу обертку:
Деактивация
Закончив работу в виртуальной среде, вы можете отключить ее, выполнив консольную команду:
Альтернативы venv
На данный момент существует несколько альтернатив для venv:
Стоит ли использовать виртуальное окружение в своей работе — однозначно да. Это мощный и удобный инструмент изоляции проектов друг от друга и от системы. С помощью виртуального окружения можно использовать даже разные версии Python!
Виртуальные окружения в Python
Python знаменит своей обширной стандартной библиотекой и девизом «батарейки в комплекте» (batteries included). Даже из коробки Python позволяет удобно и быстро решить огромный пласт задач, например, например, работа с файлами, запуск простого веб-сервера, работа с электронной почтой, парсинг XML и JSON, и так далее. Во всяком случае, это намного удобнее, чем писать shell-скрипты 😅
Кроме того, у Python имеется огромная экосистема сторонних библиотек, поддерживаемых сообществом энтузиастов. Эти библиотеки реализуют отсутствующую в стандартной поставке функциональность, либо пере-реализуют уже имеющуюся, но удобнее. Если у вас возникла потребность в какой-то функциональности, то почти наверняка кто-то уже написал для этого библиотеку, и нужно просто погуглить.
Установка сторонней библиотеки
Каждый начинающий программист знает, как установить библиотеку. Набираем
и понеслась! Множество библиотек в своих инструкциях по установке именно так и предлагают их устанавливать. Это и правда работает, это и правда так просто, но есть нюансы. В этом месте закопаны очень популярные грабли, по которым прошлось множество начинающих питонистов, в том числе и я.
Как pip устанавливает пакеты
Давайте разберемся, что же происходит, когда юзер набирает в терминал такую команду. В общих чертах происходит следующее.
Давайте подробнее разберем третий шаг. Установка пакета — звучит загадочно и сложно, но на самом деле ничего сложного здесь не происходит. pip просто распаковывает zip-архив в определенное место (это справедливо для формата wheel, для установки пакетов в других форматах могут потребоваться дополнительные действия, но давайте разберем самый распространённый и простой случай). Куда именно происходит установка? Это можно узнать, выполнив следующую команду:
В списке sys.path можно увидеть директорию site-packages — именно туда и будет установлена библиотека. Давайте в этом убедимся.
До установки пакета:
После установки пакета:
Как видим, в директорию site-packages добавилась библиотека requests вместе со всеми своими зависимостями.
Важные мысли, которые я пытаюсь донести:
А это значит, что в один интерпретатор Python нельзя установить две версии одной библиотеки одновременно. При установке новой версии предыдущая «перезатирается». Просто как если бы вы распаковали другой архив с совпадающими именами файлов в то же самое место.
Боль — это жизненный опыт
Что же будет, если вам понадобится работать над двумя проектами, которые будут требовать разных, не совместимых между собой версий одной и той же библиотеки? Возможно, между этими версиями в библиотеку были внесены какие-то крупные ломающие изменения, например, переименовались методы/функции или изменился набор аргументов.
Вы просто не сможете работать над такими проектами одновременно. Установка зависимостей одного проекта сломает другой, и наоборот. При переключении между проектами придётся каждый раз устанавливать зависимости нужного проекта, что довольно легко забыть сделать.
Ситуация кажется маловероятной, но я гарантирую, что рано или поздно это случится, если устанавливать зависимости всех своих проектов в один интерпретатор. Всё усугубляется тем фактом, что прямые зависимости вашего проекта тянут за собой свои зависимости (под-зависимости), те, в свою очередь, тоже могут от чего-то зависеть (под-под-зависимости). В итоге вы получаете целое дерево зависимостей. И если где-то в этом дереве окажется библиотека не той версии, что ожидалось, то весь проект может начать очень странно работать. Вы получите такие эзотерические ошибки, которых еще никто в интернете до вас не встречал. Если всё сразу сломалось, то считайте, что легко отделались — по крайней мере, так довольно просто понять, в чём проблема. Но бывают и ситуации намного хуже, когда приложение просто начинает немножко иначе работать, без каких-либо ошибок, и возможно придется потратить долгие часы на траблшутинг, чтобы найти настоящую причину.
Надеюсь, я убедил вас, что устанавливать зависимости нескольких проектов в один интерпретатор — это очень-очень плохо. Но как же тогда правильно?
Виртуальные окружения
Как создавать виртуальные окружения
Начиная с Python версии 3.5 (на данный момент это самая старая из официально поддерживаемых версий языка, так что справедливо ожидать, что как минимум везде установлен Python 3.5 или новее), создать виртуальное окружение стало очень просто:
Например, допустим, что мы работаем над проектом blog_source :
В директорию env будет скопирован тот самый интерпретатор, при помощи которого виртуальное окружение и создавалось. Т.е. если
то в виртуальном окружении будет та же самая версия:
Активируем окружение
Посмотрим, что внутри директории env :
Обратите внимание, что в директории bin есть некий файл activate в нескольких вариантах для разных шеллов. Это и есть «точка входа» в виртуальное окружение. Просто создать виртуальное окружение мало, нужно его активировать. Но сначала проверим, какие python и pip (исполняемые файлы) используются в обычном режиме работы:
Это мой обычный Python, вне виртуального окружения, назовём его глобальным. Теперь активируем виртуальное окружение:
Для Windows процесс активации будет отличаться (допустим, что виртуальное окружение создано в C:\src\blog_source ):
Теперь проверим еще раз, какие python и pip используются:
Посмотрите на пути — мы внутри виртуального окружения! Теперь можно смело устанавливать любые пакеты, и это никак не повлияет на глобальный Python или на другие виртуальные окружения:
Можно запускать любые файлы, и они будут иметь доступ к установленным пакетам:
IDE тоже нужно настроить, указав путь к bin/python внутри виртуального окружения, тогда редактор сможет лучше вам помогать.
И мы видим, что команда python снова вызывает глобальный интерпретатор. При этом виртуальное окружение осталось в своей директории, оно просто не активно. В следующий раз, когда будет нужно поработать с виртуальным окружением, не забудьте снова его активировать.
Виртуальное окружение можно полностью удалить, когда оно перестанет быть нужным:
В идеале, у вас должна быть возможность в любой момент удалить и пересоздать виртуальное окружение заново, для этого храните список зависимостей проекта и содержите его в актуальном состоянии (например, в requirements.txt ). В процессе разработки могут случиться всякие казусы с зависимостями, и иногда проще пересоздать виртуальное окружение заново, чем пытаться починить сломанное.
Вот так можно работать с виртуальными окружениями в Python. Всегда устанавливайте зависимости проектов только в изолированные виртуальные окружения. Не смешивайте зависимости разных проектов в одном окружении.
Ничего не устанавливайте в глобальный интерпретатор
Установка начинается, прогресс-бары заполняются, но в итоге всё завершается чем-то типа такого:
Может нарушить целостность системы.
Подробнее про этот метод установки читайте здесь.
установить программу через пакетный менеджер ОС, например:
Выводы
Да, виртуальные окружения — определенно не самая удобная часть разработки на Python, и уж точно не самая простая тема, к этому просто нужно привыкнуть. Несколько раз повторил, выработал привычку — в целом, ничего сложного. Кроме того, экосистема Python развивается очень быстро, и я надеюсь, что скоро правильная установка пакетов и управление виртуальными окружениями станут намного легче. Уже сейчас можно пользоваться такими инструментами, которые в некоторой мере прячут от пользователя виртуальные окружения:
Стабильных вам зависимостей и кода без багов!
Python: виртуальное окружение, виртуальная среда
Статья расскажет, зачем нужна виртуальная среда в проектах на Python. Что это такое, как она используется, на что стоит обратить внимание.
Виртуальное окружение — для чего оно?
Язык программирования Python, как многие другие языки, обладает своим уникальным способом загрузки и хранения пакетов (модулей). Это имеет как плюсы, так и минусы. Дело в том, что есть несколько расположений, где сохраняются пакеты, установленные в системе. Большая часть их хранится в дочернем каталоге пути, а он, в свою очередь, находится в sys.prefix.
На том же Маке можно без проблем найти, где конкретно sys.prefix указывает на применение оболочки Python:
Нас интересуют в большей мере сторонние пакеты, которые установлены посредством easy_install либо pip install. В большинстве случаев они находятся в одной из директорий, на которую указывает site.getsitepackages:
Все эти нюансы следует знать, чтобы понимать ситуацию, ведь по дефолту каждый системный объект станет применять одинаковые каталоги при хранении пакетов. Может показаться, что это несущественно. Да, если речь идёт о системных пакетах, которые считаются частью стандартной библиотеки. Совсем другое дело, если речь идёт о сторонних пакетах.
Представьте простую ситуацию: у вас 2 пакета (ProjectA and ProjectB). Оба связаны зависимостью с одной и той же библиотекой — ProjectC. Если же мы начнём запрашивать различные версии ProjectC, могут возникнуть затруднения. ProjectA может запросить одну версию (1.0), а ProjectB — другую, более новую (2.0). В этом и заключается большая проблема «Питона» — он не различает версии в каталоге «site-packages».
Раз проекты хранятся с учётом их названий, различий между версиями нет, в результате чего ProjectA и ProjectB будут применять одну и ту же версию, а это не есть хорошо.
Как раз здесь на сцену и выходит виртуальное окружение (среда) Python, делая это совместно с инструментами virtualenv/ven.
Так что же такое виртуальное окружение (virtual environment)?
Основной задачей виртуального окружения в языке Python является создание изолированной среды для «Пайтон»-проектов. Что это значит: — любой проект способен иметь собственные зависимости; — зависимости второго проекта не оказывают влияния на зависимости первого и наоборот.
Если вернуться к примеру выше, становится очевидным, что следует просто разделить виртуальные окружения для ProjectA и ProjectB. В итоге каждая отдельная среда будет зависеть от любой версии ProjectC и не зависеть друг от друга.
Профит налицо, ведь отсутствуют ограничения на то, во скольких экземплярах будет виртуальное окружение, ведь они являются обычными директориями, где содержится несколько скриптов. А ещё их довольно легко создать, используя средства командной строки — речь идёт об инструментах virtualenv либо pyenv для Python.
Применение виртуальной среды
Если у вас Python 3, модуль venv должен быть уже установлен. Если же нет, вопрос можно решить, выполнив установку Python-инструмента virtualenv посредством pip:
Внимание! Следует понимать, что venv и virtualenv — это, по сути, 2 разных инструмента, имеющих несколько различий в плане команд.
Создадим новый каталог, с которым потом станем работать, следующей командой:
Теперь создадим новое виртуальное окружение внутри каталога:
Внимание! По дефолту ни один из существующих сторонних пакетов не включён.
В Python 3 подход venv имеет преимущество, вынуждающее применять определённую версию Python-интерпретатора, который нужен для создания виртуальной среды. Зато вы избежите проблем, выясняя, какая именно инсталляция «Пайтон» базируется в новом виртуальном окружении.
В вышеуказанном примере данная команда создаст каталог «env», структура которого будет выглядеть приблизительно так:
Разберём, что в папках, подробнее: • bin – это файлы, взаимодействующие с виртуальным окружением; • include – это С-заголовки, которые компилируют Python-пакеты; • lib – это копия Python-версии совместно с папкой «site-packages», где находится каждая зависимость.
Идём дальше. У нас в наличии копии (символические ссылки) ряда Python-инструментов. Данные файлы применяются, чтобы команды и «Пайтон»-код выполнялись в контексте созданной среды, что позволяет достичь изоляции от глобальной среды.
Весьма интересны скрипты activate, находящиеся в bin. Они применяются в целях настройки вашей оболочки для применения исполняемого файла окружения «Пайтон» и его сайтовых пакетов по дефолту.
Для использования этих пакетов среды в условиях изоляции, надо их активировать, выполнив активацию простой командой:
Здесь важно учесть, что ваше приглашение командной строки уже носит префикс вашего окружения (для нашего примера это env). Речь идёт об индикаторе, свидетельствующем, что в данный момент времени env активен. Это, в свою очередь, подтверждает то, что выполнимые «Пайтон»-файлы задействуют пакеты и настройки лишь из этого виртуального окружения.
Если надо показать изолированный пакет, что называется, в деле, подойдёт модуль bcrypt. Давайте скажем, что он установлен где-то в системе, а не в нашем виртуальном окружении.
Перед проверкой вернёмся назад в контекст «system» с помощью команды deactivate:
Теперь сеанс оболочки вернулся в нормальное состояние, а команда python будет ссылаться на общую Python-инсталляцию. Сделать это можно, когда заблагорассудиться, но после того, как закроете определённое виртуальное окружение. Далее следует установить bcrypt и воспользоваться им в целях хэширования пароля:
А теперь давайте посмотрим, что случится, если попробовать ту же самую команду, когда виртуальное окружение активно:
Virtual environment python. Виртуальное окружение в питоне
Сам довольно долго игнорировал эту вещь, пока не дошёл до первого деплоя.
Копирую ответ с хабра
«Виртуальное окружение необходимо для того, чтобы решить проблему несовместимости библиотек внутри проекта. Предположим, что вы работаете над каким-нибудь проектом, который использует определенную библиотеку. Затем у вас появляется новый проект в котором вы решили использовать эту же библиотеку, но новой версии, которая по каким-то причинам не совместима со старой. И тут вы можете создать виртуальное окружение для нового проекта, установить в него необходимую версию библиотеки и иметь возможность работать над двумя проектами сразу.»
Можно ещё проще. Вы учитесь/программируете на своём пк, ставите разные библиотеки и т.д. И тут вас приглашают в уже функционирующий проект, вы его клонируете к себе, но запустить ничего не получается, у вас python 3.8, а в проекте 3,9 или наоборот, к примеру 3,5. у вас установлена одна версия django, а на проекте другая.
В-общем по тем или иным причинам, определенные версии библиотек могут не дружить друг с другом.
И именно здесь приходит на помощь виртуальное окружение, независимо от того, какие версии чего бы то ни было у вас установлены «глобально», вы можете для любого проекта, создавать отдельное виртуальное окружение и именно для этого проекта устанавливать необходимые библиотеки.
Причин для использования как всегда больше, и нет смысла их все описывать.
Виртуальное окружение логично создавать в папке проекта. То есть в вашей папке с проектами projects есть папка myProject666 (projects/myProject666), внутри этой папки есть папка с проектом, myProject666API (projects/myProject666/myProject666API) и созданная командой папка (projects/myProject666/myEnvName)
установка библиотеки для создания виртуального окружения: pip install virtualenv
Активация созданного окружения на линукс: source myEnvName/bin/activate
Активация созданного окружения на винде: myEnvName\Scripts\activate
После активации ставятся нужные библиотеки и запускается проект будучи «активированным»
В-общем не надо стесняться, используйте virtual environment
Нет момента про то, как потом пользователю запускать программу, тупо жмякая на ярлык? Как правильно прописать программу в планировщик? Как это работает с anaconda?
Скомкано и не совсем верно. venv не управляет версиями питона, для этого есть отдельные инструменты, например pyenv, а virtualenv и его стандартная реализация venv в поставке современного питона 3 (рекомендован начиная с 3.5) занимаются разруливанием версий сторонних модулей, самописных или с pypi.org
Вот уже на протяжении нескольких лет Тимофей, преподаватель кафедры информатики МФТИ, выкладывает свои лекции по программированию на своём Youtube канале с открытым доступом.
Разработка системы заметок с нуля. Часть 2: REST API для RESTful API Service + JWT + Swagger
Продолжаем серию материалов про создание системы заметок. В этой части мы спроектируем и разработаем RESTful API Service на Go cо Swagger и авторизацией. Будет много кода, ещё больше рефакторинга и даже немного интеграционных тестов.
В первой части мы спроектировали систему и посмотрели, какие сервисы требуются для построения микросервисной архитектуры.
Подробности в видео и текстовой расшифровке под ним.
Начнём с макетов интерфейса. Нам нужно понять, какие ручки будут у нашего API и какой состав данных он должен отдавать. Макеты мы будем делать, чтобы понять, какие сущности, поля и эндпоинты нам нужны. Используем для этого онлайн-сервис NinjaMock. Он подходит, если макет надо сделать быстро и без лишних действий.
Страницу регистрации сделаем простую, с четырьмя полями: Name, Email, Password и Repeat Password. Лейблы делать не будем, обойдемся плейсходерами. Авторизацию сделаем по юзернейму и паролю.
После входа в приложение пользователь увидит список заметок, который будет выглядеть примерно так:
Интерфейс, который будет у нашего веб-приложения:
— Слева — список категорий любой вложенности.
— Справа — список заметок в виде карточек, который делится на два списка: прикреплённые и обычные карточки.
— Каждая карточка состоит из заголовка, который урезается, если он очень длинный.
— Справа указано, сколько секунд/минут/часов/дней назад была создана заметка.
— Тело заголовка — отрендеренный Markdown.
— Панель инструментов. Через неё можно изменить цвет, прикрепить или удалить заметку.
Тут важно отметить, что файлы заметки мы не отображаем и не будем запрашивать у API для списка заметок.
Полная карточка открывается по клику на заметку. Тут можно сразу отобразить полностью длинный заголовок. Высота заметки зависит от количества текста. Для файлов появляется отдельная секция. Мы их будем получать отдельным асинхронным запросом, который не помешает пользователю редактировать заметку. Файлы можно скачать по ссылке, также есть отдельная кнопка на добавление файлов.
Так будет выглядеть открытая заметка
В ходе прототипирования стало понятно, что в первой части мы забыли добавить еще один микросервис — TagsService. Он будет управлять тегами.
Для страниц авторизации и регистрации нам нужны эндпоинты аутентификации и регистрации соответственно. В качестве аутентификации и сессий пользователя мы будем использовать JWT. Что это такое и как работает, разберём чуть позднее. Пока просто запомните эти 3 буквы.
Для страницы списка заметок нам нужны эндпоинты /api/categories для получения древовидного списка категорий и /api/notes?category_id=? для получения списка заметок текущей категории. Перемещаясь по другим категориям, мы будем отдельно запрашивать заметки для выбранной категории, а на фронтенде сделаем кэш на клиенте. В ходе работы с заметками нам нужно уметь создавать новую категорию. Это будет метод POST на URL /api/categories. Также мы будем создавать новый тег при помощи метода POST на URL /api/tags.
Чтобы обновить заметку, используем метод PATCH на URL /api/notes/:uuid с измененными полями. Делаем PATCH, а не PUT, потому что PUT требует отправки всех полей сущности по спецификации HTTP, а PATCH как раз нужен для частичного обновления. Для отображения заметки нам ещё нужен эндпоинт /api/notes/:uuid/files с методами POST и GET. Также нам нужно скачивать файл, поэтому сделаем метод GET на URL /api/files/:uuid.
Структура репозитория системы
Ещё немного общей информации. Структура репозитория всей системы будет выглядеть следующим образом:
В директории app будет исходный код сервиса (если он будет). На уровне с app будут другие директории других продуктов, которые используются с этим сервисом, например, MongoDB или ELK. Продукты, которые будут использоваться на уровне всей системы, например, Consul, будут в отдельных директориях на уровне с сервисами.
Писать будем на Go
— Идём на официальный сайт.
— Копируем ссылку до архива, скачиваем, проверяем хеш-сумму.
— Распаковываем и добавляем в переменную PATH путь до бинарников Go
— Пишем небольшой тест проверки работоспособности, собираем бинарник и запускаем.
Установка завершена, всё работает
Теперь создаём проект. Структура стандартная:
— cmd — точка входа в приложение,
— internal — внутренняя бизнес-логика приложения,
— pkg — для кода, который можно переиспользовать из проекта в проект.
Я очень люблю логировать ход работы приложения, поэтому перенесу свою обёртку над логером logrus из другого проекта. Основная функция здесь Init, которая создает логер, папку logs и в ней файл all.log со всеми логами. Кроме файла логи будут выводиться в STDOUT. Также в пакете реализована поддержка логирования в разные файлы с разным уровнем логирования, но в текущем проекте мы это использовать не будем.
APIService будет работать на сокете. Создаём роутер, затем файл с сокетом и начинаем его слушать. Также мы хотим перехватывать от системы сигналы завершения работы. Например, если кто-то пошлёт приложению сигнал SIGHUP, приложение должно корректно завершиться, закрыв все текущие соединения и сессии. Хотел перехватывать все сигналы, но линтер предупреждает, что os.Kill и SIGSTOP перехватить не получится, поэтому их удаляем из этого списка.
Теперь давайте добавим сразу стандартный handler для метрик. Я его копирую в директорию pkg, далее добавляю в роутер. Все последующие роутеры будем добавлять так же.
Далее создаём точку входа в приложение. В директории cmd создаём директорию main, а в ней — файл app.go. В нём мы создаём функцию main, в которой инициализируем и создаём логер. Роутер создаём через ключевое слово defer, чтобы метод Init у роутера вызвался только тогда, когда завершится функция main. Таким образом можно выполнять очистку ресурсов, закрытие контекстов и отложенный запуск методов. Запускаем, проверяем логи и сокет, всё работает.
Но для разработки нам нужно запускать приложение на порту, а не на сокете. Поэтому давайте добавим запуск приложения на порту в наш роутер. Определять, как запускать приложение, мы будем с помощью конфига.
Создадим для приложения контекст. Сделаем его синглтоном при помощи механизма sync.Once. Пока что в нём будет только конфиг. Контекст в виде синглтона создаю исключительно в учебных целях, впоследствии он будет выпилен. В большинстве случаев синглтоны — необходимое зло, в нашем проекте они не нужны. Далее создаём конфиг. Это будет YAML-файл, который мы будем парсить в структуру.
В роутере мы вытаскиваем из контекста конфиг и на основании listen.type либо создаем сокет, либо вешаем приложение на порт. Код graceful shutdown выделяем в отдельный пакет и передаём на вход список сигналов и список интерфейсов io.Close, которые надо закрывать. Запускаем приложение и проверяем наш эндпоинт heartbeat. Всё работает. Давайте и конфиг сделаем синглтоном через механизм sync.Once, чтобы потом безболезненно удалить контекст, который создавался в учебных целях.
Теперь переходим к API. Создаём эндпоинты, полученные при анализе прототипов интерфейса. Тут важно отметить, что у нас все данные привязаны к пользователю. На первый взгляд, все ручки должны начинаться с пользователя и его идентификатора /api/users/:uuid. Но у нас будет авторизация, иначе любой пользователь сможет программно запросить заметки любого другого пользователя. Авторизацию можно сделать следующим образом: Basic Auth, Digest Auth, JSON Web Token, сессии и OAuth2. У всех способов есть свои плюсы и минусы. Для этого проекта мы возьмём JSON Web Token.
Работа с JSON Web Token
JSON Web Token (JWT) — это JSON-объект, который определён в открытом стандарте RFC 7519. Он считается одним из безопасных способов передачи информации между двумя участниками. Для его создания необходимо определить заголовок (header) с общей информацией по токену, полезные данные (payload), такие как id пользователя, его роль и т.д., а также подписи (signature).
JWT использует преимущества подхода цифровой подписи JWS (Signature) и кодирования JWE (Encrypting). Подпись не даёт кому-то подделать токен без информации о секретном ключе, а кодирование защищает от прочтения данных третьими лицами. Давайте разберёмся, как они могут нам помочь для аутентификации и авторизации пользователя.
Аутентификация — процедура проверки подлинности. Мы проверяем, есть ли пользователь с полученной связкой логин-пароль в нашей системе.
Авторизация — предоставление пользователю прав на выполнение определённых действий, а также процесс проверки (подтверждения) данных прав при попытке выполнения этих действий.
Другими словами, аутентификация проверяет легальность пользователя. Пользователь становится авторизированным, если может выполнять разрешённые действия.
Важно понимать, что использование JWT не скрывает и не маскирует данные автоматически. Причина использования JWT — проверка, что отправленные данные были действительно отправлены авторизованным источником. Данные внутри JWT закодированы и подписаны, но не зашифрованы. Цель кодирования данных — преобразование структуры. Подписанные данные позволяют получателю данных проверить аутентификацию источника данных.
Реализация JWT в нашем APIService:
— Создаём директории middleware и jwt, а также файл jwt.go.
— Описываем кастомные UserClaims и сам middlware.
— Получаем заголовок Authorization, оттуда берём токен.
— Берём секрет из конфига.
— Создаём верификатор HMAC.
— Парсим и проверяем токен.
— Анмаршалим полученные данные в модель UserClaims.
— Проверяем, что токен валидный на текущий момент.
При любой ошибке отдаём ответ с кодом 401 Unauthorized. Если ошибок не было, в контекст сохраняем ID пользователя в параметр user_id, чтобы во всех хендлерах его можно было получить. Теперь надо этот токен сгенерировать. Это будет делать хендлер авторизации с методом POST и эндпоинтом /api/auth. Он получает входные данные в виде полей username и password, которые мы описываем отдельной структурой user. Здесь также будет взаимодействие с UserService, нам надо там искать пользователя по полученным данным. Если такой пользователь есть, то создаём для него UserClaims, в которых указываем все нужные для нас данные. Определяем время жизни токена при помощи переменной ExpiresAt — берём текущее время и добавляем 15 секунд. Билдим токен и отдаём в виде JSON в параметре token. Клиента к UserService у нас пока нет, поэтому делаем заглушку.
Добавим в хендлер с heartbeat еще один тестовый хендлер, чтобы проверить работу аутентификации. Пишем небольшой тест. Для этого используем инструмент sketch, встроенный в IDE. Делаем POST-запрос на /api/auth, получаем токен и подставляем его в следующий запрос. Получаем ответ от эндпоинта /api/heartbeat, по истечении 5 секунд мы начнём получать ошибку с кодом 401 Unauthorized.
Наш токен действителен очень ограниченное время. Сейчас это 15 секунд, а будет минут 30. Но этого всё равно мало. Когда токен протухнет, пользователю необходимо будет заново авторизовываться в системе. Это сделано для того, чтобы защитить пользовательские данные. Если злоумышленник украдет токен авторизации, который будет действовать очень большой промежуток времени или вообще бессрочно, то это будет провал.
Чтобы этого избежать, прикрутим refresh-токен. Он позволит пересоздать основной токен доступа без запроса данных авторизации пользователя. Такие токены живут очень долго или вообще бессрочно. После того как только старый JWT истекает мы больше не можем обратиться к API. Тогда отправляем refresh-токен. Нам приходит новая пара токена доступа и refresh-токена.
Хранить refresh-токены на сервере мы будем в кэше. В качестве реализации возьмём FreeCache. Я использую свою обёртку над кэшем из другого проекта, которая позволяет заменить реализацию FreeCache на любую другую, так как отдает интерфейс Repository с методами, которые никак не связаны с библиотекой.
Пока рассуждал про кэш, решил зарефакторить существующий код, чтобы было удобней прокидывать объекты без dependency injection и синглтонов. Обернул хендлеры и роутер в структуры. В хендлерах сделал интерфейс с методом Register, которые регистрируют его в роутере. Все объекты теперь инициализируются в main, весь роутер переехал в мейн. Старт приложения выделили в отдельную функцию также в main-файле. Теперь, если хендлеру нужен какой-то объект, я его просто буду добавлять в конструктор структуры хендлера, а инициализировать в main. Плюс появилась возможность прокидывать всем хендлерам свой логер. Это будет удобно когда надо будет добавлять поле trace_id от Zipkin в строчку лога.
Вернемся к refresh_token. Теперь при создании токена доступа создадим refresh_token и отдадим его вместе с основным. Сделаем обработку метода PUT для эндпоинта /api/auth, а в теле запроса будем ожидать параметр refresh_token, чтобы сгенерировать новую пару токена доступа и refresh-токена. Refresh-токен мы кладём в кэш в качестве ключа. Значением будет user_id, чтобы по нему можно было запросить данные пользователя у UserService и сгенерировать новый токен доступа. Refresh-токен одноразовый, поэтому сразу после получения токена из кэша удаляем его.
Для описания нашего API будем использовать спецификацию OpenAPI 3.0 и Swagger — YAML-файл, который описывает все схемы данных и все эндпоинты. По нему очень легко ориентироваться, у него приятный интерфейс. Но описывать вручную всё очень муторно, поэтому лучше генерировать его кодом.
— Создаём эндпоинты /api/auth с методами POST и PUT для получения токена по юзернейму и паролю и по Refresh-токену соответственно.
— Добавляем схемы объектов Token и User.
— Создаём эндпоинты /api/users с методом POST для регистрации нового пользователя. Для него создаём схему CreateUser.
Понимаем, что забыли сделать хендлер для регистрации пользователя. Создаём метод Signup у хенлера Auth и структуру newUser со всеми полями для регистрации. Генерацию JWT выделяем в отдельный метод, чтобы можно было его вызывать как в Auth, так и в Signup-хендлерах. У нас всё еще нет UserService, поэтому проставляем TODO. Нам надо будет провалидировать полученные данные от пользователя и потом отправить их в UserService, чтобы он уже создал пользователя и ответил нам об успехе. Далее вызываем функцию создания пары токена доступа и refresh-токена и отдаём с кодом 201.
У нас есть подсказка в виде Swagger-файла. На его основе создаём все нужные хендлеры. Там, где вызов микросервисов, будем проставлять комментарий с TODO.
Создаём хендлер для категорий, определяем URL в константах. Далее создаём структуры. Опираемся на Swagger-файл, который создали ранее. Далее создаём сам хендлер и реализуем метод Register, который регистрирует его в роутере. Затем создаём методы с логикой работы и сразу пишем тест API на этот метод. Проверяем, находим ошибки в сваггере. Таким образом мы создаём все методы по работе с категориями: получение и создание.
Далее создаём таким же образом хендлер для заметок. Понимаем, что забыли методы частичного обновления и удаления как для заметок, так и для категорий. Дописываем их в Swagger и реализуем методы в коде. Также обязательно тестируем Swagger в онлайн-редакторе.
Здесь надо обратить внимание на то, что методы создания сущности возвращают код ответа 201 и заголовок Location, в котором находится URL для получения сущности. Оттуда можно вытащить идентификатор созданной сущности.
В третьей части мы познакомимся с графовой базой данных Neo4j, а также будем работать над микросервисами CategoryService и APIService.