diff --git a/docs/JWT_CLAIMS.md b/docs/JWT_CLAIMS.md index 716cb05..e6a7f23 100644 --- a/docs/JWT_CLAIMS.md +++ b/docs/JWT_CLAIMS.md @@ -104,7 +104,7 @@ A valid JWT signature is necessary but not sufficient: after decoding, `SessionM - Reduces the impact of a leaked JWT 3. **Flexible session management** - - Supports sliding expiration: when a session is renewed, a new token with an updated `exp` is returned in the response header named by `header_name` (`X-Session-Token` by default) + - Supports sliding expiration: when a session is renewed, the middleware sends a new token with an updated `exp` through the transport the request used: the response header named by `header_name` (`X-Session-Token` by default), or `Set-Cookie` for a cookie - Supports updating session data in real time - Supports features such as flash messages diff --git a/docs/SESSION.md b/docs/SESSION.md index 3080d24..3acf510 100644 --- a/docs/SESSION.md +++ b/docs/SESSION.md @@ -8,8 +8,8 @@ the cache backend; the client only holds a single signed token. | Middleware | Token source | Response side | Status | |------------|--------------|---------------|--------| -| `FastAPICacheXSessionMiddleware` | Custom header (default `X-Session-Token`) / `Authorization: Bearer` / **cookie** (default name `session`) | Routed by source: a token that arrived in a header is returned in the response header; a token that arrived in a cookie (or a brand-new session) gets `Set-Cookie` | **Recommended** | -| `SessionMiddleware` | Custom header / `Authorization: Bearer`; **no cookie support** | A renewed token is sent back in the response header | Deprecated, **removed in 0.4.0** | +| `FastAPICacheXSessionMiddleware` | Custom header (default `X-Session-Token`) / `Authorization: Bearer` / **cookie** (default name `session`) | Routed by source: a request that sent a header or bearer token (even one that no longer resolves) gets its token in the response header; otherwise (a cookie, or no token at all) it gets `Set-Cookie` | **Recommended** | +| `SessionMiddleware` | Custom header / `Authorization: Bearer`; **no cookie support** | A renewed token, or one for a regenerated ID, is sent back in the response header | Deprecated, **removed in 0.4.0** | **Use `FastAPICacheXSessionMiddleware` for all new projects.** It covers every transport of `SessionMiddleware` (it reads `X-Session-Token` and `Authorization: Bearer` in the same way) and @@ -150,9 +150,10 @@ constructed) and will be removed in 0.4.0. Use `FastAPICacheXSessionMiddleware` second": it reads the custom header (default `X-Session-Token`) and/or `Authorization: Bearer` first and only falls back to the cookie when neither is present, so clients that used `X-Session-Token` with `SessionMiddleware` keep working unchanged. The response side is routed - by source too: for a token that arrived in a header, a renewed token is sent back in the same - response header and no `Set-Cookie` is emitted; a token that arrived in a cookie (or a brand-new - anonymous session) uses `Set-Cookie`. + by source too: when the request sent a header or bearer token (even one that no longer + resolves), a new or renewed token is sent back in the `header_name` response header and no + `Set-Cookie` is emitted; a token that arrived in a cookie (or a brand-new anonymous session for a + request without a token) uses `Set-Cookie`. Both middlewares put the loaded `Session` object into `request.state`, so the existing session dependencies `get_session`, `get_optional_session`, `require_session` and `require_user_session` @@ -212,7 +213,7 @@ async def me(session=Depends(require_user_session)): [Authenticated endpoints](HTTP_CACHING.md#authenticated-endpoints). The cookie is always `HttpOnly`; `Secure`, `SameSite`, `Domain`, `Path` and `Max-Age` follow the -`cookie_*` settings. +`cookie_*` settings (`cookie_max_age=None`, or `0`, omits `Max-Age`). ## Configuration @@ -263,7 +264,7 @@ SessionConfig( 0.4.0 names the session cookie `__Host-session` and sets the `Secure` flag by default. Browsers accept a `__Host-` cookie only when it is `Secure`, has `Path=/` and no `Domain`, and never from a subdomain, which removes the usual way to plant a session cookie (session fixation). The new name also means every browser holding a `session` cookie is logged out once after the upgrade. -Until then, `FastAPICacheXSessionMiddleware` emits a `FutureWarning` when its config leaves `cookie_name` or `cookie_https_only` at the default. Set both to silence it: +Until then, `FastAPICacheXSessionMiddleware` emits a `FutureWarning` when it is constructed (when the app builds its middleware stack, at startup or on the first request) and its config leaves `cookie_name` or `cookie_https_only` at the default. Set both to silence it: - `cookie_name="session", cookie_https_only=False` keeps the current cookie (and keeps working after the upgrade, e.g. for local development over plain HTTP); - `cookie_name="__Host-session", cookie_https_only=True` switches now, over HTTPS. @@ -327,8 +328,8 @@ only supports `HS256`, `HS384` and `HS512`: with an asymmetric algorithm, `Sessi An HMAC key must be at least as long as the hash output (RFC 7518 §3.2): 32 bytes for `HS256`, 48 for `HS384` and 64 for `HS512`, counted after UTF-8 encoding. `secret_key` only has to be 32 characters, so with `HS384` or `HS512` a shorter key makes the serializer emit a `UserWarning` -once when it is built (PyJWT itself also warns with `InsecureKeyLengthWarning` whenever it signs -or verifies a token). Use a longer key, for example `secrets.token_urlsafe(64)`. +once when it is built (PyJWT 2.11 and later also warn with `InsecureKeyLengthWarning` whenever they +sign or verify a token). Use a longer key, for example `secrets.token_urlsafe(64)`. Security notes: @@ -470,9 +471,9 @@ from fastapi import Request from fastapi_cachex.session import SessionUser, login -# LoginRequest is the body model from Basic Usage +# Credentials is the body model from Basic Usage @app.post("/login") -async def log_in(credentials: LoginRequest, request: Request): +async def log_in(credentials: Credentials, request: Request): ... # verify credentials.password await login(request, SessionUser(user_id=credentials.username)) return {"ok": True} @@ -561,7 +562,8 @@ lifecycle: `create_session()` / `create_anonymous_session()` return `(session, token)`; `get_session()` returns `(session, renewed_token)`, where `renewed_token` is set only when sliding expiration renewed the token and should be sent back to the client. `get_session()` raises a `SessionError` subclass on -failure: `SessionTokenError` (malformed token), `SessionSecurityError` (bad +failure: `SessionTokenError` (malformed token; for a JWT also a bad signature, +an expired `exp` or a wrong `iss`/`aud`), `SessionSecurityError` (bad `simple` signature or binding mismatch), `SessionNotFoundError`, `SessionInvalidError` (session not active) or `SessionExpiredError` (TTL or absolute timeout exceeded). Since 0.3.8, `SessionError` derives from `CacheXError`, so `except CacheXError` @@ -614,9 +616,9 @@ from fastapi_cachex.session import SessionUser from fastapi_cachex.session.dependencies import SessionManagerDep -# LoginRequest is the body model from Basic Usage +# Credentials is the body model from Basic Usage @app.post("/login") -async def login(credentials: LoginRequest, manager: SessionManagerDep): +async def login(credentials: Credentials, manager: SessionManagerDep): ... # verify credentials.password user = SessionUser(user_id=credentials.username) session, token = await manager.create_session(user=user) diff --git a/fastapi_cachex/session/dependencies.py b/fastapi_cachex/session/dependencies.py index ecbaaac..1b18581 100644 --- a/fastapi_cachex/session/dependencies.py +++ b/fastapi_cachex/session/dependencies.py @@ -148,8 +148,9 @@ async def login( Warns: FutureWarning: Once per app, if the manager the middleware registered - is not the one set with ``SessionManagerProxy.set()``. 0.4.0 - resolves this dependency through ``SessionManagerProxy`` only. + is not the one set with ``SessionManagerProxy.set()`` (or none is + set there). 0.4.0 resolves this dependency through + ``SessionManagerProxy`` only. """ state = request.app.state manager: SessionManager | None = getattr( diff --git a/fastapi_cachex/session/manager.py b/fastapi_cachex/session/manager.py index 5576652..891b8a2 100644 --- a/fastapi_cachex/session/manager.py +++ b/fastapi_cachex/session/manager.py @@ -51,6 +51,12 @@ def __init__( config: Session configuration token_serializer: Optional custom token serializer. If provided, overrides the built-in selection (simple/jwt). + + Raises: + ValueError: If ``config.token_format`` is ``"jwt"`` with an + asymmetric ``jwt_algorithm`` and no ``token_serializer``. + ImportError: If ``config.token_format`` is ``"jwt"``, no + ``token_serializer`` is given and PyJWT is not installed. """ self.backend = backend self.config = config @@ -123,7 +129,16 @@ async def create_anonymous_session( user_agent: str | None = None, **extra_data: object, ) -> tuple[Session, str]: - """Create a new session without user information.""" + """Create a new session without user information. + + Args: + ip_address: Client IP address (if IP binding enabled) + user_agent: Client User-Agent (if UA binding enabled) + **extra_data: Additional session data + + Returns: + Tuple of (Session, token_string) + """ return await self._create_session( user=None, ip_address=ip_address, @@ -209,11 +224,14 @@ async def get_session( propagate it to the client (e.g. via a response header). Raises: - SessionTokenError: If token is invalid + SessionTokenError: If the token cannot be parsed (for a JWT, also + a bad signature, an expired ``exp`` or a wrong ``iss``/``aud``) SessionNotFoundError: If session not found - SessionExpiredError: If session has expired + SessionExpiredError: If the session is past its ``expires_at`` or + ``absolute_timeout``; it is saved as ``EXPIRED`` first SessionInvalidError: If session is not active - SessionSecurityError: If security checks fail + SessionSecurityError: If a ``simple`` token's signature is wrong, + or an IP / User-Agent binding does not match """ # Parse and verify token try: diff --git a/fastapi_cachex/session/middleware.py b/fastapi_cachex/session/middleware.py index 31979cf..5ba2de2 100644 --- a/fastapi_cachex/session/middleware.py +++ b/fastapi_cachex/session/middleware.py @@ -231,8 +231,17 @@ def __init__( Args: app: ASGI application - session_manager: Session manager instance - config: Session configuration + session_manager: Session manager instance; defaults to the one + set in ``SessionManagerProxy`` + config: Session configuration; defaults to + ``session_manager.config`` + + Raises: + ProxyNotSetError: If ``session_manager`` is omitted and + ``SessionManagerProxy`` holds none. + + Warns: + DeprecationWarning: Always; use ``FastAPICacheXSessionMiddleware``. """ warnings.warn( "SessionMiddleware is deprecated, use FastAPICacheXSessionMiddleware. " @@ -365,7 +374,10 @@ class FastAPICacheXSessionMiddleware: session payload is persisted via the configured ``SessionManager``/cache backend instead of being encoded into the cookie itself. Only a signed session token is stored client-side, in the cookie named by - ``SessionConfig.cookie_name``. + ``SessionConfig.cookie_name``. A token in the custom header or an + ``Authorization: Bearer`` header is read before the cookie, and a request + that sent one (even one that no longer resolves) gets its token back in + the ``header_name`` response header instead of ``Set-Cookie``. """ def __init__( @@ -378,8 +390,14 @@ def __init__( Args: app: ASGI application - session_manager: Session manager instance - config: Session configuration + session_manager: Session manager instance; defaults to the one + set in ``SessionManagerProxy`` + config: Session configuration; defaults to + ``session_manager.config`` + + Raises: + ProxyNotSetError: If ``session_manager`` is omitted and + ``SessionManagerProxy`` holds none. Warns: FutureWarning: If the configuration leaves ``cookie_name`` or @@ -612,12 +630,15 @@ async def _write_session( loaded_token: str | None, renewed_token: str | None, ) -> tuple[str, str | None]: - """Create-or-update the backend session for a modified, non-empty dict. + """Create-or-update the backend session for a modified dict. + + The dict is non-empty, or was emptied on a session that has a user. Args: session: The Starlette session dict for this request connection: Incoming HTTP connection (for IP / User-Agent binding) - backend_session: Loaded backend session, or None for a new session + backend_session: The session the dict belongs to (loaded, or + started by ``login()``), or None to create an anonymous one loaded_token: Token for the loaded session (None when creating anew) renewed_token: Sliding-expiration renewed token, if any diff --git a/fastapi_cachex/session/token_serializers.py b/fastapi_cachex/session/token_serializers.py index 6d58e73..ff29f72 100644 --- a/fastapi_cachex/session/token_serializers.py +++ b/fastapi_cachex/session/token_serializers.py @@ -43,7 +43,11 @@ def to_string(self, token: SessionToken) -> str: # pragma: no cover - Protocol def from_string( self, token_str: str ) -> SessionToken: # pragma: no cover - Protocol body - """Parse a string into a `SessionToken` (with necessary verification).""" + """Parse a string into a `SessionToken` (with necessary verification). + + Raise ``ValueError`` for an invalid token; ``SessionManager`` turns it + into ``SessionTokenError``. + """ class SimpleTokenSerializer: @@ -99,9 +103,9 @@ def _warn_if_key_too_short(secret: str, algorithm: str) -> None: """Warn once when ``secret`` is shorter than ``algorithm``'s hash output. ``SessionConfig`` only requires 32 characters, enough for HS256 but not - for HS384 (48 bytes) or HS512 (64 bytes). PyJWT warns about a short key on - every token it signs or verifies; this names the setting to fix, once, - when the serializer is built. + for HS384 (48 bytes) or HS512 (64 bytes). PyJWT 2.11 and later warn about a + short key on every token they sign or verify; this names the setting to + fix, once, when the serializer is built. """ min_bytes = _HMAC_MIN_KEY_BYTES[algorithm] key_bytes = len(secret.encode("utf-8")) @@ -123,7 +127,9 @@ class JWTTokenSerializer: Encodes the session reference into a signed JWT with claims: - sid: session id (custom claim) - iat: issued at (epoch seconds) - - exp: expiry (epoch seconds), derived from config.session_ttl + - exp: expiry (epoch seconds), the session's ``expires_at`` (so it follows + sliding renewal and the ``absolute_timeout`` cap), or iat + + config.session_ttl when the session has none Optionally: - iss: issuer (if configured) - aud: audience (if configured) @@ -143,6 +149,12 @@ def __init__(self, config: SessionConfig, jwt_module: Any | None = None) -> None which only the HMAC algorithms can use; an asymmetric algorithm needs a custom ``token_serializer`` that holds the key pair. + ImportError: If no ``jwt_module`` is given and PyJWT (the ``jwt`` + extra) is not installed. + + Warns: + UserWarning: If ``secret_key`` is shorter in UTF-8 bytes than the + HMAC hash output (48 for HS384, 64 for HS512). """ if config.jwt_algorithm not in JWT_HMAC_ALGORITHMS: supported = ", ".join(sorted(JWT_HMAC_ALGORITHMS)) @@ -205,6 +217,10 @@ def from_string(self, token_str: str) -> SessionToken: """Decode and verify a JWT string into a `SessionToken`. Verifies signature, `exp`, and `iat`, and optional `iss`/`aud`. + + Raises: + ValueError: If the token fails decoding or verification, or its + payload is malformed. """ options = { "require": ["sid", "iat", "exp"], diff --git a/i18n/zh-TW/docs/JWT_CLAIMS.md b/i18n/zh-TW/docs/JWT_CLAIMS.md index 069e77a..b5ec0da 100644 --- a/i18n/zh-TW/docs/JWT_CLAIMS.md +++ b/i18n/zh-TW/docs/JWT_CLAIMS.md @@ -104,7 +104,7 @@ FastAPI-CacheX 採用**有狀態 Session** 模型,與純粹無狀態的 JWT - 降低 JWT 外洩的影響 3. **彈性的 Session 管理** - - 支援滑動過期:Session 續期時,帶有更新後 `exp` 的新權杖會放在由 `header_name` 指定的回應標頭中回傳(預設為 `X-Session-Token`) + - 支援滑動過期:Session 續期時,中介軟體會透過請求使用的傳輸方式送出帶有更新後 `exp` 的新權杖:由 `header_name` 指定的回應標頭(預設為 `X-Session-Token`),或對 Cookie 使用 `Set-Cookie` - 支援即時更新 Session 資料 - 支援 flash 訊息等功能 diff --git a/i18n/zh-TW/docs/SESSION.md b/i18n/zh-TW/docs/SESSION.md index b6ed113..30e940a 100644 --- a/i18n/zh-TW/docs/SESSION.md +++ b/i18n/zh-TW/docs/SESSION.md @@ -6,8 +6,8 @@ FastAPI-CacheX 的 Session 管理提供完整的使用者 Session 處理,包 | 中介軟體 | 權杖來源 | 回應端 | 狀態 | |------------|--------------|---------------|--------| -| `FastAPICacheXSessionMiddleware` | 自訂標頭(預設 `X-Session-Token`)/`Authorization: Bearer`/**Cookie**(預設名稱 `session`) | 依來源決定:從標頭傳入的權杖會在回應標頭中傳回;從 Cookie 傳入的權杖(或全新的 Session)則使用 `Set-Cookie` | **建議使用** | -| `SessionMiddleware` | 自訂標頭/`Authorization: Bearer`;**不支援 Cookie** | 更新後的權杖會在回應標頭中傳回 | 已棄用,**將於 0.4.0 移除** | +| `FastAPICacheXSessionMiddleware` | 自訂標頭(預設 `X-Session-Token`)/`Authorization: Bearer`/**Cookie**(預設名稱 `session`) | 依來源決定:送出標頭或 Bearer 權杖的請求(即使該權杖已無法解析)會在回應標頭中收到權杖;其他情況(Cookie,或完全沒有權杖)則使用 `Set-Cookie` | **建議使用** | +| `SessionMiddleware` | 自訂標頭/`Authorization: Bearer`;**不支援 Cookie** | 更新後的權杖,或重新產生 ID 後的權杖,會在回應標頭中傳回 | 已棄用,**將於 0.4.0 移除** | **所有新專案請使用 `FastAPICacheXSessionMiddleware`。** 它涵蓋 `SessionMiddleware` 的所有傳輸方式(以相同方式讀取 `X-Session-Token` 與 `Authorization: Bearer`),並加入 Cookie 支援。自 0.3.1 起,`SessionMiddleware` 在建構時會發出 `DeprecationWarning`,並將於 **0.4.0 移除**。兩個中介軟體提供給相同的 Session 依賴項(`get_session`、`get_optional_session`、`require_session`),因此遷移通常只需要修改 `add_middleware` 那一行;以標頭傳送權杖的既有用戶端不需要任何修改。 @@ -93,7 +93,7 @@ app.add_middleware(FastAPICacheXSessionMiddleware) # 從 proxy 取得 `SessionMiddleware` 自 0.3.1 起已棄用(建構時會發出 `DeprecationWarning`),並將於 0.4.0 移除。請改用 `FastAPICacheXSessionMiddleware`: - **`SessionMiddleware`**(一個 `BaseHTTPMiddleware`):以自訂標頭(預設 `X-Session-Token`)和/或 `Authorization: Bearer` 傳遞權杖,適合由用戶端管理權杖的 API 優先架構。不支援以 Cookie 傳輸。 -- **`FastAPICacheXSessionMiddleware`**(一個純 ASGI 中介軟體):與 Starlette 內建的 `SessionMiddleware` 相容,提供相同的類 dict `request.session`。它以 Cookie(預設 Cookie 名稱 `session`)傳遞簽署過的 Session 權杖,而 Session 內容則存放在後端(`SessionManager` 的快取後端),而不是像 Starlette 自己的實作那樣編碼進 Cookie 本身。權杖解析採「標頭優先、Cookie 其次」:它會先讀取自訂標頭(預設 `X-Session-Token`)和/或 `Authorization: Bearer`,只有兩者都不存在時才退回使用 Cookie,因此原本搭配 `SessionMiddleware` 使用 `X-Session-Token` 的用戶端不需修改即可繼續運作。回應端同樣依來源決定:從標頭傳入的權杖,更新後的權杖會在同一個回應標頭中傳回,且不會發出 `Set-Cookie`;從 Cookie 傳入的權杖(或全新的匿名 Session)則使用 `Set-Cookie`。 +- **`FastAPICacheXSessionMiddleware`**(一個純 ASGI 中介軟體):與 Starlette 內建的 `SessionMiddleware` 相容,提供相同的類 dict `request.session`。它以 Cookie(預設 Cookie 名稱 `session`)傳遞簽署過的 Session 權杖,而 Session 內容則存放在後端(`SessionManager` 的快取後端),而不是像 Starlette 自己的實作那樣編碼進 Cookie 本身。權杖解析採「標頭優先、Cookie 其次」:它會先讀取自訂標頭(預設 `X-Session-Token`)和/或 `Authorization: Bearer`,只有兩者都不存在時才退回使用 Cookie,因此原本搭配 `SessionMiddleware` 使用 `X-Session-Token` 的用戶端不需修改即可繼續運作。回應端同樣依來源決定:請求送出標頭或 Bearer 權杖時(即使該權杖已無法解析),新的或更新後的權杖會在 `header_name` 回應標頭中傳回,且不會發出 `Set-Cookie`;從 Cookie 傳入的權杖(或沒有權杖的請求所建立的全新匿名 Session)則使用 `Set-Cookie`。 兩個中介軟體都會將載入的 `Session` 物件放進 `request.state`,因此既有的 Session 依賴項 `get_session`、`get_optional_session`、`require_session` 與 `require_user_session` 在任一個中介軟體下都能直接運作,不需任何修改: @@ -124,7 +124,7 @@ async def me(session=Depends(require_user_session)): - 帶有 Session 權杖的回應(新建立的 Session、滑動續期、重新產生的 ID),或帶有讓 Session Cookie 失效之 `Set-Cookie` 的回應,一律不可快取。中介軟體會設定 `Cache-Control: private, no-store`,取代路由原本設定的值(包括 `@cache(public=True)` 的路由),並且即使處理函式沒有碰過 `request.session`,也會加入與上一項相同的 `Vary` 名稱。否則 CDN 或反向 proxy 可能存下權杖,再交給下一位訪客。不帶權杖的回應則維持原本的標頭。已棄用的 `SessionMiddleware` 在回應標頭送出權杖時也會這麼做。 - 對帶有 Session 的請求(中介軟體從任何來源載入的 Session,有沒有使用者都算,或不是空的 `request.session`),`@cache` 不會讀寫後端,並像 `Authorization` 一樣以 `private` 回應。`public=True` 讓路由在各 Session 間共用;`cache_authorized=True` 搭配包含 Session 使用者的 `key_builder` 則依使用者快取。見[需驗證身分的端點](HTTP_CACHING.md#authenticated-endpoints)。 -Cookie 一律為 `HttpOnly`;`Secure`、`SameSite`、`Domain`、`Path` 與 `Max-Age` 則依 `cookie_*` 設定。 +Cookie 一律為 `HttpOnly`;`Secure`、`SameSite`、`Domain`、`Path` 與 `Max-Age` 則依 `cookie_*` 設定(`cookie_max_age=None` 或 `0` 時不設 `Max-Age`)。 ## 設定 {#configuration} @@ -173,7 +173,7 @@ SessionConfig( 0.4.0 會將 Session Cookie 命名為 `__Host-session`,並預設加上 `Secure` 旗標。瀏覽器只接受帶 `Secure`、`Path=/` 且沒有 `Domain` 的 `__Host-` Cookie,也不接受子網域設定的這種 Cookie,因此移除了植入 Session Cookie 最常見的途徑(Session 固定攻擊(session fixation))。新名稱也代表升級後,所有持有 `session` Cookie 的瀏覽器都會被登出一次。 -在那之前,若 `FastAPICacheXSessionMiddleware` 的設定讓 `cookie_name` 或 `cookie_https_only` 維持預設值,會發出 `FutureWarning`。明確設定兩者即可消除警告: +在那之前,`FastAPICacheXSessionMiddleware` 在建構時(應用程式建立中介軟體堆疊時,也就是啟動時或第一個請求時),若設定讓 `cookie_name` 或 `cookie_https_only` 維持預設值,會發出 `FutureWarning`。明確設定兩者即可消除警告: - `cookie_name="session", cookie_https_only=False` 保留目前的 Cookie(升級後也能繼續運作,例如透過純 HTTP 進行本機開發); - `cookie_name="__Host-session", cookie_https_only=True` 現在就切換,需透過 HTTPS。 @@ -211,7 +211,7 @@ config = SessionConfig( `jwt_algorithm` 必須是 `HS256`、`HS384`、`HS512`、`RS256`、`RS384`、`RS512`、`ES256`、`ES384`、`ES512`、`PS256`、`PS384`、`PS512` 或 `EdDSA` 其中之一;其他任何值(包括 `none`)都會拋出 `ValidationError`。內建的序列化器以同一把 `secret_key` 簽署與驗證,因此只支援 `HS256`、`HS384` 與 `HS512`:使用非對稱演算法時,除非你傳入持有金鑰對的自訂 `token_serializer`,否則 `SessionManager` 會拋出 `ValueError`。 -HMAC 金鑰的長度至少須等於雜湊輸出(RFC 7518 §3.2):`HS256` 為 32 位元組、`HS384` 為 48、`HS512` 為 64,以 UTF-8 編碼後計算。`secret_key` 只要求 32 個字元,因此搭配 `HS384` 或 `HS512` 時,較短的金鑰會讓序列化器在建立時發出一次 `UserWarning`(PyJWT 本身每次簽署或驗證權杖時也會發出 `InsecureKeyLengthWarning`)。請使用更長的金鑰,例如 `secrets.token_urlsafe(64)`。 +HMAC 金鑰的長度至少須等於雜湊輸出(RFC 7518 §3.2):`HS256` 為 32 位元組、`HS384` 為 48、`HS512` 為 64,以 UTF-8 編碼後計算。`secret_key` 只要求 32 個字元,因此搭配 `HS384` 或 `HS512` 時,較短的金鑰會讓序列化器在建立時發出一次 `UserWarning`(PyJWT 2.11 以上版本每次簽署或驗證權杖時也會發出 `InsecureKeyLengthWarning`)。請使用更長的金鑰,例如 `secrets.token_urlsafe(64)`。 安全性注意事項: @@ -323,9 +323,9 @@ from fastapi import Request from fastapi_cachex.session import SessionUser, login -# LoginRequest 是基本用法中的請求本文模型 +# Credentials 是基本用法中的請求本文模型 @app.post("/login") -async def log_in(credentials: LoginRequest, request: Request): +async def log_in(credentials: Credentials, request: Request): ... # 驗證 credentials.password await login(request, SessionUser(user_id=credentials.username)) return {"ok": True} @@ -376,7 +376,7 @@ session, new_token = await manager.regenerate_session_id(session) ## SessionManager 概覽 {#sessionmanager-at-a-glance} -`SessionManager(backend, config, token_serializer=None)` 處理整個生命週期:`create_session()`/`create_anonymous_session()` 回傳 `(session, token)`;`get_session()` 回傳 `(session, renewed_token)`,其中 `renewed_token` 只有在滑動過期更新了權杖時才會有值,並應傳回給用戶端。`get_session()` 失敗時會拋出 `SessionError` 的子類別:`SessionTokenError`(權杖格式錯誤)、`SessionSecurityError`(簽章錯誤或綁定不符)、`SessionNotFoundError`、`SessionInvalidError`(Session 不是啟用狀態)或 `SessionExpiredError`(超過 TTL 或絕對逾時)。從 0.3.8 起,`SessionError` 繼承自 `CacheXError`,因此 `except CacheXError` 也會捕捉 Session 錯誤。 +`SessionManager(backend, config, token_serializer=None)` 處理整個生命週期:`create_session()`/`create_anonymous_session()` 回傳 `(session, token)`;`get_session()` 回傳 `(session, renewed_token)`,其中 `renewed_token` 只有在滑動過期更新了權杖時才會有值,並應傳回給用戶端。`get_session()` 失敗時會拋出 `SessionError` 的子類別:`SessionTokenError`(權杖格式錯誤;JWT 還包括簽章錯誤、`exp` 已過期或 `iss`/`aud` 不符)、`SessionSecurityError`(`simple` 權杖簽章錯誤或綁定不符)、`SessionNotFoundError`、`SessionInvalidError`(Session 不是啟用狀態)或 `SessionExpiredError`(超過 TTL 或絕對逾時)。從 0.3.8 起,`SessionError` 繼承自 `CacheXError`,因此 `except CacheXError` 也會捕捉 Session 錯誤。 `get_session()` 只有在滑動過期更新了 Session 時才寫入後端,因此只讀取 Session 的請求只需一次後端讀取。回傳的 Session 中 `last_accessed` 是目前時間,但儲存的值只會在 Session 下一次被寫入(建立、修改、更新或重新產生)時更新。傳入 `touch=True` 可在每次查詢時都儲存它。0.3.8 之前,每次查詢都會儲存 Session。 @@ -413,9 +413,9 @@ from fastapi_cachex.session import SessionUser from fastapi_cachex.session.dependencies import SessionManagerDep -# LoginRequest 是基本用法中的請求本文模型 +# Credentials 是基本用法中的請求本文模型 @app.post("/login") -async def login(credentials: LoginRequest, manager: SessionManagerDep): +async def login(credentials: Credentials, manager: SessionManagerDep): ... # 驗證 credentials.password user = SessionUser(user_id=credentials.username) session, token = await manager.create_session(user=user)