查看源代码 安全注意事项

LiveView 的生命周期始于一个普通的 HTTP 请求,然后建立一个有状态的连接。HTTP 请求和有状态的连接都通过参数和会话接收客户端数据。

这意味着任何会话验证都必须在 HTTP 请求(plug 管道)和有状态的连接(LiveView 挂载)中进行。

身份验证与授权

在谈论安全时,通常使用两个术语:身份验证和授权。身份验证是指识别用户。授权是指确定用户是否可以访问系统中的特定资源或功能。

在传统的 Web 应用程序中,用户通过输入电子邮件和密码或使用第三方服务(如 Google、Twitter 或 Facebook)进行身份验证后,用于识别用户的令牌将存储在会话中,会话是一个存储在用户浏览器中的 cookie(键值对)。

每次有请求时,我们都会从会话中读取该值,如果该值有效,我们就会从数据库中获取存储在会话中的用户。会话会由 Phoenix 自动验证,工具如 mix phx.gen.auth 可以为您生成身份验证系统的基础模块。

用户经过身份验证后,他们可以在页面上执行许多操作,其中一些操作需要特定的权限。这被称为授权,具体的规则通常因应用程序而异。

在传统的 Web 应用程序中,我们在每个请求上执行身份验证和授权检查。鉴于 LiveView 从普通的 HTTP 请求开始,它们通过 plug 与普通请求共享身份验证逻辑。用户经过身份验证后,我们通常会在 mount 回调中验证会话。授权规则通常在 mount(例如,用户是否可以查看此页面?)和 handle_event(用户是否可以删除此项?)中执行。

挂载注意事项

在初始 HTTP 挂载和 LiveView 连接时都会调用 mount/3 回调。因此,在挂载过程中执行的任何授权都将涵盖所有场景。

用户经过授权并存储在会话中后,获取用户并进一步授权其帐户的逻辑需要在 LiveView 内部进行。例如,如果您有以下 plug

plug :ensure_user_authenticated
plug :ensure_user_confirmed

那么 LiveView 的 mount/3 回调应执行相同的验证

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

从 v0.17 开始,LiveView 包含 on_mount (Phoenix.LiveView.on_mount/1) 钩子,它允许您封装此逻辑并在每次挂载时执行它,就像您使用 plug 一样

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。这是一种便利,可以避免在父级和子级 LiveView 之间多次获取 current_user

现在,我们可以在相关时使用该钩子。一种选择是在您的路由器中的 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

如果您愿意,也可以将该钩子添加到 MyAppWeb 下的 def live_view 中,以默认情况下在所有 LiveView 上运行它

def live_view do
  quote do
    use Phoenix.LiveView,
      layout: {MyAppWeb.Layouts, :app}

    on_mount MyAppWeb.UserLiveAuth
    unquote(html_helpers())
  end
end

事件注意事项

用户每次在您的系统上执行操作时,您都应该验证用户是否有权执行该操作,无论您是否使用 LiveView。例如,假设用户可以查看 Web 应用程序中的所有项目,但他们可能没有权限删除任何项目。在 UI 级别,您可以通过在项目列表中不显示删除按钮来相应地处理这种情况,但精通技术的用户可以直接与服务器通信并请求删除。因此,**您必须始终在服务器上验证权限**。

在 LiveView 中,大多数操作由 handle_event 回调处理。因此,您通常在这些回调中授权用户。在刚刚描述的场景中,您可以这样实现

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_user, 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_user)
  assign(socket, projects: projects)
end

首先,我们使用 on_mount 根据存储在会话中的数据对用户进行身份验证。然后,我们根据经过身份验证的用户加载所有项目。现在,每当有请求删除项目时,我们仍然将当前用户作为参数传递给 Project 上下文,因此它会验证用户是否有权删除它。如果无法删除,则引发异常就可以了。毕竟,用户不应该触发此代码路径(除非他们在摆弄不应该摆弄的东西!)。

断开所有实时用户实例的连接

到目前为止,LiveView 和普通 Web 应用程序之间的安全模型非常相似。毕竟,我们必须始终对每个用户进行身份验证和授权。它们之间的主要区别在于注销或撤销访问权限时。

