ForcePilot/server/routers/auth_router.py
Wenjie Zhang 747c5de88f feat(auth): 实现账户登录失败次数限制和锁定机制(注意,需要迁移数据库)
- 数据库用户模型新增登录失败限制相关字段(login_failed_count, last_failed_login, login_locked_until)
- 增加用户方法判断是否处于登录锁定状态及计算锁定持续时间
- 登录接口新增登录锁定检测,密码错误时增加失败计数及设置锁定时间
- 登录成功时重置失败计数和锁定状态
- 新增数据库迁移模块,自动检测并执行数据库结构更新,确保新增字段同步
- 前端登录页添加账户锁定状态显示及倒计时功能
- 前端请求中捕获423锁定状态,展示对应锁定提示,禁用登录按钮
- 优化本地状态管理,支持登录锁定错误抛出与异常处理机制
2025-09-17 03:59:51 +08:00

366 lines
12 KiB
Python

from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel
from sqlalchemy.orm import Session
from server.db_manager import db_manager
from server.models.user_model import User
from server.utils.auth_middleware import get_admin_user, get_current_user, get_db
from server.utils.auth_utils import AuthUtils
from server.utils.common_utils import log_operation
# 创建路由器
auth = APIRouter(prefix="/auth", tags=["authentication"])
# 请求和响应模型
class Token(BaseModel):
access_token: str
token_type: str
user_id: int
username: str
role: str
class UserCreate(BaseModel):
username: str
password: str
role: str = "user"
class UserUpdate(BaseModel):
username: str | None = None
password: str | None = None
role: str | None = None
class UserResponse(BaseModel):
id: int
username: str
role: str
created_at: str
last_login: str | None = None
class InitializeAdmin(BaseModel):
username: str
password: str
# =============================================================================
# === 工具函数 ===
# =============================================================================
# 路由:登录获取令牌
# =============================================================================
# === 认证分组 ===
# =============================================================================
@auth.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
# 查找用户
user = db.query(User).filter(User.username == form_data.username).first()
# 如果用户不存在,为防止用户名枚举攻击,返回通用错误信息
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
# 检查用户是否处于登录锁定状态
if user.is_login_locked():
remaining_time = user.get_remaining_lock_time()
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"登录被锁定,请等待 {remaining_time} 秒后再试",
headers={
"WWW-Authenticate": "Bearer",
"X-Lock-Remaining": str(remaining_time)
},
)
# 验证密码
if not AuthUtils.verify_password(user.password_hash, form_data.password):
# 密码错误,增加失败次数
user.increment_failed_login()
db.commit()
# 记录失败操作
log_operation(db, user.id if user else None, "登录失败", f"密码错误,失败次数: {user.login_failed_count}")
# 检查是否需要锁定
if user.is_login_locked():
remaining_time = user.get_remaining_lock_time()
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"由于多次登录失败,账户已被锁定 {remaining_time}",
headers={
"WWW-Authenticate": "Bearer",
"X-Lock-Remaining": str(remaining_time)
},
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
# 登录成功,重置失败计数器
user.reset_failed_login()
user.last_login = datetime.now()
db.commit()
# 生成访问令牌
token_data = {"sub": str(user.id)}
access_token = AuthUtils.create_access_token(token_data)
# 记录登录操作
log_operation(db, user.id, "登录")
return {
"access_token": access_token,
"token_type": "bearer",
"user_id": user.id,
"username": user.username,
"role": user.role,
}
# 路由:校验是否需要初始化管理员
@auth.get("/check-first-run")
async def check_first_run():
is_first_run = db_manager.check_first_run()
return {"first_run": is_first_run}
# 路由:初始化管理员账户
@auth.post("/initialize", response_model=Token)
async def initialize_admin(admin_data: InitializeAdmin, db: Session = Depends(get_db)):
# 检查是否是首次运行
if not db_manager.check_first_run():
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="系统已经初始化,无法再次创建初始管理员",
)
# 创建管理员账户
hashed_password = AuthUtils.hash_password(admin_data.password)
new_admin = User(
username=admin_data.username, password_hash=hashed_password, role="superadmin", last_login=datetime.now()
)
db.add(new_admin)
db.commit()
db.refresh(new_admin)
# 生成访问令牌
token_data = {"sub": str(new_admin.id)}
access_token = AuthUtils.create_access_token(token_data)
# 记录操作
log_operation(db, new_admin.id, "系统初始化", "创建超级管理员账户")
return {
"access_token": access_token,
"token_type": "bearer",
"user_id": new_admin.id,
"username": new_admin.username,
"role": new_admin.role,
}
# 路由:获取当前用户信息
# =============================================================================
# === 用户信息分组 ===
# =============================================================================
@auth.get("/me", response_model=UserResponse)
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user.to_dict()
# 路由:创建新用户(管理员权限)
# =============================================================================
# === 用户管理分组 ===
# =============================================================================
@auth.post("/users", response_model=UserResponse)
async def create_user(
user_data: UserCreate, request: Request, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db)
):
# 检查用户名是否已存在
existing_user = db.query(User).filter(User.username == user_data.username).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="用户名已存在",
)
# 创建新用户
hashed_password = AuthUtils.hash_password(user_data.password)
# 检查角色权限
# 超级管理员可以创建任何类型的用户
if user_data.role == "superadmin" and current_user.role != "superadmin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有超级管理员才能创建超级管理员账户",
)
# 管理员只能创建普通用户
if current_user.role == "admin" and user_data.role != "user":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="管理员只能创建普通用户账户",
)
new_user = User(username=user_data.username, password_hash=hashed_password, role=user_data.role)
db.add(new_user)
db.commit()
db.refresh(new_user)
# 记录操作
log_operation(db, current_user.id, "创建用户", f"创建用户: {user_data.username}, 角色: {user_data.role}", request)
return new_user.to_dict()
# 路由:获取所有用户(管理员权限)
@auth.get("/users", response_model=list[UserResponse])
async def read_users(
skip: int = 0, limit: int = 100, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db)
):
users = db.query(User).offset(skip).limit(limit).all()
return [user.to_dict() for user in users]
# 路由:获取特定用户信息(管理员权限)
@auth.get("/users/{user_id}", response_model=UserResponse)
async def read_user(user_id: int, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="用户不存在",
)
return user.to_dict()
# 路由:更新用户信息(管理员权限)
@auth.put("/users/{user_id}", response_model=UserResponse)
async def update_user(
user_id: int,
user_data: UserUpdate,
request: Request,
current_user: User = Depends(get_admin_user),
db: Session = Depends(get_db),
):
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="用户不存在",
)
# 检查权限
if user.role == "superadmin" and current_user.role != "superadmin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有超级管理员才能修改超级管理员账户",
)
# 超级管理员账户不能被降级(只能由其他超级管理员修改)
if user.role == "superadmin" and user_data.role and user_data.role != "superadmin" and current_user.id != user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="不能降级超级管理员账户",
)
# 更新信息
update_details = []
if user_data.username is not None:
# 检查用户名是否已被其他用户使用
existing_user = db.query(User).filter(User.username == user_data.username, User.id != user_id).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="用户名已存在",
)
user.username = user_data.username
update_details.append(f"用户名: {user_data.username}")
if user_data.password is not None:
user.password_hash = AuthUtils.hash_password(user_data.password)
update_details.append("密码已更新")
if user_data.role is not None:
user.role = user_data.role
update_details.append(f"角色: {user_data.role}")
db.commit()
# 记录操作
log_operation(db, current_user.id, "更新用户", f"更新用户ID {user_id}: {', '.join(update_details)}", request)
return user.to_dict()
# 路由:删除用户(管理员权限)
@auth.delete("/users/{user_id}", response_model=dict)
async def delete_user(
user_id: int, request: Request, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db)
):
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="用户不存在",
)
# 检查权限
if user.role == "superadmin":
# 只有超级管理员可以删除超级管理员
if current_user.role != "superadmin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有超级管理员才能删除超级管理员账户",
)
# 检查是否是最后一个超级管理员
superadmin_count = db.query(User).filter(User.role == "superadmin").count()
if superadmin_count <= 1:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="不能删除最后一个超级管理员账户",
)
# 不能删除自己的账户
if user.id == current_user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="不能删除自己的账户",
)
# 记录操作
log_operation(
db, current_user.id, "删除用户", f"删除用户: {user.username}, ID: {user.id}, 角色: {user.role}", request
)
# 删除用户
db.delete(user)
db.commit()
return {"success": True, "message": "用户已删除"}