Вопросы безопасности
Исходный кодЖизненный цикл LiveView начинается с обычного HTTP-запроса. Затем устанавливается соединение с сохранением состояния. И HTTP-запрос, и соединение с сохранением состояния получают данные клиента через параметры и сессию.
Это означает, что проверку сессии необходимо выполнять как в HTTP-запросе (конвейере plug), так и в соединении с сохранением состояния (монтировании LiveView).
Аутентификация и авторизация
В контексте безопасности обычно используют два термина: аутентификация и авторизация. Аутентификация — это установление личности пользователя. Авторизация — это проверка того, есть ли у пользователя доступ к определённому ресурсу или функции системы.
В обычном веб-приложении после аутентификации пользователя — например, по адресу электронной почты и паролю или с помощью стороннего сервиса, такого как Google, Twitter или Facebook, — в сессии сохраняется токен, идентифицирующий пользователя. Сессия — это cookie (пара «ключ-значение»), хранящаяся в браузере пользователя.
При каждом запросе мы считываем значение из сессии и, если оно действительно, получаем из базы данных пользователя, сохранённого в сессии. Phoenix автоматически проверяет сессию, а такие инструменты, как mix phx.gen.auth, могут сгенерировать для вас основные компоненты системы аутентификации.
После аутентификации пользователь может выполнять на странице множество действий, для некоторых из которых нужны определённые разрешения. Это называется авторизацией, а конкретные правила часто отличаются в разных приложениях.
В обычном веб-приложении мы проверяем аутентификацию и авторизацию при каждом запросе. Поскольку LiveView запускаются как обычный HTTP-запрос, они используют общую с обычными запросами логику аутентификации через plug. Запрос поступает в endpoint, который вызывает router. С помощью plug обеспечивается аутентификация пользователя и сохранение необходимой информации в сессии.
После аутентификации пользователя мы обычно проверяем сессию в callback mount/3.
Правила авторизации обычно проверяются в mount/3 (например, может ли пользователь просматривать эту страницу?), handle_params/3 (может ли пользователь перейти сюда?), а также в Phoenix.LiveView.handle_event/3 или Phoenix.LiveComponent.handle_event/3 (может ли пользователь удалить этот элемент?).
live_session
Основной механизм группировки LiveView — это Phoenix.LiveView.Router.live_session/2. Благодаря ему LiveView гарантирует, что события навигации внутри одной live_session будут обходиться без обычных HTTP-запросов, не проходя через конвейер plug. События между разными live-сессиями будут проходить через router.
Например, представьте, что вам нужно аутентифицировать два разных типа пользователей. Обычные пользователи входят в систему по адресу электронной почты и паролю, а для панели администратора используется HTTP-аутентификация. Для каждого потока аутентификации можно указать отдельные live_sessions:
scope "/" do
pipe_through [:authenticate_user]
get ...
live_session :default do
live ...
end
end
scope "/admin" do
pipe_through [:http_auth_admin]
get ...
live_session :admin do
live ...
end
end
Теперь при каждой попытке перейти в панель администратора или выйти из неё будет выполняться обычная навигация по страницам и устанавливаться совершенно новое соединение LiveView.
Важно помнить, что для LiveView нужны собственные проверки безопасности. Поэтому выше мы используем pipe_through для защиты обычных маршрутов (get, post и т. д.), а LiveView должны выполнять собственные проверки в callback mount/3 (или с помощью хуков Phoenix.LiveView.on_mount/1).
Для этого можно сочетать live_session с on_mount, а также использовать другие параметры, например :root_layout. Вместо того чтобы объявлять on_mount для каждого LiveView, можно объявить его на уровне router — тогда он будет применяться ко всем расположенным под ним LiveView:
scope "/" do
pipe_through [:authenticate_user]
live_session :default, on_mount: MyAppWeb.UserLiveAuth do
live ...
end
end
scope "/admin" do
pipe_through [:authenticate_admin]
live_session :admin, on_mount: MyAppWeb.AdminLiveAuth do
live ...
end
end
Каждый live-маршрут под :default live_session при монтировании вызовет хук MyAppWeb.UserLiveAuth. Этот модуль был определён ранее в руководстве. Обычные веб-запросы также будут проходить через :authenticate_user, который должен выполнять те же проверки, что и MyAppWeb.UserLiveAuth, но с учётом особенностей plug.
Аналогично, у :admin live_session есть собственный поток аутентификации, реализованный с помощью MyAppWeb.AdminLiveAuth. В нём также определён аналог plug под названием :authenticate_admin, который будет использоваться для обычных запросов. Если под live-сессией не определено обычных веб-запросов, проверки pipe_through не нужны.
Объявление on_mount в live_session равнозначно объявлению этого параметра в каждом LiveView. Теперь рассмотрим логику, которую обычно выполняют при монтировании.
Особенности монтирования
Callback mount/3 вызывается и при первоначальном монтировании через HTTP, и при подключении LiveView. Поэтому любая авторизация, выполняемая во время монтирования, охватит все сценарии.
После того как пользователь прошёл аутентификацию и его данные сохранены в сессии, получение пользователя и дальнейшая проверка прав его учётной записи должны выполняться внутри LiveView. Например, если у вас есть следующие plug:
plug :ensure_user_authenticated plug :ensure_user_confirmed
Тогда callback mount/3 в вашем LiveView должен выполнять те же проверки:
def mount(_params, %{"user_id" => user_id} = _session, socket) do
socket = assign(socket, current_user: Accounts.get_user!(user_id))
socket =
if socket.assigns.current_user.confirmed_at do
socket
else
redirect(socket, to: "/login")
end
{:ok, socket}
end
Хук on_mount позволяет инкапсулировать эту логику и выполнять её при каждом монтировании:
defmodule MyAppWeb.UserLiveAuth do
import Phoenix.Component
import Phoenix.LiveView
alias MyAppWeb.Accounts # from `mix phx.gen.auth`
def on_mount(:default, _params, %{"user_token" => user_token} = _session, socket) do
socket =
assign_new(socket, :current_user, fn ->
Accounts.get_user_by_session_token(user_token)
end)
if socket.assigns.current_user.confirmed_at do
{:cont, socket}
else
{:halt, redirect(socket, to: "/login")}
end
end
end
Мы используем assign_new/3. Это удобный способ избежать многократного получения current_user во вложенных родительских и дочерних LiveView.
Теперь мы можем использовать хук там, где это необходимо. Один из вариантов — указать хук в router в разделе live_session:
live_session :default, on_mount: MyAppWeb.UserLiveAuth do # Your routes end
Другой вариант — указать хук непосредственно в LiveView:
defmodule MyAppWeb.PageLive do use MyAppWeb, :live_view on_mount MyAppWeb.UserLiveAuth ... end
При желании можно добавить хук в def live_view в разделе MyAppWeb, чтобы по умолчанию запускать его во всех LiveView:
def live_view do
quote do
use Phoenix.LiveView
on_mount MyAppWeb.UserLiveAuth
unquote(html_helpers())
end
end
Особенности обработки событий
Каждый раз, когда пользователь выполняет действие в вашей системе, нужно проверять, есть ли у него на это права, независимо от того, используете ли вы LiveView. Например, представьте, что пользователь может просматривать все проекты в веб-приложении, но не имеет права удалять ни один из них. На уровне интерфейса вы можете скрыть кнопку удаления в списке проектов, однако опытный пользователь всё равно может напрямую обратиться к серверу и запросить удаление. Поэтому необходимо всегда проверять разрешения на сервере.
В LiveView большинство действий обрабатывается callback handle_event/3 (или Phoenix.LiveComponent.handle_event/3 в компонентах). Поэтому обычно проверка прав пользователя выполняется в этих callback. В описанном сценарии это можно реализовать так:
on_mount MyAppWeb.UserLiveAuth
def mount(_params, _session, socket) do
{:ok, load_projects(socket)}
end
def handle_event("delete_project", %{"project_id" => project_id}, socket) do
Project.delete!(socket.assigns.current_scope, project_id)
{:noreply, update(socket, :projects, &Enum.reject(&1, fn p -> p.id == project_id end))}
end
defp load_projects(socket) do
projects = Project.all_projects(socket.assigns.current_scope)
assign(socket, projects: projects)
end
Сначала мы использовали on_mount, чтобы аутентифицировать пользователя на основании данных, хранящихся в сессии. Затем загрузили все проекты, доступные аутентифицированному пользователю в рамках разрешённой ему области. Теперь, когда поступает запрос на удаление проекта, мы по-прежнему передаём текущую область в качестве аргумента контексту Project, чтобы он проверил, разрешено ли пользователю удалять проект. Если удалить его нельзя, вполне допустимо просто вызвать исключение. Ведь пользователи не должны попадать в этот участок кода (если только они не пытаются вмешаться в то, что им не положено!).
Никогда не доверяйте пользовательским данным: параметрам и полезной нагрузке
Общее правило безопасности веб-приложений: никогда не доверяйте пользовательским данным (см. OWASP Top 10). В LiveView оно относится, в частности, к params, передаваемым в callback mount/3 и handle_params/3, а также к payload, передаваемым в handle_event/3 (или Phoenix.LiveComponent.handle_event/3 в компонентах).
Поскольку приложения LiveView обрабатывают взаимодействия с интерфейсом через постоянное соединение, легко забыть, что клиент может изменять данные, отправляемые серверу. Злоумышленник может использовать инструменты разработчика браузера или собственные скрипты, чтобы отправить в LiveView любую полезную нагрузку, полностью обходя ограничения интерфейса. Чтобы следовать рекомендациям таких организаций, как Erlang Ecosystem Foundation (EEF) и OWASP, необходимо предусмотреть защиту при обработке этих параметров.
Для работы с недоверенными данными в LiveView доступны те же механизмы, что и в контроллерах: changeset, области Phoenix и т. д. Рекомендуем ознакомиться с руководством по безопасности Phoenix, где приведены примеры и способы снизить риски.
Отключение всех экземпляров LiveView пользователя
До сих пор модель безопасности LiveView и обычных веб-приложений была практически одинаковой. Ведь мы всегда должны аутентифицировать и авторизовать каждого пользователя. Главное различие проявляется при выходе из системы или отзыве доступа.
Поскольку LiveView поддерживает постоянное соединение между клиентом и сервером, выход пользователя из системы или его удаление из системы не отразится на LiveView, пока пользователь не перезагрузит страницу.
К счастью, это можно решить, задав в сессии live_socket_id. Например, при входе пользователя в систему можно сделать следующее:
conn
|> put_session(:current_user_id, user.id)
|> put_session(:live_socket_id, "users_socket:#{user.id}")
Теперь все сокеты LiveView будут идентифицироваться и прослушивать указанный live_socket_id. Затем можно отключить всех активных пользователей, идентифицируемых этим ID, отправив широковещательное сообщение в топик:
MyAppWeb.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})
Примечание
Если вы использовали mix phx.gen.auth для генерации системы аутентификации, соответствующие строки уже присутствуют в сгенерированном коде. В сгенерированном коде используется user_token вместо обращения к user_id.
После отключения LiveView клиент попытается восстановить соединение и повторно выполнить callback mount/3. В этом случае, если пользователь больше не вошёл в систему или у него больше нет доступа к текущему ресурсу, проверка mount/3 завершится неудачно, и пользователь будет перенаправлен.
Такой же механизм доступен в Phoenix.Channel. Поэтому, если ваше приложение использует и каналы, и LiveView, можно применить тот же подход для отключения любого соединения с сохранением состояния.
Итоги
Важно помнить следующее:
live_sessionможно использовать, чтобы отделять группы LiveView друг от друга. Хотяlive_sessionможно использовать для разграничения различных правил авторизации, это приведёт к частой перезагрузке страниц. Поэтому обычно мы используемlive_session, чтобы применять разные требования к аутентификации или менять корневые макеты.Логика аутентификации (вход пользователя в систему) обычно является частью конвейера обычных веб-запросов и используется как контроллерами, так и LiveView. При аутентификации информация о пользователе сохраняется в сессии. Обычные веб-запросы используют
plug, чтобы получить пользователя из сессии, а LiveView считывают его в callbackon_mount. В обоих случаях обычно выполняется один запрос к базе данных.mix phx.gen.authнастраивает всё необходимое.После аутентификации логика авторизации в LiveView выполняется как во время
mount/handle_params(например, «может ли пользователь просматривать эту страницу?»), так и при обработке событий (например, «может ли пользователь удалить этот элемент?»). Эти правила часто зависят от предметной области или бизнес-логики и обычно реализуются в модулях контекста с помощью областей. То же самое требуется и для обычных запросов и ответов.
© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix-live-view.hexdocs.pm/security-model.html