由于 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 断开连接后,客户端将尝试重新建立连接并重新执行 mount/3 回调。在这种情况下,如果用户不再登录或不再具有访问当前资源的权限,mount/3 将失败,用户将被重定向。

这是由 Phoenix.Channel 提供的相同机制。因此,如果您的应用程序同时使用通道和 LiveView,您可以使用相同的技术断开任何有状态连接。

live_sessionlive_redirect

LiveView 支持实时重定向,允许用户通过 LiveView 连接在页面之间导航。每当有 live_redirect 时,都会挂载新的 LiveView,跳过常规 HTTP 请求,并且不会通过 plug 管道。

但是,如果您想在应用程序的不同部分之间建立更严格的界限,您也可以使用 Phoenix.LiveView.Router.live_session/2 来对您的实时路由进行分组。这很方便,因为您只能在同一个 live_session 中的 LiveView 之间执行 live_redirect

例如,假设您需要对两种不同类型的用户进行身份验证。您的普通用户通过电子邮件和密码登录,并且您有一个使用 http 身份验证的管理员仪表板。您可以为每个身份验证流程指定不同的 live_session

live_session :default do
  scope "/" do
    pipe_through [:authenticate_user]
    get ...
    live ...
  end
end

live_session :admin do
  scope "/admin" do
    pipe_through [:http_auth_admin]
    get ...
    live ...
  end
end

现在,每当您尝试导航到管理员面板或退出管理员面板时,都会发生常规页面导航,并且会建立一个全新的实时连接。

再次强调,值得记住的是,LiveView 需要自己的安全检查,因此我们在上面使用 pipe_through 来保护常规路由(get、post 等),LiveView 应该使用 on_mount 钩子运行自己的检查。

live_session 也可以用于强制每个 LiveView 组具有不同的根布局,因为布局不会在实时重定向之间更新

live_session :default, root_layout: {Layouts, :root} do
  ...
end

live_session :admin, root_layout: {Layouts, :admin} do
  ...
end

最后,您甚至可以将 live_sessionon_mount 结合使用。您不必在每个 LiveView 上声明 on_mount,而可以在路由器级别声明它,它将在其下的所有 LiveView 上执行它

live_session :default, on_mount: MyAppWeb.UserLiveAuth do
  scope "/" do
    pipe_through [:authenticate_user]
    live ...
  end
end

live_session :admin, on_mount: MyAppWeb.AdminLiveAuth do
  scope "/admin" do
    pipe_through [:authenticate_admin]
    live ...
  end
end

:default live_session 下的每个实时路由都会在挂载时调用 MyAppWeb.UserLiveAuth 钩子。该模块在本指南中已定义。我们还将通过 :authenticate_user 传递常规 Web 请求,该请求必须执行与 MyAppWeb.UserLiveAuth 相同的检查,但针对 plug 进行调整。

类似地,:admin live_session 具有自己的身份验证流程,由 MyAppWeb.AdminLiveAuth 提供支持。它还定义了一个名为 :authenticate_admin 的等效 plug,它将用于任何常规请求。如果实时会话下没有定义常规 Web 请求,则 pipe_through 检查不是必需的。

live_session 上声明 on_mount 与在每个 LiveView 中声明它完全相同。它将在每次挂载 LiveView 时执行,即使在 live_redirect 后也是如此。

总结

需要注意的重要概念是

  • 您的身份验证逻辑(登录用户)通常是您常规 Web 请求管道的一部分,它由控制器和 LiveView 共享。身份验证然后将用户信息存储在会话中。常规 Web 请求使用 plug 从会话中读取用户,LiveView 在 on_mount 回调中读取它。这两种情况下通常都是对数据库进行一次查找。运行 mix phx.gen.auth 会设置所有必要的配置

  • 经过身份验证后,您在 LiveView 中的授权逻辑将在 mount(例如“用户是否可以查看此页面?”)和事件(例如“用户是否可以删除此项?”)期间执行。这些规则通常与特定领域/业务相关,并且通常发生在您的上下文模块中。这也是对常规请求和响应的要求

  • live_session 可以用来在 LiveView 组之间划定界限。虽然你可以使用 live_session 在不同的授权规则之间划分界限,但这会导致频繁的页面重新加载。因此,我们通常使用 live_session 来执行不同的身份验证要求,或者在需要更改根布局时使用它。