檢視原始碼 安全性考量
LiveView 起步時會是一個一般的HTTP要求。接著會建立一個有狀態的連線。HTTP的要求與有狀態的連線都會透過參數與階段接收客戶端的資料。
這意謂著必須在 HTTP 要求(外掛程式管線)與有狀態的連線(LiveView mount)中進行階段驗證。
驗證與授權
在談論安全性時,通常會使用驗證與授權這兩個用詞。驗證是指辨識一個使用者。授權是指判斷一個使用者是否能存取系統中的特定資源或功能。
在一個一般的網路應用程式中,一旦使用者驗證通過,例如輸入電子郵件和密碼,或使用第三方服務如 Google、Twitter 或 Facebook,便會有一個辨識該使用者的代幣儲存在階段,這個代幣就像一個保存在使用者瀏覽器中的 cookie(一對鍵值)。
每當發出一個要求,我們就會從階段中讀取值,如果有效,我們會從資料庫中擷取儲存在階段中的使用者。階段會由 Phoenix 自動驗證,而且像 mix phx.gen.auth
等的工具可以為您產生一個驗證系統的組成區塊。
一旦使用者驗證通過,他們就可以在頁面上執行許多動作,其中有些動作需要特定的權限。這稱為授權,這些特定的規則在每個應用程式中常會有所不同。
在一個一般的網路應用程式中,我們會在每個要求上執行驗證與授權檢查。由於 LiveView 會以一個一般的 HTTP 要求開始,因此它們可以透過外掛程式與一般的要求共用驗證邏輯。一旦使用者驗證通過,我們通常會在 mount
回呼上驗證階段。授權規則一般會在 mount
(例如,使用者是否被允許看到此頁面?)和 handle_event
(使用者是否被允許刪除此項目?)上發生。
mounting 考量
無論是初始的 HTTP mount 或 LiveView 連線時,都會呼叫 mount/3
回呼。因此,在 mount 期間執行的任何授權都會涵蓋所有情境。
使用者經過授權且儲存在此階段後,提取使用者的邏輯以及進一步授權其帳戶的需要在 LiveView 中執行。例如,如果您具有下列的插入程式
plug :ensure_user_authenticated
plug :ensure_user_confirmed
那麼 mount/3
會回應 LiveView 的 callback,執行相同的驗證
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
) 的掛接功能,讓您可以封裝此邏輯,並在每次掛接時執行,就如同在插入程式中操作一般
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
。這樣可以避免在 parent-child 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
callback 處理。因此,您通常會在這些 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_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 與一般的網路應用程式非常相似。最後我們必須認證和授權每個使用者。主要的差異發生在登出或撤銷存取的時候。
因為 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
提供的相同機制。因此,如果你的應用程式同時使用 channels 和 LiveViews,你可以使用相同的技術來斷開任何有狀態的連線。
live_session
和 live_redirect
LiveView 支援即時重新導向,允許使用者透過 LiveView 連線在頁面間導覽。每當有 live_redirect
時,將會 mount 一個新的 LiveView,略過一般的 HTTP 要求,而且不會經過插入程序。
但是,如果你想在應用程式的部分之間建立更強的邊界,你也可以使用 Phoenix.LiveView.Router.live_session/2
來群組你的 live 路由。這可以很方便,因為你只能在相同的 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
現在,每次嘗試瀏覽管理面板時,以及離開時,將發生常規頁面導覽,且將建立一個全新的即時連線。
再次強調,值得注意的是,即時檢視需要它們自己的安全性檢查,因此我們在上方使用pipe_through
來保護常規路由(取得、新增文章等),而即時檢視應使用on_mount
hooks執行自己的檢查。
可以在同一組內使用live_session
強制執行每個即時檢視群組不同的根佈局,這是因為佈局並不會在即時重新導向過程中更新
live_session :default, root_layout: {Layouts, :root} do
...
end
live_session :admin, root_layout: {Layouts, :admin} do
...
end
最後,你甚至可以合併live_session
與on_mount
。你可以不用在每個即時檢視宣告on_mount
,而是在路由層級宣告,它會在底下所有即時檢視上強制執行
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
hook。這個模組在本指南前面定義過。我們也會透過:authenticate_user
導通一般的網頁要求,它必須執行與MyAppWeb.UserLiveAuth
相同的檢查,但量身打造為插入程式。
類似地,:admin
live_session
有它自己的驗證流程,由MyAppWeb.AdminLiveAuth
提供支援。它也會定義一個名為:authenticate_admin
的等效插入程式,將會用於任何一般的要求。如果在即時會話下沒有定義一般的網頁要求,則pipe_through
檢查就不是必需的。
在live_session
上宣告on_mount
與在每個即時檢視宣告相同。它會在每次掛載即時檢視時執行,即使在live_redirect
之後。
總結
需要牢記的重要觀念:
你的驗證邏輯(讓使用者登入)通常是你的常規 Web 要求程序的一部分,由控制器和即時檢視共享。然後,驗證會將使用者資訊儲存在會話中。常規 Web 要求使用
plug
從會話中讀取使用者,即時檢視會在on_mount
回呼函式中讀取。這在兩種情況下通常是一次簡單的資料庫查詢。執行mix phx.gen.auth
會設定所有必要的項目通過驗證後,你會在即時檢視中執行授權邏輯,兩者都會在
mount
(例如「使用者可以查看此頁面嗎?」)期間和事件期間(例如「使用者可以刪除這項嗎?」)發生。這些規則通常是與網域/業務相關的,通常會在你的內容模組中發生。這也常規要求與回應的條件live_session
可用於在 LiveView 群組間繪製界線。儘管你可以使用live_session
在不同的授權規則間繪製線條,但這樣做將導致網頁頻繁重新載入。基於這個原因,我們通常會使用live_session
來強制執行不同的驗證需求,或在你需要變更根布局時