查看源代码 安全注意事项
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_session
和 live_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_session
与 on_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
来执行不同的身份验证要求,或者在需要更改根布局时使用它。