Эликсир / Феникс: как реализовать тайм-аут / истечение времени сессии
Я работаю над ванильным приложением 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 ...