[英]Why is this liveview mounting again after being redirected to a non-liveview page?
I'm working in a project that is mostly a freshly generated web app from phx.new
and phx.gen.auth
.我在一个项目中工作,该项目主要是从
phx.new
和phx.gen.auth
新生成的网络应用程序。 I have an index page that is non-liveview.我有一个非实时查看的索引页面。 After login, the user is redirected to the main page, which is a liveview.
登录后,用户被重定向到主页,这是一个实时视图。
The expectation: After clicking the generated Log out
link, the user should be redirected to the /
index page, which is not a liveview.期望:点击生成的
Log out
链接后,用户应该被重定向到/
索引页面,这不是一个实时视图。 This behavior is specified by the generated authentication.此行为由生成的身份验证指定。
The experience: The problem is that when I click the generated Log out
link, instead of being redirected to the logged out index splash page, as the generated authentication is written to do, instead I'm redirected to the login page, where I see two flash messages: one :info
flash indicating successful logout, and a second :error
flash complaining ""You must be logged in to access this page." I don't want users to see that :error
flash on the login page, and worse, I think, is the fact that the reason that :error
flash is appearing is because the PageLive
liveview, which is not present on the index page, is running its mount/3
function again (a third time), which is causing the liveview authentication to run again, and cause a second redirect. Importantly, this issue occurs intermittently , ie sometimes the redirect works correctly and sends the user to the index page without issue, and other times the second, redundant redirect and the mistaken flash message is displayed. I think this经验:问题是当我单击生成的
Log out
链接时,而不是被重定向到注销的索引启动页面,因为生成的身份验证是为了执行而我被重定向到登录页面,在那里我看到两条 flash 消息:一条:info
flash 表示成功注销,第二条:error
flash 抱怨“你必须登录才能访问此页面。”我不希望用户在登录页面上看到:error
flash,以及更糟糕的是,我认为,出现:error
flash 的原因是因为PageLive
没有出现在索引页面上,它再次运行它的mount/3
功能(第三次),这导致liveview身份验证再次运行,并导致第二次重定向。重要的是,这个问题间歇性地发生,即有时重定向工作正常并将用户发送到索引页面没有问题,而其他时候第二次冗余重定向和错误的闪现消息是显示。我认为这个 indicates some kind of race condition.表示某种竞争条件。
I have a relatively newly generated project with these routes (among others):我有一个相对较新生成的项目,其中包含这些路线(以及其他路线):
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
The authentication was generated by phx.gen.auth
.身份验证由
phx.gen.auth
生成。 The delete
action in the generated UserSessionController
triggers the generated UserAuth.log_out_user/1
to fire.生成的
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
The live route in the router to /dash
routes through a liveview called PageLive
, which simply mounts over some authentication, as recommended in liveview docs :路由器中的实时路由到
/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
Here's the log of the process after the user clicks log out:以下是用户点击注销后的进程日志:
**[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
Notice how in the logs above, the 302 redirect occurs, and then immediately the socket reconnects and mount/3
runs, which then triggers another redirect, this time to the /users/log_in
route.注意在上面的日志中,302 重定向是如何发生的,然后套接字立即重新连接并运行
mount/3
,然后触发另一个重定向,这次是到/users/log_in
路由。 As far as I understand, the socket should not be trying to reconnect here, and I can't see what's triggering this.据我所知,套接字不应该在这里尝试重新连接,我看不出是什么触发了这个。
Why is the PageLive
mount being triggered again after the 302 redirect to a non-liveview page upon logout, thus triggering a second redirect to the login page?为什么在注销时302重定向到非实时查看页面后再次触发
PageLive
挂载,从而触发第二次重定向到登录页面?
They key is this code他们的关键是这个代码
MyappWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
in log_out_user/1
.在
log_out_user/1
。 Here you disconnect the live view socket via an Erlang message.在这里,您通过 Erlang 消息断开实时取景套接字。
This triggers the socket to shutdown server side (see Phoenix.Socket )这会触发套接字关闭服务器端(请参阅Phoenix.Socket )
def __info__(%Broadcast{event: "disconnect"}, state) do
{:stop, {:shutdown, :disconnected}, state}
end
But then it is a live view, which will reconnect after a disconnect happened via client side javascript and then remount the live view, which causes MyappWeb.UserLiveAuth
to add the flash message again.但是它是一个实时视图,它会在通过客户端 javascript 断开连接后重新连接,然后重新安装实时视图,这会导致
MyappWeb.UserLiveAuth
再次添加 Flash 消息。
For reference, check the LiveView docs :作为参考, 请查看 LiveView 文档:
Once a LiveView is disconnected, the client will attempt to reestablish the connection and re-execute the mount/3 callback.
一旦 LiveView 断开连接,客户端将尝试重新建立连接并重新执行 mount/3 回调。 In this case, if the user is no longer logged in or it no longer has access to the current resource, mount/3 will fail and the user will be redirected.
在这种情况下,如果用户不再登录或不再访问当前资源,mount/3 将失败并且用户将被重定向。
A potential solution could be, to do the flash + redirect already in a plug inside of the routing pipeline instead of the mount, so non logged in users will be redirected when loading the page, and then just {:halt, socket}
in the live view mount, so there is no redirect on logout.一个潜在的解决方案可能是,在路由管道内的插件而不是挂载中执行 flash + 重定向,因此在加载页面时将重定向未登录的用户,然后在
{:halt, socket}
中实时取景安装,因此注销时没有重定向。 Or alternatively send the broadcast for disconnecting after the logout request has already redirected (maybe spinning off an async Task
could help).或者,在注销请求已经重定向后发送广播以断开连接(也许分离异步
Task
可能会有所帮助)。
So maybe wrap the broadcast like this, to make the browser close the liveview itself (while still redirecting all other open live views):所以也许可以像这样包装广播,让浏览器关闭实时视图本身(同时仍然重定向所有其他打开的实时视图):
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)
In user_auth.ex
, I have these lines, which delete the user session from the db:在
user_auth.ex
,我有这些行,它们从数据库中删除用户会话:
def log_out_user(conn) do
user_token = get_session(conn, :user_token)
user_token && Accounts.delete_session_token(user_token)
The reason the problem is occurring is because the live mount is failing authentication, since the session token no longer exists in the database.出现问题的原因是实时安装未通过身份验证,因为会话令牌不再存在于数据库中。 This triggers a redirect before the regular
UserAuth.log)out_user/1
redirect has a chance to fire.这会在常规
UserAuth.log)out_user/1
重定向有机会触发之前触发重定向。
Inspired by @smallbutton's solution, I'm using ensuring that the session token is not deleted until after the log_out_user/1
redirect to avoid the race condition:受@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
This is an unsatisfying workaround though, because I don't like to have to manually coordinate events like this.不过,这是一个不令人满意的解决方法,因为我不喜欢像这样手动协调事件。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.