简体   繁体   English

为什么在重定向到非实时视图页面后会再次安装此实时视图?

[英]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.newphx.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.生成的UserSessionControllerdelete操作会触发生成的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.

相关问题 已经登录后被重定向到登录页面 - Being redirected to login page after being already logged in 在GWT中:如何为页面添加书签并在身份验证后能够重定向到该页面? - In GWT: how to bookmark a page and be able to be redirected to it after authentication? 在 nuxtjs 和 laravel sanctum 中刷新页面后重定向到登录 - Redirected to login after refreshing page in nuxtjs and laravel sanctum 登录 Odoo12 后系统重定向到相同的登录页面 - System redirected to same login page after login in Odoo12 为什么我在访问安全页面时没有被重定向到 /login? - Why do I not get redirected to /login when accessing a secure page? Next JS 使用魔法登录,在从电子邮件登录重定向后尝试抓取路由器 - Next JS with magic login, trying to grab the router after being redirected from email login Angular 登录成功后再次重定向到登录页面 - Angular redirect again to login page after successfully login 通过身份验证的用户被重定向到登录 - Authenticated users being redirected to login 为什么在Angular11中成功注册并登录用户后,我没有使用router.navigate()重定向? - Why am I not redirected using router.navigate() in After successful registration and login of a user in Angular11? 成功登录后,用户将重定向到SPA加载的主页。 在JWT中使用会话cookie是个坏主意吗? - after successful login the user is redirected to the home page where the SPA loads. Is using session cookie with JWT a bad idea?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM