Files
Neo-ZQYY/apps/backend/app/routers/auth.py
Neo 17f045a89e fix(backend): Wave 1 Day 1 三个 P0 D Bug 修复
- W1-T3 修 4 处 fdw_etl.* 必坏残留 → app.* (P0-5 致命 1)
  · tenant_users.py L431/L456-457: v_dim_assistant + v_dim_staff(_ex)
  · tenant_excel.py L394/L411: v_dim_assistant + v_dim_staff
  · tenant_clues.py L119: v_dim_member
  · 修复后 tenant-admin 用户审核 / Excel 上传 / 维客线索恢复正常

- W1-T4 JWT aud sign 端写入 (P0-5 致命 2 最小止血)
  · jwt.py 全部 token 创建/解码函数加 audience 参数
  · auth.py admin 端加 audience="admin"
  · xcx_auth.py miniapp 端加 audience="miniapp" (8 处调用)
  · 18 router 切强制 aud 校验留 Wave 2

- W1-T5 DBViewer 白名单 + 黑名单双保险 (P0-8)
  · 白名单: SELECT/WITH/EXPLAIN/SHOW 开头
  · 黑名单: 17 关键词覆盖全 DML/DDL/DCL
  · 注释剥离避免误伤;15/15 单测 PASS

参考: docs/audit/changes/2026-05-04__wave1_day1_d_bug_triple_fix.md
2026-05-04 07:36:20 +08:00

114 lines
3.3 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.
"""
认证路由:登录与令牌刷新。
- POST /api/auth/login — 验证用户名密码,返回 JWT 令牌对
- POST /api/auth/refresh — 用刷新令牌换取新的访问令牌
"""
import logging
from fastapi import APIRouter, HTTPException, status
from jose import JWTError
from app.auth.jwt import (
create_access_token,
create_token_pair,
decode_refresh_token,
verify_password,
)
from app.database import get_connection
from app.schemas.auth import LoginRequest, RefreshRequest, TokenResponse
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/auth", tags=["认证"])
@router.post("/login", response_model=TokenResponse)
async def login(body: LoginRequest):
"""
用户登录。
查询 admin_users 表验证用户名密码,成功后返回 JWT 令牌对。
- 用户不存在或密码错误401
- 账号已禁用is_active=false401
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"SELECT id, password_hash, site_id, is_active, roles "
"FROM admin_users WHERE username = %s",
(body.username,),
)
row = cur.fetchone()
finally:
conn.close()
if row is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
)
user_id, password_hash, site_id, is_active, roles = row
if not is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="账号已被禁用",
)
if not verify_password(body.password, password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
)
tokens = create_token_pair(user_id, site_id, roles=roles or [], audience="admin")
return TokenResponse(**tokens)
@router.post("/refresh", response_model=TokenResponse)
async def refresh(body: RefreshRequest):
"""
刷新访问令牌。
验证 refresh_token 有效性,成功后仅返回新的 access_token
refresh_token 保持不变,由客户端继续持有)。
"""
try:
# 兼容:旧 token 无 aud(audience=None);新 token 应带 aud="admin"
# 灰度期暂不强制 aud 校验,Wave 2 切换强制
payload = decode_refresh_token(body.refresh_token)
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的刷新令牌",
)
user_id = int(payload["sub"])
site_id = payload["site_id"]
# CHANGE 2026-03-24 | Prompt: 修复 refresh 丢失 roles | 刷新前查询数据库获取最新 roles
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"SELECT roles FROM admin_users WHERE id = %s",
(user_id,),
)
row = cur.fetchone()
finally:
conn.close()
roles = row[0] if row else []
# 生成新的 access_token,refresh_token 原样返回
new_access = create_access_token(user_id, site_id, roles=roles or [], audience="admin")
return TokenResponse(
access_token=new_access,
refresh_token=body.refresh_token,
token_type="bearer",
)