Guardian (Основы)
JWT
Что такое JWT? JWT занимается созданием токенов для аутентификации. В то время как многие системы аутентификации предоставляют доступ только к идентификатору ресурса, JWT позволяет хранить еще и другую информацию, такую как:
- Кем выдан токен
- Для кого выдан токен
- Какие системы должны использовать этот токен
- В какое время он был выдан
- На какое время он выдан
В дополнение к этим полям Guardian предоставляет и другие поля для облегчения дополнительных функций:
- Каков тип токена
- Какие права есть у владельца данного токена
Это всё — простые примеры полей в JWT. Технология позволяет добавить дополнительную информацию, которой требует логика приложения. Главное — не забывайте что этот токен должен быть коротким, ведь он должен поместиться в HTTP заголовке.
В результате такого подхода JWT токены могут передаваться как полноценный целостный элемент данных идентификации пользователя.
Когда их использовать
Токены JWT могут использоваться для аутентификации в любой части приложения:
- Одностраничные приложения
- Контроллеры (через браузерную сессию)
- Контроллеры (через HTTP заголовки авторизации - API)
- Phoenix Channels
- Запросы между сервисами
- Запросы между процессами
- Сторонний доступ (OAuth)
- Функциональность “запомнить меня”
- Другие интерфейсы - чистый TCP, UDP, CLI
Такие токены могут использоваться в приложении где угодно, когда нужно предоставить проверяемую аутентификацию.
Нужно ли использовать базу данных?
JWT токены не нужно отслеживать с помощью базы данных. Достаточно знать, что они уже выписаны и использовать время жизни токена для контроля доступа. Часто встречаются случаи, когда нужно сделать запрос в базу для поиска/получения нужного ресурса, однако JWT это не требует.
Например, если нужно аутентифицировать подключение к UDP сокету с помощью JWT, можно обойтись без подключения к базе данных. Вся необходимая информация уже будет встроена в токен в момент его создания. Данные из токена можно использовать сразу после проверки подписи.
Однако, можно использовать базу данных для отслеживания JWT токенов. Если воспользоваться этой возможностью, можно проверить, что токен не был отозван. Как вариант, можно использовать записи в базе данных для отзыва сразу всех токенов пользователя. Это довольно просто реализуется в Guardian с помощью GuardianDb. GuardianDb использует интерфейсы Guardian для проведения валидации, сохранения и удаления из базы данных. Мы дойдем до этой темы позже.
Настройка
В Guardian довольно много различных настроек. Мы рассмотрим их позже, но начнем с самого простого варианта.
Минимальная настройка
Для начала работы с Guardian нужно сделать несколько вещей.
Настройка
mix.exs
def application do
[
mod: {MyApp, []},
applications: [:guardian, ...]
]
end
def deps do
[
{:guardian, "~> x.x"},
...
]
end
config/config.exs
# в каждом окружении стоит переопределять этот ключ
config :guardian, Guardian,
issuer: "MyAppId",
secret_key: Mix.env(),
serializer: MyApp.GuardianSerializer
Это минимальный набор информации, который нужно предоставить Guardian для функционирования.
Не стоит встраивать секретный ключ напрямую в основную конфигурацию приложения.
Вместо этого у каждого окружения должен быть свой ключ.
Хорошей практикой является использовать название Mix окружения в качестве значения ключа для dev и test окружений.
В то же время, staging и production окружения должны использовать сложные секретные ключи (например, сгенерированный с помощью mix phoenix.gen.secret
).
lib/my_app/guardian_serializer.ex
defmodule MyApp.GuardianSerializer do
@behaviour Guardian.Serializer
alias MyApp.Repo
alias MyApp.User
def for_token(user = %User{}), do: {:ok, "User:#{user.id}"}
def for_token(_), do: {:error, "Неизвестный ресурс"}
def from_token("User:" <> id), do: {:ok, Repo.get(User, id)}
def from_token(_), do: {:error, "Неизвестный ресурс"}
end
Сериализатор отвечает за определение нужного ресурса на основе данных из токена. Это может быть поиск в базе данных, внешнем API или даже простая строка. Он также отвечает за обратный процесс превращения объекта пользователя в сериализованный вариант.
Это всё, что нужно для минимальной настройки. Есть еще много других настроек, но для начала этого достаточно.
Использование в приложении
Теперь, когда всё настроено, нам нужно интегрировать Guardian в приложение. Так как это минималистичный вариант, давайте сначала рассмотрим его использование в контексте HTTP запросов.
HTTP запросы
Guardian предоставляет различные Plug для интеграции в HTTP запросы. О Plug можно почитать в другом уроке. Guardian не требует Phoenix, с ним легче всего будет привести следующие примеры, потому мы будем его использовать.
Самый легкий способ интеграции — роутер. Так как HTTP интеграции Guardian основаны на plug-ах, их можно использовать в любом совместимом инструментарии.
Основная логика Guardian plug выглядит так:
-
Найти токен внутри запроса и проверить его:
Verify...
plug-и. -
Опционально загрузить ресурсы, которые указаны в токене: plug
LoadResource
. -
Проверить, что найден пользователь. Иначе — запретить доступ: plug
EnsureAuthenticated
.
Чтобы удовлетворить все потребности разработчиков приложений, Guardian реализует эти этапы отдельно.
Чтобы найти токен, используйте plug Verify*
.
Давайте воспользуемся этими знаниями:
pipeline :maybe_browser_auth do
plug(Guardian.Plug.VerifySession)
plug(Guardian.Plug.VerifyHeader, realm: "Bearer")
plug(Guardian.Plug.LoadResource)
end
pipeline :ensure_authed_access do
plug(Guardian.Plug.EnsureAuthenticated, %{"typ" => "access", handler: MyApp.HttpErrorHandler})
end
Эти pipeline могут быть использованы, чтобы собрать из них реализацию различных требований к аутентификации. Первый вариант сначала пытается найти токен в сессии, потом — в заголовках запроса. Если находит, загружает ресурс.
Второй — требует наличия корректного проверенного токена типа “access”. Для использования их нужно добавить в роутер:
scope "/", MyApp do
pipe_through([:browser, :maybe_browser_auth])
get("/login", LoginController, :new)
post("/login", LoginController, :create)
delete("/login", LoginController, :delete)
end
scope "/", MyApp do
pipe_through([:browser, :maybe_browser_auth, :ensure_authed_access])
resource("/protected/things", ProtectedController)
end
Путь login
в примере выше будет с аутентифицированными пользователем (если таковой есть).
Во втором же примере мы проверяем, что токен присутствует и правилен.
Нет никаких требований по обязательному наличию их в процессе обработки запросов, можно добавить их только в тех местах, где это необходимо.
На данный момент всё ещё не хватает одной части — обработчика ошибок, возникающих в EnsureAuthenticated
.
Это очень простой модуль, у которого должны быть реализованы две функции:
-
unauthenticated/2
-
unauthorized/2
Обе эти функции принимают в качестве параметров Plug.Conn и параметры запроса. Для этого можно использовать даже Phoenix контроллер!
Контроллер
В контроллере есть несколько вариантов получения доступа к текущему пользователю. Начнем с самого простого:
defmodule MyApp.MyController do
use MyApp.Web, :controller
use Guardian.Phoenix.Controller
def some_action(conn, params, user, claims) do
# Здесь может быть какой-то обработчик
end
end
После подключения модуля Guardian.Phoenix.Controller
обработчики будут получать два дополнительных параметра, по которым можно делать сопоставление с образцом.
Стоит заметить, что если не использовался EnsureAuthenticated
, то вместо пользователя может быть nil
.
Второй, более гибкий/очевидный вариант — использовать специальные функции самого Guardian
.
defmodule MyApp.MyController do
use MyApp.Web, :controller
def some_action(conn, params) do
if Guardian.Plug.authenticated?(conn) do
user = Guardian.Plug.current_resource(conn)
else
# Пользователь не найден
end
end
end
Авторизация/Выход
Реализация входа и выхода с использованием браузерной сессии довольно проста. В контроллере входа:
def create(conn, params) do
case find_the_user_and_verify_them_from_params(params) do
{:ok, user} ->
# С указанием типа токена `access`.
# Другие типы также могут быть использованы (например, `:refresh`)
conn
|> Guardian.Plug.sign_in(user, :access)
|> respond_somehow()
{:error, reason} ->
nil
# Обработка ситуации, когда предоставлены неверные данные
end
end
def delete(conn, params) do
conn
|> Guardian.Plug.sign_out()
|> respond_somehow()
end
Когда речь идет об авторизации в API — ситуация слегка отличается, ведь тогда нет сессии и нужно передать токен клиенту.
Для входа в API скорее всего будет использоваться заголовок Authorization
в запросах к приложению.
Этот метод полезен, когда вы не собираетесь использовать сессии.
def create(conn, params) do
case find_the_user_and_verify_them_from_params(params) do
{:ok, user} ->
{:ok, jwt, _claims} = Guardian.encode_and_sign(user, :access)
conn |> respond_somehow(%{token: jwt})
{:error, reason} ->
# Обработка ситуации, когда предоставлены неверные данные
end
end
def delete(conn, params) do
jwt = Guardian.Plug.current_token(conn)
Guardian.revoke!(jwt)
respond_somehow(conn)
end
Реализация логина через браузерную сессию самостоятельно вызывает encode_and_sign
внутри имплементации Guardian.Plug.sign_in
. Потому оба приведённых примера эквивалентны.
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!