0841b1a103
- 前端:Vue3 + TS + Element Plus,24 个页面路由(课件/组题/教案/动画/思维导图/作文批改/命题/课堂/资源/社区等) - 后端:FastAPI + SQLAlchemy + SQLite,17 个路由模块,AI 服务层含降级模板 - AI:glm-5.x 推理模型已禁用思维链,确保输出真实内容 - 修复:ai_service 两处请求体注入 enable_thinking/thinking disabled - 测试账号:13900999999 / test1234
97 lines
2.6 KiB
Python
97 lines
2.6 KiB
Python
"""安全审计日志服务。
|
|
|
|
提供 log_action() 用于在任意路由中记录安全相关事件。日志写入数据库 audit_logs 表,
|
|
同时输出到 logger,便于后续接入文件/集中式日志收集。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, Optional
|
|
|
|
from fastapi import Request
|
|
from sqlalchemy.orm import Session
|
|
|
|
from models.audit import AuditLog
|
|
from models.user import User
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _user_label(user: Optional[User]) -> str:
|
|
"""返回用户可读标识:优先 username,其次 phone,最后 id。"""
|
|
if user is None:
|
|
return ""
|
|
for attr in ("username", "phone", "name"):
|
|
val = getattr(user, attr, None)
|
|
if val:
|
|
return str(val)
|
|
return str(getattr(user, "id", ""))
|
|
|
|
|
|
def _client_ip(request: Optional[Request]) -> str:
|
|
if request is None:
|
|
return ""
|
|
try:
|
|
fwd = request.headers.get("x-forwarded-for")
|
|
if fwd:
|
|
return fwd.split(",")[0].strip()[:64]
|
|
client = getattr(request, "client", None)
|
|
if client and client.host:
|
|
return client.host[:64]
|
|
except Exception:
|
|
pass
|
|
return ""
|
|
|
|
|
|
def _user_agent(request: Optional[Request]) -> str:
|
|
if request is None:
|
|
return ""
|
|
try:
|
|
return (request.headers.get("user-agent") or "")[:255]
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
def log_action(
|
|
db: Session,
|
|
*,
|
|
action: str,
|
|
user: Optional[User] = None,
|
|
request: Optional[Request] = None,
|
|
target_type: str = "",
|
|
target_id: Any = "",
|
|
detail: str = "",
|
|
status: str = "success",
|
|
) -> None:
|
|
"""记录一条审计日志。失败不影响主流程。"""
|
|
try:
|
|
log = AuditLog(
|
|
user_id=user.id if user else None,
|
|
username=_user_label(user),
|
|
action=action[:64],
|
|
target_type=target_type[:32] if target_type else "",
|
|
target_id=str(target_id)[:64] if target_id else "",
|
|
detail=detail[:2000] if detail else "",
|
|
ip=_client_ip(request),
|
|
user_agent=_user_agent(request),
|
|
status=status[:16] if status else "success",
|
|
)
|
|
db.add(log)
|
|
db.commit()
|
|
except Exception as exc: # pragma: no cover - 日志失败不应中断业务
|
|
logger.warning("audit log failed: %s", exc)
|
|
try:
|
|
db.rollback()
|
|
except Exception:
|
|
pass
|
|
# 同时输出到日志通道
|
|
logger.info(
|
|
"audit action=%s user=%s target=%s/%s status=%s ip=%s",
|
|
action,
|
|
_user_label(user),
|
|
target_type,
|
|
target_id,
|
|
status,
|
|
_client_ip(request),
|
|
)
|