Files
track/backend/app/api/v1/endpoints/auth.py
duxingchen 17b2fab5ca fix(audit): 补齐「退出」留痕 + 登录记录显示中文操作人
【问题①:退出动作在审计里完全不可见】
根因:后端没有 logout 接口。前端「退出」只清本地 localStorage、不产生任何
请求,中间件自然无从采集(中间件里早已预留 "logout" 动作映射,但端点没做)。

- 新增 POST /auth/logout,【仅用于留痕】。JWT 无状态,服务端本就没有可吊销
  的会话,该接口不做任何令牌失效动作 —— 它存在的唯一目的,是让审计记下
  「谁在何时退出了系统」。这点在 docstring 里写明,免得日后被误当安全边界。
- 挂 Depends(get_current_user),让 JWT 依赖把操作人写进 request.state,
  从而记录到真实姓名而非「未认证」
- 前端 logout 改为【先上报、后清 token】。顺序不能反:api.ts 的请求拦截器
  是从 localStorage 取 token 的,清掉后就发不出这个请求了。
  刻意不 await、失败也不阻断 —— 用户点「退出」必须退得掉。

【问题②:登录记录的「操作人」显示英文账号而非中文名】
根因:登录接口只写了 request.state.audit_user(账号),没写 audit_display_name
—— 那一刻还不知道显示名。前端按 display_name || user_id 渲染,于是退化成账号。

- 改为登录成功后补写 display_name / role。可行的原因是中间件在 call_next
  返回【之后】才落库,此刻写 request.state 依然能被采集到。
- 失败登录走不到这一步,保持「只有账号可追责」(密码绝不落库),语义不变。
2026-09-21 11:20:59 +08:00

69 lines
3.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""认证 API — 对接 MOM sys_user + 双 Token 刷新"""
from fastapi import APIRouter, Depends, Request
from app.schemas.user import (
LoginRequest,
LoginResponse,
RefreshRequest,
RefreshResponse,
UserResponse,
)
from app.services.auth_service import login, refresh_access_token, get_current_user
router = APIRouter(prefix="/auth", tags=["认证"])
@router.post("/login", response_model=LoginResponse)
def login_endpoint(data: LoginRequest, request: Request):
"""登录 — 验证 MOM sys_user 表,返回 Access + Refresh 双 Token"""
# 登录请求本身尚未认证,中间件拿不到操作人。但「谁在尝试登录、失败了多少次」
# 恰恰是审计里最该有的信息,所以在校验之前就把尝试的账号写进 state
# 登录失败时同样留痕,且能按账号追踪暴力破解。
# 注意:绝不把 data.password 写进 state / 审计,密码不落库。
request.state.audit_user = data.username
result = login(data.username, data.password)
# 登录成功后补上显示名 / 角色 —— 否则审计里这条记录的「操作人」会退化成账号
# (前端按 display_name || user_id 渲染,见 AdminAuditLogPage
# 能在这里补的原因:中间件是在 call_next 返回【之后】才落库的,此刻写入
# request.state 依然会被采集到。
# 失败登录走不到这里,保持「只有账号可追责」——这正是想要的语义。
if result.user:
request.state.audit_display_name = result.user.display_name
request.state.audit_role = result.user.role
return result
@router.post("/refresh", response_model=RefreshResponse)
def refresh_endpoint(data: RefreshRequest):
"""刷新 Access Token — 使用 Refresh Token 换取新的 Access Token"""
return refresh_access_token(data.refresh_token)
@router.post("/logout")
def logout_endpoint(current_user: dict = Depends(get_current_user)):
"""登出 —— 仅用于审计留痕。
JWT 是无状态的,服务端没有可吊销的会话,因此本接口**不做任何令牌失效**
(客户端清掉本地 token 即为登出),返回体也没有实际语义。
它存在的唯一目的:让审计中间件记下「谁在何时退出了系统」。
没有这个端点时,前端「退出」只清本地存储、不产生任何请求,
退出动作在审计里完全不可见 —— 而"谁在什么时候退掉了系统"
在追责场景下和"谁登录了"同等重要。
挂 Depends(get_current_user) 是为了让 JWT 依赖把操作人写进 request.state
(见 auth_service.get_current_user记录到真实姓名而非「未认证」。
"""
return {"ok": True}
@router.get("/me", response_model=UserResponse)
def get_me(current_user: dict = Depends(get_current_user)):
"""获取当前用户信息(从 Access Token 解析)"""
return UserResponse(
id=current_user["sub"],
username=current_user.get("username", ""),
display_name=current_user.get("display_name", ""),
role=current_user.get("role", "operator"),
)