施工(HEL-226): 实现登录门户与本机免密切换账号
用设备 Cookie 和授权表记住本机已验证账号,登录页按确认样图做成门户,不再把密码写进浏览器。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
8a5e78f022
commit
f0a1adf52f
+26
-10
@@ -9,7 +9,13 @@ from http.cookies import SimpleCookie
|
||||
from typing import Any
|
||||
from urllib.parse import unquote
|
||||
|
||||
from backend.bootstrap.config import SESSION_COOKIE, SESSION_MAX_AGE, STATIC_DIR
|
||||
from backend.bootstrap.config import (
|
||||
DEVICE_COOKIE,
|
||||
DEVICE_MAX_AGE,
|
||||
SESSION_COOKIE,
|
||||
SESSION_MAX_AGE,
|
||||
STATIC_DIR,
|
||||
)
|
||||
from backend.features.accounts.security import token_hash
|
||||
from backend.http.context import correlation_id
|
||||
from backend.http.errors import normalize_error_payload
|
||||
@@ -21,15 +27,21 @@ class HttpTransportMixin:
|
||||
application_service: Any
|
||||
route_registry: Any
|
||||
|
||||
def session_token(self) -> str:
|
||||
def cookie_value(self, name: str) -> str:
|
||||
cookie = SimpleCookie()
|
||||
try:
|
||||
cookie.load(self.headers.get("Cookie", ""))
|
||||
except Exception:
|
||||
return ""
|
||||
morsel = cookie.get(SESSION_COOKIE)
|
||||
morsel = cookie.get(name)
|
||||
return morsel.value if morsel else ""
|
||||
|
||||
def session_token(self) -> str:
|
||||
return self.cookie_value(SESSION_COOKIE)
|
||||
|
||||
def device_token(self) -> str:
|
||||
return self.cookie_value(DEVICE_COOKIE)
|
||||
|
||||
def require_auth(self, send_error: bool = True) -> bool:
|
||||
raw_token = self.session_token()
|
||||
service = self.application_service
|
||||
@@ -79,15 +91,18 @@ class HttpTransportMixin:
|
||||
return self.require_member()
|
||||
return True
|
||||
|
||||
def session_cookie(self, value: str, clear: bool = False) -> str:
|
||||
max_age = 0 if clear else SESSION_MAX_AGE
|
||||
cookie = (
|
||||
f"{SESSION_COOKIE}={value}; Path=/; HttpOnly; SameSite=Lax; Max-Age={max_age}"
|
||||
)
|
||||
def _cookie_header(self, name: str, value: str, max_age: int) -> str:
|
||||
cookie = f"{name}={value}; Path=/; HttpOnly; SameSite=Lax; Max-Age={max_age}"
|
||||
if self.headers.get("X-Forwarded-Proto", "").lower() == "https":
|
||||
cookie += "; Secure"
|
||||
return cookie
|
||||
|
||||
def session_cookie(self, value: str, clear: bool = False) -> str:
|
||||
return self._cookie_header(SESSION_COOKIE, value, 0 if clear else SESSION_MAX_AGE)
|
||||
|
||||
def device_cookie(self, value: str, clear: bool = False) -> str:
|
||||
return self._cookie_header(DEVICE_COOKIE, value, 0 if clear else DEVICE_MAX_AGE)
|
||||
|
||||
def read_json_body(self, allow_empty: bool = False) -> dict[str, Any]:
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
if length == 0 and allow_empty:
|
||||
@@ -132,7 +147,7 @@ class HttpTransportMixin:
|
||||
self,
|
||||
payload: dict[str, Any],
|
||||
status: HTTPStatus = HTTPStatus.OK,
|
||||
headers: dict[str, str] | None = None,
|
||||
headers: dict[str, str] | list[tuple[str, str]] | tuple[tuple[str, str], ...] | None = None,
|
||||
) -> None:
|
||||
request_id = getattr(self, "_correlation_id", "")
|
||||
if not request_id:
|
||||
@@ -145,7 +160,8 @@ class HttpTransportMixin:
|
||||
self.send_header("Content-Length", str(len(content)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("X-Request-ID", request_id)
|
||||
for name, value in (headers or {}).items():
|
||||
header_items = headers.items() if isinstance(headers, dict) else (headers or ())
|
||||
for name, value in header_items:
|
||||
self.send_header(name, value)
|
||||
self.end_headers()
|
||||
self.wfile.write(content)
|
||||
|
||||
Reference in New Issue
Block a user