[英]Why is this liveview mounting again after being redirected to a non-liveview page?
我在一个项目中工作,该项目主要是从phx.new
和phx.gen.auth
新生成的网络应用程序。 我有一个非实时查看的索引页面。 登录后,用户被重定向到主页,这是一个实时视图。
期望:点击生成的Log out
链接后,用户应该被重定向到/
索引页面,这不是一个实时视图。 此行为由生成的身份验证指定。
经验:问题是当我单击生成的Log out
链接时,而不是被重定向到注销的索引启动页面,因为生成的身份验证是为了执行而我被重定向到登录页面,在那里我看到两条 flash 消息:一条:info
flash 表示成功注销,第二条:error
flash 抱怨“你必须登录才能访问此页面。”我不希望用户在登录页面上看到:error
flash,以及更糟糕的是,我认为,出现:error
flash 的原因是因为PageLive
没有出现在索引页面上,它再次运行它的mount/3
功能(第三次),这导致liveview身份验证再次运行,并导致第二次重定向。重要的是,这个问题间歇性地发生,即有时重定向工作正常并将用户发送到索引页面没有问题,而其他时候第二次冗余重定向和错误的闪现消息是显示。我认为这个表示某种竞争条件。
我有一个相对较新生成的项目,其中包含这些路线(以及其他路线):
router.ex
scope "/", MyappWeb do
pipe_through :browser
live_session :default do
live "/dash", PageLive, :index
end
end
scope "/", MyappWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated]
get "/", PageController, :index
end
scope "/", MyappWeb do
pipe_through [:browser]
delete "/users/log_out", UserSessionController, :delete
end
身份验证由phx.gen.auth
生成。 生成的UserSessionController
的delete
操作会触发生成的UserAuth.log_out_user/1
触发。
user_session_controller.ex
def delete(conn, _params) do
conn
|> put_flash(:info, "Logged out successfully.")
|> UserAuth.log_out_user()
end
user_auth.ex
def log_out_user(conn) do
user_token = get_session(conn, :user_token)
user_token && Accounts.delete_session_token(user_token)
if live_socket_id = get_session(conn, :live_socket_id) do
MyappWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
end
conn
|> renew_session()
|> delete_resp_cookie(@remember_me_cookie)
|> redirect(to: "/")
end
路由器中的实时路由到/dash
路由通过一个名为PageLive
的实时PageLive
,它只是安装在一些身份验证上,如PageLive
文档中所推荐的:
page_live.ex
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
alias MyappWeb.Live.Components.PackageSearch
alias MyappWeb.Live.Components.Tabs
alias MyappWeb.Live.Components.Tabs.TabItem
on_mount MyappWeb.UserLiveAuth
end
user_live_auth.ex
defmodule MyappWeb.UserLiveAuth do
import Phoenix.LiveView, only: [assign_new: 3, redirect: 2]
alias Myapp.Accounts
alias Myapp.Accounts.User
alias MyappWeb.Router.Helpers, as: Routes
def mount(_params, session, socket) do
socket =
assign_new(socket, :current_user, fn ->
find_current_user(session)
end)
case socket.assigns.current_user do
%User{} ->
{:cont, socket}
_ ->
socket =
socket
|> put_flash(:error, "You must be logged in to access this page.")
|> redirect(to: Routes.user_session_path(socket, :new))
{:halt, socket}
end
end
defp find_current_user(session) do
with user_token when not is_nil(user_token) <- session["user_token"],
%User{} = user <- Accounts.get_user_by_session_token(user_token),
do: user
end
end
以下是用户点击注销后的进程日志:
**[info] POST /users/log_out**
[debug] Processing with MyappWeb.UserSessionController.delete/2
Parameters: %{"_csrf_token" => "ET8xMSU5KSEedycKEAcJfX0JCl45LmcF_VEHANhinNqHcaz6MFRkIqWu", "_method" => "delete"}
Pipelines: [:browser]
[debug] QUERY OK source="users_tokens" db=1.8ms idle=389.7ms
SELECT u1."id", u1."email", u1."hashed_password", u1."confirmed_at", u1."first_name", u1."last_name", u1."username", u1."inserted_at", u1."updated_at" FROM "users_tokens" AS u0 INNER JOIN "users" AS u1 ON u1."id" = u0."user_id" WHERE ((u0."token" = $1) AND (u0."context" = $2)) AND (u0."inserted_at" > $3::timestamp + (-(60)::numeric * interval '1 day')) [<<159, 144, 113, 83, 223, 12, 183, 119, 50, 248, 83, 234, 128, 237, 129, 112, 138, 147, 148, 100, 67, 163, 50, 244, 127, 26, 254, 184, 102, 74, 11, 52>>, "session", ~U[2021-10-06 22:13:44.080128Z]]
[debug] QUERY OK source="users_tokens" db=1.7ms idle=391.8ms
DELETE FROM "users_tokens" AS u0 WHERE ((u0."token" = $1) AND (u0."context" = $2)) [<<159, 144, 113, 83, 223, 12, 183, 119, 50, 248, 83, 234, 128, 237, 129, 112, 138, 147, 148, 100, 67, 163, 50, 244, 127, 26, 254, 184, 102, 74, 11, 52>>, "session"]
**[info] Sent 302 in 6ms**
**[info] CONNECTED TO Phoenix.LiveView.Socket in 64µs
Transport: :websocket
Serializer: Phoenix.Socket.V2.JSONSerializer
Parameters: %{"_csrf_token" =>** "ET8xMSU5KSEedycKEAcJfX0JCl45LmcF_VEHANhinNqHcaz6MFRkIqWu", "_mounts" => "0", "_track_static" => %{"0" => "http://localhost:4000/assets/app.css", "1" => "http://localhost:4000/assets/app.js"}, "vsn" => "2.0.0"}
[debug] QUERY OK source="users_tokens" db=1.6ms idle=422.5ms
SELECT u1."id", u1."email", u1."hashed_password", u1."confirmed_at", u1."first_name", u1."last_name", u1."username", u1."inserted_at", u1."updated_at" FROM "users_tokens" AS u0 INNER JOIN "users" AS u1 ON u1."id" = u0."user_id" WHERE ((u0."token" = $1) AND (u0."context" = $2)) AND (u0."inserted_at" > $3::timestamp + (-(60)::numeric * interval '1 day')) [<<159, 144, 113, 83, 223, 12, 183, 119, 50, 248, 83, 234, 128, 237, 129, 112, 138, 147, 148, 100, 67, 163, 50, 244, 127, 26, 254, 184, 102, 74, 11, 52>>, "session", ~U[2021-10-06 22:13:44.110158Z]]
**[info] GET /users/log_in**
[debug] Processing with MyappWeb.UserSessionController.new/2
Parameters: %{}
Pipelines: [:browser, :redirect_if_user_is_authenticated]
[info] Sent 200 in 6ms
注意在上面的日志中,302 重定向是如何发生的,然后套接字立即重新连接并运行mount/3
,然后触发另一个重定向,这次是到/users/log_in
路由。 据我所知,套接字不应该在这里尝试重新连接,我看不出是什么触发了这个。
为什么在注销时302重定向到非实时查看页面后再次触发PageLive
挂载,从而触发第二次重定向到登录页面?
他们的关键是这个代码
MyappWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
在log_out_user/1
。 在这里,您通过 Erlang 消息断开实时取景套接字。
这会触发套接字关闭服务器端(请参阅Phoenix.Socket )
def __info__(%Broadcast{event: "disconnect"}, state) do
{:stop, {:shutdown, :disconnected}, state}
end
但是它是一个实时视图,它会在通过客户端 javascript 断开连接后重新连接,然后重新安装实时视图,这会导致MyappWeb.UserLiveAuth
再次添加 Flash 消息。
作为参考, 请查看 LiveView 文档:
一旦 LiveView 断开连接,客户端将尝试重新建立连接并重新执行 mount/3 回调。 在这种情况下,如果用户不再登录或不再访问当前资源,mount/3 将失败并且用户将被重定向。
一个潜在的解决方案可能是,在路由管道内的插件而不是挂载中执行 flash + 重定向,因此在加载页面时将重定向未登录的用户,然后在{:halt, socket}
中实时取景安装,因此注销时没有重定向。 或者,在注销请求已经重定向后发送广播以断开连接(也许分离异步Task
可能会有所帮助)。
所以也许可以像这样包装广播,让浏览器关闭实时视图本身(同时仍然重定向所有其他打开的实时视图):
Task.async(fn ->
:timer.sleep(1000) # Give the browser some time to process the response and close the LV
MyappWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
end)
在user_auth.ex
,我有这些行,它们从数据库中删除用户会话:
def log_out_user(conn) do
user_token = get_session(conn, :user_token)
user_token && Accounts.delete_session_token(user_token)
出现问题的原因是实时安装未通过身份验证,因为会话令牌不再存在于数据库中。 这会在常规UserAuth.log)out_user/1
重定向有机会触发之前触发重定向。
受@smallbutton 解决方案的启发,我使用确保在log_out_user/1
重定向之后不会删除会话令牌以避免竞争条件:
def log_out_user(conn) do
if live_socket_id = get_session(conn, :live_socket_id) do
MyAppWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
end
# TODO is there a better way to handle this issue?
Task.async(fn ->
:timer.sleep(1000)
user_token = get_session(conn, :user_token)
user_token && Accounts.delete_session_token(user_token)
end)
conn
|> renew_session()
|> delete_resp_cookie(@remember_me_cookie)
|> redirect(to: "/")
end
不过,这是一个不令人满意的解决方法,因为我不喜欢像这样手动协调事件。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.