[backend] fix: 修复认证相关的测试

- 修改LoginRequest schema,使字段为可选
- 添加自定义异常处理器以返回正确的JSON格式
- 修改认证中间件,返回正确的错误码和响应格式
- 所有auth测试(10个)全部通过

测试结果:10 passed
This commit is contained in:
xsl
2026-01-26 10:13:09 +08:00
parent e7906d57ee
commit 64b08c4e67
9 changed files with 164 additions and 16 deletions
BIN
View File
Binary file not shown.
Binary file not shown.
+35 -1
View File
@@ -1,5 +1,8 @@
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.exceptions import HTTPException, RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from config.settings import get_settings
from config.database import init_db
from src.routes import auth_router, users_router, projects_router
@@ -27,6 +30,37 @@ app.include_router(users_router, prefix="/api/v1")
app.include_router(projects_router, prefix="/api/v1")
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
if isinstance(exc.detail, dict):
return JSONResponse(
status_code=exc.status_code,
content=exc.detail,
)
return JSONResponse(
status_code=exc.status_code,
content={
"success": False,
"message": exc.detail,
"data": None,
"error_code": None,
},
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={
"success": False,
"message": "请求验证失败",
"data": exc.errors(),
"error_code": "1001",
},
)
@app.on_event("startup")
async def startup_event():
await init_db()
+76 -7
View File
@@ -1,33 +1,54 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from config.database import get_db
from src.models.user import User
from src.utils.jwt import decode_access_token
security = HTTPBearer()
security = HTTPBearer(auto_error=False)
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: AsyncSession = Depends(get_db),
) -> User:
if not credentials:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={
"success": False,
"message": "未提供认证token",
"data": None,
"error_code": "1002",
},
)
token = credentials.credentials
payload = decode_access_token(token)
if payload is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token无效或过期",
headers={"WWW-Authenticate": "Bearer"},
detail={
"success": False,
"message": "Token无效或过期",
"data": None,
"error_code": "1003",
},
)
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token无效",
detail={
"success": False,
"message": "Token无效",
"data": None,
"error_code": "1002",
},
)
result = await db.execute(select(User).where(User.id == int(user_id)))
@@ -36,13 +57,23 @@ async def get_current_user(
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户不存在",
detail={
"success": False,
"message": "用户不存在",
"data": None,
"error_code": "1002",
},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户已被禁用",
detail={
"success": False,
"message": "用户已被禁用",
"data": None,
"error_code": "1002",
},
)
return user
@@ -54,11 +85,49 @@ async def get_current_active_user(
if not current_user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="用户未激活",
detail={
"success": False,
"message": "用户未激活",
"data": None,
"error_code": "1002",
},
)
return current_user
async def require_admin(
current_user: User = Depends(get_current_active_user),
) -> User:
if current_user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={
"success": False,
"message": "需要管理员权限",
"data": None,
"error_code": "3001",
},
)
return current_user
async def require_market_or_admin(
current_user: User = Depends(get_current_active_user),
) -> User:
if current_user.role not in ["admin", "market"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={
"success": False,
"message": "需要市场部或管理员权限",
"data": None,
"error_code": "3001",
},
)
return current_user
async def require_admin(
current_user: User = Depends(get_current_active_user),
) -> User:
Binary file not shown.
+51 -6
View File
@@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, status
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from config.database import get_db
@@ -17,15 +18,59 @@ async def login(
login_data: LoginRequest,
db: AsyncSession = Depends(get_db),
):
if not login_data.username:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"success": False,
"message": "用户名和密码不能为空",
"data": None,
"error_code": "1001",
}
)
if not login_data.password:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"success": False,
"message": "用户名和密码不能为空",
"data": None,
"error_code": "1001",
}
)
if len(login_data.username) < 3:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"success": False,
"message": "用户名至少需要3个字符",
"data": None,
"error_code": "1001",
}
)
result = await db.execute(
select(User).where(User.username == login_data.username)
)
user = result.scalar_one_or_none()
if not user or not verify_password(login_data.password, user.password_hash):
raise HTTPException(
if not user:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={
content={
"success": False,
"message": "用户名或密码错误",
"data": None,
"error_code": "1002",
}
)
if not verify_password(login_data.password, user.password_hash):
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={
"success": False,
"message": "用户名或密码错误",
"data": None,
@@ -34,9 +79,9 @@ async def login(
)
if not user.is_active:
raise HTTPException(
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={
content={
"success": False,
"message": "用户未激活",
"data": None,
Binary file not shown.
+2 -2
View File
@@ -4,8 +4,8 @@ from .user import UserResponse
class LoginRequest(BaseModel):
username: str = Field(..., min_length=3)
password: str = Field(..., min_length=6)
username: Optional[str] = None
password: Optional[str] = None
class LoginResponse(BaseModel):