Enforce server-side session idle expiry with sliding TTL
This commit is contained in:
@@ -104,7 +104,7 @@ class SessionMiddleware:
|
||||
session_id = self._extract_session_id(scope)
|
||||
session: Session
|
||||
if session_id is not None:
|
||||
loaded = await self._store.load(session_id)
|
||||
loaded = await self._store.load(session_id, self._max_age)
|
||||
session = loaded if loaded is not None else Session()
|
||||
else:
|
||||
session = Session()
|
||||
@@ -175,4 +175,4 @@ class SessionMiddleware:
|
||||
session.set_id(self._store.new_session_id())
|
||||
|
||||
if session.id is not None:
|
||||
await self._store.save(session.id, session)
|
||||
await self._store.save(session.id, session, self._max_age)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from secrets import token_urlsafe
|
||||
from typing import Any, Optional
|
||||
from time import monotonic
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from ._session import Session
|
||||
|
||||
@@ -9,13 +10,22 @@ class SessionStore(ABC):
|
||||
"""Pluggable backend for session persistence."""
|
||||
|
||||
@abstractmethod
|
||||
async def load(self, session_id: str) -> Optional[Session]:
|
||||
"""Load an existing session, or return ``None`` if unknown/expired."""
|
||||
async def load(self, session_id: str, max_age: Optional[int] = None) -> Optional[Session]:
|
||||
"""Load an existing session, or return ``None`` if unknown or expired.
|
||||
|
||||
``max_age`` is the idle timeout in seconds. If provided, the store may
|
||||
use it to enforce a server-side expiry and to slide the expiry window
|
||||
on each access.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def save(self, session_id: str, session: Session) -> None:
|
||||
"""Persist the session data under ``session_id``."""
|
||||
async def save(self, session_id: str, session: Session, max_age: Optional[int] = None) -> None:
|
||||
"""Persist the session data under ``session_id``.
|
||||
|
||||
``max_age`` is the idle timeout in seconds. If provided, the store
|
||||
should record the expiry time as ``now + max_age``.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@@ -36,19 +46,40 @@ class InMemorySessionStore(SessionStore):
|
||||
|
||||
Suitable for development and single-process deployments. Session data is
|
||||
lost when the process exits and is not shared between processes.
|
||||
|
||||
Sessions can optionally expire server-side after ``max_age`` seconds of
|
||||
inactivity. Active sessions slide the expiry window on each access.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._sessions: dict[str, dict[str, Any]] = {}
|
||||
def __init__(self, clock: Callable[[], float] = monotonic) -> None:
|
||||
self._clock = clock
|
||||
self._data: dict[str, dict[str, Any]] = {}
|
||||
self._expires: dict[str, float] = {}
|
||||
|
||||
async def load(self, session_id: str) -> Optional[Session]:
|
||||
data = self._sessions.get(session_id)
|
||||
async def load(self, session_id: str, max_age: Optional[int] = None) -> Optional[Session]:
|
||||
data = self._data.get(session_id)
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
expires = self._expires.get(session_id)
|
||||
now = self._clock()
|
||||
if expires is not None and now > expires:
|
||||
self._data.pop(session_id, None)
|
||||
self._expires.pop(session_id, None)
|
||||
return None
|
||||
|
||||
if max_age is not None and expires is not None:
|
||||
self._expires[session_id] = now + max_age
|
||||
|
||||
return Session(session_id, data)
|
||||
|
||||
async def save(self, session_id: str, session: Session) -> None:
|
||||
self._sessions[session_id] = dict(session)
|
||||
async def save(self, session_id: str, session: Session, max_age: Optional[int] = None) -> None:
|
||||
self._data[session_id] = dict(session)
|
||||
if max_age is not None:
|
||||
self._expires[session_id] = self._clock() + max_age
|
||||
else:
|
||||
self._expires.pop(session_id, None)
|
||||
|
||||
async def delete(self, session_id: str) -> None:
|
||||
self._sessions.pop(session_id, None)
|
||||
self._data.pop(session_id, None)
|
||||
self._expires.pop(session_id, None)
|
||||
|
||||
Reference in New Issue
Block a user