Эликсир / Феникс: как реализовать тайм-аут / истечение времени сессии

Я работаю над ванильным приложением Elixir / Phoenix и следую общим шагам в книге " Программирование Phoenix", чтобы реализовать базовую систему входа и выхода (см. Фрагменты ниже). Однако я не вижу советов в книге или в Интернете о том, как настроить сеансы подключаемых файлов на основе файлов cookie по истечении определенного периода времени. Каковы некоторые подходы к тайм-ауту сессии в приложениях Phoenix?

Вот некоторые соответствующие фрагменты моей системы аутентификации:

В endpoint.exприложение настроено на использование сеанса на основе файлов cookie только для чтения:

plug Plug.Session,
  store: :cookie,
  key: "_zb_key",
  signing_salt: "RANDOM HEX"

Я написал плагин auth.ex который (помимо прочего) может войти в систему аутентифицированного пользователя и может установить current_user по итогам сессии user_id найдено в последующих запросах:

def login!(conn, user) do
  conn
    |> assign(:current_user, user)
    |> put_session(:user_id, user.id)
    |> configure_session(renew: true)
end

# ... more ...

def load_current_user(conn, _opts) do
  cond do
    conn.assigns[:current_user] ->
      conn # If :current_user was already set, honor it
    user_id = get_session(conn, :user_id) ->
      user = Zb.Repo.get!(Zb.User, user_id)
      assign(conn, :current_user, user)
    true ->
      conn # No user_id was found; make no changes
  end
end

# ... more ...

3 ответа

Модуль Plug.Sessions имеет встроенную опцию, позволяющую установить срок действия файла cookie с помощью max_age ключ. Например, продление вашего endpoint.ex фрагмент будет выглядеть так:

plug Plug.Session,
  store: :cookie,
  key: "_zb_key",
  signing_salt: "RANDOM HEX"
  max_age: 24*60*60*37,       # 37 days

Кредит: https://teamgaslight.com/blog/til-how-to-explicitly-set-session-expiration-in-phoenix

Документация: https://hexdocs.pm/plug/Plug.Session.html

Это наше производственное решение ( просмотр в Gist):

sliding_session_timeout.ex
defmodule Auth.SlidingSessionTimeout do
  import Plug.Conn

  def init(opts \\ []) do
    Keyword.merge([timeout_after_seconds: 3600], opts)
  end

  def call(conn, opts) do
    timeout_at = get_session(conn, :session_timeout_at)
    if timeout_at && now() > timeout_at do
      logout_user(conn)
    else
      put_session(conn, :session_timeout_at, new_session_timeout_at(opts[:timeout_after_seconds]))
    end
  end

  defp logout_user(conn) do
    conn
    |> clear_session()
    |> configure_session([:renew])
    |> assign(:session_timeout, true)
  end

  defp now do
    DateTime.utc_now() |> DateTime.to_unix
  end

  defp new_session_timeout_at(timeout_after_seconds) do
    now() + timeout_after_seconds
  end
end

Как это использовать

Подключите его в конце вашего :browser трубопровод в вашем приложении Феникс router.ex,

Обратите внимание, что аутентификация (получение user_id от сеанса, загрузки пользователя из БД) и авторизации - проблемы для других плагинов, далее в процессе разработки. Поэтому убедитесь, что он подключен до ваших подключений аутентификации и авторизации на основе сеанса.

pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_flash
  plug :put_secure_browser_headers
  plug Auth.SlidingSessionTimeout, timeout_after_seconds: 3600    # <=
end

Сначала я искал варианты истечения срока действия cookie в библиотеке Plug, затем понял, что более простой (и более безопасный) подход - просто установить дату окончания срока действия в сеансе вместе с user_id. Сеанс защищен от несанкционированного доступа, поэтому, когда я получаю каждый запрос, я могу сравнить дату и время; если сеанс еще не истек, я установил current_user как обычно. В противном случае я звоню logout! удалить просроченный сеанс.

Реализация будет выглядеть примерно так (требуется библиотека Timex):

# Assign current_user to the conn, if a user is logged in
def load_current_user(conn, _opts) do
  cond do
    no_login_session?(conn) ->
      conn # No user_id was found; make no changes
    current_user_already_set?(conn) ->
      conn
    session_expired?(conn) ->
      logout!(conn)
    user = load_user_from_session(conn) ->
      conn
        |> put_session(:expires_at, new_expiration_datetime_string)
        |> assign(:current_user, user)
  end
end

defp session_expired?(conn) do
  expires_at = get_session(conn, :expires_at) |> Timex.parse!("{ISO:Extended}")
  Timex.after?(Timex.now, expires_at)
end

# ... more ...

# Start a logged-in session for an (already authenticated) user
def login!(conn, user) do
  conn
    |> assign(:current_user, user)
    |> put_session(:user_id, user.id)
    |> put_session(:expires_at, new_expiration_datetime_string)
    |> configure_session(renew: true)
end

defp new_expiration_datetime_string do
  Timex.now |> Timex.shift(hours: +2) |> Timex.format("{ISO:Extended}")
end

# ... more ...
Другие вопросы по тегам