- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
278 lines
8.5 KiB
Python
278 lines
8.5 KiB
Python
"""
|
||
管理员认证 API
|
||
"""
|
||
|
||
import os
|
||
from datetime import datetime, timedelta
|
||
from typing import Optional
|
||
|
||
import jwt
|
||
import pymysql
|
||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||
from passlib.context import CryptContext
|
||
|
||
from .schemas import (
|
||
AdminLoginRequest,
|
||
AdminLoginResponse,
|
||
AdminUserInfo,
|
||
AdminChangePasswordRequest,
|
||
ResponseModel,
|
||
)
|
||
|
||
router = APIRouter(prefix="/auth", tags=["管理员认证"])
|
||
|
||
# 密码加密
|
||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||
|
||
# JWT 配置
|
||
SECRET_KEY = os.getenv("ADMIN_JWT_SECRET", "admin-secret-key-kaopeilian-2026")
|
||
ALGORITHM = "HS256"
|
||
ACCESS_TOKEN_EXPIRE_HOURS = 24
|
||
|
||
# 安全认证
|
||
security = HTTPBearer()
|
||
|
||
# 管理库连接配置
|
||
ADMIN_DB_CONFIG = {
|
||
"host": os.getenv("ADMIN_DB_HOST", "prod-mysql"),
|
||
"port": int(os.getenv("ADMIN_DB_PORT", "3306")),
|
||
"user": os.getenv("ADMIN_DB_USER", "root"),
|
||
"password": os.getenv("ADMIN_DB_PASSWORD", "ProdMySQL2025!@#"),
|
||
"db": os.getenv("ADMIN_DB_NAME", "kaopeilian_admin"),
|
||
"charset": "utf8mb4",
|
||
}
|
||
|
||
|
||
def get_db_connection():
|
||
"""获取数据库连接"""
|
||
return pymysql.connect(**ADMIN_DB_CONFIG, cursorclass=pymysql.cursors.DictCursor)
|
||
|
||
|
||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||
"""验证密码"""
|
||
return pwd_context.verify(plain_password, hashed_password)
|
||
|
||
|
||
def get_password_hash(password: str) -> str:
|
||
"""获取密码哈希"""
|
||
return pwd_context.hash(password)
|
||
|
||
|
||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||
"""创建访问令牌"""
|
||
to_encode = data.copy()
|
||
if expires_delta:
|
||
expire = datetime.utcnow() + expires_delta
|
||
else:
|
||
expire = datetime.utcnow() + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS)
|
||
to_encode.update({"exp": expire})
|
||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||
return encoded_jwt
|
||
|
||
|
||
def decode_access_token(token: str) -> dict:
|
||
"""解码访问令牌"""
|
||
try:
|
||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||
return payload
|
||
except jwt.ExpiredSignatureError:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="Token已过期",
|
||
)
|
||
except jwt.InvalidTokenError:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="无效的Token",
|
||
)
|
||
|
||
|
||
async def get_current_admin(
|
||
credentials: HTTPAuthorizationCredentials = Depends(security)
|
||
) -> AdminUserInfo:
|
||
"""获取当前登录的管理员"""
|
||
token = credentials.credentials
|
||
payload = decode_access_token(token)
|
||
|
||
admin_id = payload.get("sub")
|
||
if not admin_id:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="无效的Token",
|
||
)
|
||
|
||
conn = get_db_connection()
|
||
try:
|
||
with conn.cursor() as cursor:
|
||
cursor.execute(
|
||
"""
|
||
SELECT id, username, email, full_name, role, is_active, last_login_at
|
||
FROM admin_users WHERE id = %s
|
||
""",
|
||
(admin_id,)
|
||
)
|
||
admin = cursor.fetchone()
|
||
|
||
if not admin:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="管理员不存在",
|
||
)
|
||
|
||
if not admin["is_active"]:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_403_FORBIDDEN,
|
||
detail="账户已被禁用",
|
||
)
|
||
|
||
return AdminUserInfo(
|
||
id=admin["id"],
|
||
username=admin["username"],
|
||
email=admin["email"],
|
||
full_name=admin["full_name"],
|
||
role=admin["role"],
|
||
last_login_at=admin["last_login_at"],
|
||
)
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
async def require_superadmin(
|
||
admin: AdminUserInfo = Depends(get_current_admin)
|
||
) -> AdminUserInfo:
|
||
"""要求超级管理员权限"""
|
||
if admin.role != "superadmin":
|
||
raise HTTPException(
|
||
status_code=status.HTTP_403_FORBIDDEN,
|
||
detail="需要超级管理员权限",
|
||
)
|
||
return admin
|
||
|
||
|
||
@router.post("/login", response_model=AdminLoginResponse, summary="管理员登录")
|
||
async def admin_login(request: Request, login_data: AdminLoginRequest):
|
||
"""
|
||
管理员登录
|
||
|
||
- **username**: 用户名
|
||
- **password**: 密码
|
||
"""
|
||
conn = get_db_connection()
|
||
try:
|
||
with conn.cursor() as cursor:
|
||
# 查询管理员
|
||
cursor.execute(
|
||
"""
|
||
SELECT id, username, email, full_name, role, password_hash, is_active, last_login_at
|
||
FROM admin_users WHERE username = %s
|
||
""",
|
||
(login_data.username,)
|
||
)
|
||
admin = cursor.fetchone()
|
||
|
||
if not admin:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="用户名或密码错误",
|
||
)
|
||
|
||
if not admin["is_active"]:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_403_FORBIDDEN,
|
||
detail="账户已被禁用",
|
||
)
|
||
|
||
# 验证密码
|
||
if not verify_password(login_data.password, admin["password_hash"]):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="用户名或密码错误",
|
||
)
|
||
|
||
# 更新最后登录时间和IP
|
||
client_ip = request.client.host if request.client else None
|
||
cursor.execute(
|
||
"""
|
||
UPDATE admin_users
|
||
SET last_login_at = NOW(), last_login_ip = %s
|
||
WHERE id = %s
|
||
""",
|
||
(client_ip, admin["id"])
|
||
)
|
||
conn.commit()
|
||
|
||
# 创建 Token
|
||
access_token = create_access_token(
|
||
data={"sub": str(admin["id"]), "username": admin["username"], "role": admin["role"]}
|
||
)
|
||
|
||
return AdminLoginResponse(
|
||
access_token=access_token,
|
||
token_type="bearer",
|
||
expires_in=ACCESS_TOKEN_EXPIRE_HOURS * 3600,
|
||
admin_user=AdminUserInfo(
|
||
id=admin["id"],
|
||
username=admin["username"],
|
||
email=admin["email"],
|
||
full_name=admin["full_name"],
|
||
role=admin["role"],
|
||
last_login_at=datetime.now(),
|
||
),
|
||
)
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
@router.get("/me", response_model=AdminUserInfo, summary="获取当前管理员信息")
|
||
async def get_me(admin: AdminUserInfo = Depends(get_current_admin)):
|
||
"""获取当前登录管理员的信息"""
|
||
return admin
|
||
|
||
|
||
@router.post("/change-password", response_model=ResponseModel, summary="修改密码")
|
||
async def change_password(
|
||
data: AdminChangePasswordRequest,
|
||
admin: AdminUserInfo = Depends(get_current_admin),
|
||
):
|
||
"""
|
||
修改当前管理员密码
|
||
|
||
- **old_password**: 旧密码
|
||
- **new_password**: 新密码
|
||
"""
|
||
conn = get_db_connection()
|
||
try:
|
||
with conn.cursor() as cursor:
|
||
# 验证旧密码
|
||
cursor.execute(
|
||
"SELECT password_hash FROM admin_users WHERE id = %s",
|
||
(admin.id,)
|
||
)
|
||
row = cursor.fetchone()
|
||
|
||
if not verify_password(data.old_password, row["password_hash"]):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_400_BAD_REQUEST,
|
||
detail="旧密码错误",
|
||
)
|
||
|
||
# 更新密码
|
||
new_hash = get_password_hash(data.new_password)
|
||
cursor.execute(
|
||
"UPDATE admin_users SET password_hash = %s WHERE id = %s",
|
||
(new_hash, admin.id)
|
||
)
|
||
conn.commit()
|
||
|
||
return ResponseModel(message="密码修改成功")
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
@router.post("/logout", response_model=ResponseModel, summary="退出登录")
|
||
async def admin_logout(admin: AdminUserInfo = Depends(get_current_admin)):
|
||
"""退出登录(客户端需清除 Token)"""
|
||
return ResponseModel(message="退出成功")
|
||
|