Files
xsk-education-management/app/auth.py
T
2026-06-15 12:38:25 +08:00

140 lines
4.0 KiB
Python

from __future__ import annotations
import hashlib
import hmac
from fastapi import Depends, Header, HTTPException, Request, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from .config import (
ACCOUNTS_AUTH_PASSWORD,
ACCOUNTS_SESSION_COOKIE,
ADMIN_AUTH_PASSWORD,
ADMIN_SESSION_COOKIE,
BASIC_AUTH_PASSWORD,
INGEST_AUTH_TOKEN,
RECORDS_SESSION_COOKIE,
)
security = HTTPBasic(auto_error=False)
def configured_password(label: str, password: str) -> str:
if not password:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"服务未配置{label}访问密码",
)
return password
def session_token(password: str, purpose: bytes) -> str:
return hmac.new(
password.encode("utf-8"),
purpose,
hashlib.sha256,
).hexdigest()
def has_valid_session(request: Request, cookie_name: str, password: str, purpose: bytes) -> bool:
token = request.cookies.get(cookie_name, "")
return bool(password and token) and hmac.compare_digest(token, session_token(password, purpose))
def has_valid_basic_auth(credentials: HTTPBasicCredentials | None, password: str) -> bool:
if credentials is None:
return False
return bool(password) and hmac.compare_digest(credentials.password, password)
def is_records_authenticated(
request: Request,
credentials: HTTPBasicCredentials | None = None,
) -> bool:
return has_valid_session(
request,
RECORDS_SESSION_COOKIE,
BASIC_AUTH_PASSWORD,
b"xsk-education-management-records-session-v1",
) or has_valid_basic_auth(credentials, BASIC_AUTH_PASSWORD)
def is_admin_authenticated(
request: Request,
credentials: HTTPBasicCredentials | None = None,
) -> bool:
return has_valid_session(
request,
ADMIN_SESSION_COOKIE,
ADMIN_AUTH_PASSWORD,
b"xsk-admin-web-session-v1",
) or has_valid_session(
request,
ACCOUNTS_SESSION_COOKIE,
ADMIN_AUTH_PASSWORD,
b"xsk-accounts-web-session-v1",
) or has_valid_basic_auth(credentials, ADMIN_AUTH_PASSWORD)
def is_accounts_authenticated(
request: Request,
credentials: HTTPBasicCredentials | None = None,
) -> bool:
return is_admin_authenticated(request, credentials)
def verify_records_auth(
request: Request,
credentials: HTTPBasicCredentials | None = Depends(security),
) -> str:
configured_password("课程记录", BASIC_AUTH_PASSWORD)
if not is_records_authenticated(request, credentials):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="请先登录",
)
return "records"
def verify_admin_auth(
request: Request,
credentials: HTTPBasicCredentials | None = Depends(security),
) -> str:
configured_password("管理后台", ADMIN_AUTH_PASSWORD)
if not is_admin_authenticated(request, credentials):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="请先登录管理后台",
)
return "admin"
def verify_accounts_auth(
request: Request,
credentials: HTTPBasicCredentials | None = Depends(security),
) -> str:
return verify_admin_auth(request, credentials)
def verify_any_auth(
request: Request,
credentials: HTTPBasicCredentials | None = Depends(security),
) -> str:
if is_records_authenticated(request, credentials) or is_admin_authenticated(request, credentials):
return "authenticated"
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="请先登录",
)
def verify_ingest_token(x_ingest_token: str = Header(default="")) -> str:
configured_password("课程小结推送", INGEST_AUTH_TOKEN)
if not hmac.compare_digest(x_ingest_token, INGEST_AUTH_TOKEN):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="课程小结推送 token 不正确",
)
return "ingest"