FastAPI 实战:用户登录注册完整实现

7 阅读11分钟

用户登录注册是绝大多数后端系统的基础功能,其核心需求是保障用户身份合法性、密码安全性,以及登录后的身份验证。FastAPI结合SQLAlchemy异步ORM和Pydantic数据校验,能快速实现高安全、易维护的登录注册功能,同时支持Token身份验证,适配前后端分离场景。本文将从数据库模型设计、密码加密、接口开发、Token生成与验证四个核心环节,结合可直接运行的代码示例,完整实现用户登录注册功能,全程围绕核心需求展开,确保代码规范、功能可用。

一、环境准备与核心依赖

实现登录注册功能需依赖三个核心库,分别负责异步数据库操作、数据校验和密码加密,提前安装相关依赖,确保项目正常运行:

# 安装核心依赖
pip install fastapi uvicorn sqlalchemy asyncmy passlib python-multipart python-jose[cryptography]

各依赖作用说明:fastapi用于接口开发,uvicorn作为ASGI服务器启动项目,sqlalchemy+asyncmy实现异步数据库操作,passlib用于密码加密与验证,python-jose用于Token生成与解析。

二、数据库模型设计(用户表与Token表)

用户登录注册需设计两张核心数据表:用户表(存储用户基础信息,含加密后的密码)、Token表(存储用户登录后的身份令牌,用于后续身份验证)。采用SQLAlchemy异步ORM设计模型,确保数据操作高效、安全。

from datetime import datetime, timedelta
from typing import Optional
from sqlalchemy import Index, Integer, String, DateTime, ForeignKey
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

# 数据库模型基类(集成异步属性)
class Base(AsyncAttrs, DeclarativeBase):
    pass

# 用户表模型
class User(Base):
    __tablename__ = "user"
    # 唯一索引,确保用户名、手机号不重复(避免重复注册)
    __table_args__ = (
        Index("idx_username_unique", "username", unique=True),
        Index("idx_phone_unique", "phone", unique=True),
    )

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="用户主键ID")
    username: Mapped[str] = mapped_column(String(50), nullable=False, comment="用户名(登录账号)")
    password: Mapped[str] = mapped_column(String(255), nullable=False, comment="加密后的密码")
    nickname: Mapped[Optional[str]] = mapped_column(String(50), comment="用户昵称,可选")
    phone: Mapped[Optional[str]] = mapped_column(String(11), comment="手机号,可选且唯一")
    avatar: Mapped[Optional[str]] = mapped_column(String(255), comment="用户头像URL,可选")
    create_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, comment="创建时间")
    update_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")

    def __repr__(self):
        return f"<User(id={self.id}, username='{self.username}', nickname='{self.nickname}')>"

# Token表模型(存储用户登录令牌,用于身份验证)
class UserToken(Base):
    __tablename__ = "user_token"
    __table_args__ = (
        Index("idx_token_unique", "token", unique=True),
        Index("idx_user_id", "user_id"),
    )

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="Token主键ID")
    user_id: Mapped[int] = mapped_column(Integer, ForeignKey("user.id", ondelete="CASCADE"), nullable=False, comment="关联用户ID")
    token: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, comment="登录令牌")
    expire_time: Mapped[datetime] = mapped_column(DateTime, nullable=False, comment="令牌过期时间")
    create_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, comment="令牌创建时间")

    def __repr__(self):
        return f"<UserToken(id={self.id}, user_id={self.user_id}, token='{self.token}')>"

说明:用户表中密码字段存储加密后的密码,不存储明文,保障密码安全;Token表与用户表通过外键关联,令牌设置过期时间(如7天),避免令牌长期有效带来的安全风险。

三、核心工具封装(密码加密与Token处理)

登录注册的核心安全点的是密码加密和Token验证,将这两个功能封装为工具函数,供后续接口和业务逻辑调用,提升代码复用性和可维护性。

1. 密码加密与验证工具

使用passlib库的bcrypt加密算法,对用户密码进行加密存储,验证时对比明文密码与加密密码的一致性,避免明文密码泄露。

from passlib.context import CryptContext

# 密码加密上下文(使用bcrypt算法,自动处理加密加盐)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def encrypt_password(plain_password: str) -> str:
    """密码加密:将明文密码转换为加密字符串"""
    return pwd_context.hash(plain_password)

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """密码验证:对比明文密码与加密密码是否一致,返回布尔值"""
    return pwd_context.verify(plain_password, hashed_password)

2. Token生成与解析工具

使用python-jose库生成JWT格式Token,包含用户ID等核心信息,设置过期时间;同时提供Token解析函数,用于登录后接口的身份验证。

from datetime import datetime, timedelta
from jose import JWTError, jwt
from fastapi import HTTPException, status

# Token配置(实际开发中建议放在配置文件,避免硬编码)
SECRET_KEY = "your-secret-key-1234567890abcdef"  # 建议使用随机字符串,可通过uuid生成
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_DAYS = 7  # Token有效期7天

def create_access_token(user_id: int) -> str:
    """生成登录Token:包含用户ID,设置过期时间"""
    # 构建Token payload(存储核心信息,避免敏感数据)
    expire = datetime.utcnow() + timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS)
    to_encode = {"sub": str(user_id), "exp": expire}
    # 生成JWT Token
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def parse_access_token(token: str) -> int:
    """解析Token:提取用户ID,验证Token有效性,无效则抛出异常"""
    try:
        # 解析Token
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        user_id: str = payload.get("sub")
        if user_id is None:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Token解析失败,无法获取用户信息",
                headers={"WWW-Authenticate": "Bearer"},
            )
        return int(user_id)
    except JWTError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Token无效或已过期,请重新登录",
            headers={"WWW-Authenticate": "Bearer"},
        )

四、数据校验模型(Pydantic)

使用Pydantic定义请求体和响应体模型,对用户注册、登录的请求参数进行校验(如用户名长度、密码强度),同时规范接口返回数据格式,避免无效数据进入业务层。

from pydantic import BaseModel, Field, EmailStr
from typing import Optional

# 注册请求体模型(校验注册参数)
class UserRegisterRequest(BaseModel):
    username: str = Field(..., min_length=4, max_length=50, description="用户名,4-50个字符")
    password: str = Field(..., min_length=6, max_length=20, description="密码,6-20个字符")
    nickname: Optional[str] = Field(None, max_length=50, description="昵称,可选")
    phone: Optional[str] = Field(None, pattern=r"^1[3-9]\d{9}$", description="手机号,可选,需符合11位手机号格式")

# 登录请求体模型(校验登录参数)
class UserLoginRequest(BaseModel):
    username: str = Field(..., description="用户名(登录账号)")
    password: str = Field(..., description="登录密码")

# 登录/注册响应体模型(规范返回格式)
class UserAuthResponse(BaseModel):
    code: int = Field(200, description="状态码")
    message: str = Field(..., description="提示信息")
    data: dict = Field(..., description="返回数据,包含Token和用户基础信息")

# 用户信息响应体模型(用于返回用户详情)
class UserInfoResponse(BaseModel):
    id: int
    username: str
    nickname: Optional[str]
    phone: Optional[str]
    avatar: Optional[str]

    # 允许从ORM模型对象直接转换为响应体
    class Config:
        from_attributes = True

五、业务逻辑封装(CRUD操作)

将用户注册、登录、Token管理等业务逻辑封装为CRUD函数,与路由层分离,确保业务逻辑可复用,同时便于后续修改和维护。所有数据库操作均为异步,提升接口响应速度。

from sqlalchemy import select, delete, update
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import HTTPException, status
from models import User, UserToken
from schemas import UserRegisterRequest
from utils import encrypt_password, verify_password, create_access_token, parse_access_token

# 注册业务逻辑:验证用户名/手机号唯一性 → 密码加密 → 创建用户
async def register_user(db: AsyncSession, user_data: UserRegisterRequest):
    # 验证用户名是否已存在
    username_query = select(User).where(User.username == user_data.username)
    username_result = await db.execute(username_query)
    if username_result.scalar_one_or_none():
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="用户名已存在,请更换用户名"
        )
    # 验证手机号(若存在)是否已存在
    if user_data.phone:
        phone_query = select(User).where(User.phone == user_data.phone)
        phone_result = await db.execute(phone_query)
        if phone_result.scalar_one_or_none():
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="手机号已被注册,请更换手机号"
            )
    # 密码加密,创建用户
    hashed_password = encrypt_password(user_data.password)
    new_user = User(
        username=user_data.username,
        password=hashed_password,
        nickname=user_data.nickname,
        phone=user_data.phone
    )
    db.add(new_user)
    await db.commit()
    await db.refresh(new_user)
    # 生成Token
    token = create_access_token(new_user.id)
    # 存储Token到数据库
    await create_user_token(db, new_user.id, token)
    return {"token": token, "user": new_user}

# 登录业务逻辑:验证用户是否存在 → 验证密码 → 生成Token
async def login_user(db: AsyncSession, user_data: UserLoginRequest):
    # 查找用户
    query = select(User).where(User.username == user_data.username)
    result = await db.execute(query)
    user = result.scalar_one_or_none()
    # 验证用户是否存在、密码是否正确
    if not user or not verify_password(user_data.password, user.password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="用户名或密码错误,请重新登录"
        )
    # 生成新Token,删除旧Token(避免一个用户多个有效Token)
    token = create_access_token(user.id)
    await delete_user_token(db, user.id)
    await create_user_token(db, user.id, token)
    return {"token": token, "user": user}

# 创建用户Token(存储到数据库)
async def create_user_token(db: AsyncSession, user_id: int, token: str):
    from datetime import datetime, timedelta
    expire_time = datetime.now() + timedelta(days=7)
    user_token = UserToken(
        user_id=user_id,
        token=token,
        expire_time=expire_time
    )
    db.add(user_token)
    await db.commit()
    return user_token

# 删除用户旧Token(登录时调用,确保唯一有效Token)
async def delete_user_token(db: AsyncSession, user_id: int):
    query = delete(UserToken).where(UserToken.user_id == user_id)
    await db.execute(query)
    await db.commit()

# 根据Token获取用户信息(用于身份验证)
async def get_user_by_token(db: AsyncSession, token: str):
    # 解析Token获取用户ID
    user_id = parse_access_token(token)
    # 查找用户
    query = select(User).where(User.id == user_id)
    result = await db.execute(query)
    user = result.scalar_one_or_none()
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="用户不存在,请重新登录"
        )
    return user

六、模块化路由开发(登录注册接口)

采用模块化路由设计,将登录注册相关接口拆分到独立路由文件,通过APIRouter创建路由实例,再挂载到主应用,使接口结构清晰、易于维护。同时实现身份验证依赖,用于登录后接口的权限控制。

1. 登录注册路由实现

from fastapi import APIRouter, Depends, Header
from sqlalchemy.ext.asyncio import AsyncSession
from schemas import UserRegisterRequest, UserLoginRequest, UserAuthResponse, UserInfoResponse
from crud import register_user, login_user, get_user_by_token
from config.db import get_db

# 创建模块化路由实例
user_router = APIRouter(prefix="/api/user", tags=["用户登录注册"])

# 依赖项:获取请求头中的Token,解析并返回用户信息(用于登录后接口验证)
async def get_current_user(token: str = Header(None), db: AsyncSession = Depends(get_db)):
    if not token:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="未提供Token,请先登录",
            headers={"WWW-Authenticate": "Bearer"},
        )
    return await get_user_by_token(db, token)

# 注册接口
@user_router.post("/register", response_model=UserAuthResponse)
async def user_register(
    user_data: UserRegisterRequest,
    db: AsyncSession = Depends(get_db)
):
    result = await register_user(db, user_data)
    return {
        "code": 200,
        "message": "注册成功",
        "data": {
            "token": result["token"],
            "userInfo": UserInfoResponse.model_validate(result["user"])
        }
    }

# 登录接口
@user_router.post("/login", response_model=UserAuthResponse)
async def user_login(
    user_data: UserLoginRequest,
    db: AsyncSession = Depends(get_db)
):
    result = await login_user(db, user_data)
    return {
        "code": 200,
        "message": "登录成功",
        "data": {
            "token": result["token"],
            "userInfo": UserInfoResponse.model_validate(result["user"])
        }
    }

# 登录后接口示例:获取当前用户信息(需验证Token)
@user_router.get("/info", response_model=UserInfoResponse)
async def get_user_info(current_user: User = Depends(get_current_user)):
    return current_user

2. 主应用挂载路由

在主入口文件中初始化FastAPI应用,挂载模块化路由,配置数据库连接依赖,启动项目即可访问所有登录注册相关接口。

from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from routers.user_router import user_router
from models import Base

# 数据库连接配置(实际开发中建议放在配置文件)
DATABASE_URL = "mysql+asyncmy://root:123456@localhost:3306/fastapi_user?charset=utf8mb4"

# 创建异步数据库引擎和会话工厂
async_engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = async_sessionmaker(bind=async_engine, expire_on_commit=False)

# 数据库会话依赖项(供路由调用)
async def get_db():
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

# 初始化FastAPI应用
app = FastAPI(title="用户登录注册系统", version="1.0.0", description="基于FastAPI的登录注册完整实现")

# 挂载模块化路由
app.include_router(user_router)

# 启动项目:uvicorn main:app --reload

# 备注:首次启动需创建数据库表,可执行以下代码(单独运行一次)
# async def create_tables():
#     async with async_engine.begin() as conn:
#         await conn.run_sync(Base.metadata.create_all)
# import asyncio
# asyncio.run(create_tables())

七、功能测试与注意事项

接口开发完成后,可通过FastAPI自带的接口文档(访问http://localhost:8000/docs)测试所有接口,确保功能正常。同时需注意以下几点,保障系统安全和稳定性:

1. 接口测试要点

  1. 注册测试:验证用户名重复、手机号重复、密码长度不足等场景,确保参数校验生效;

  2. 登录测试:验证用户名不存在、密码错误、正确登录等场景,确保Token正常生成;

  3. 身份验证测试:未携带Token访问/info接口、携带无效Token、携带有效Token,验证权限控制生效。

2. 安全注意事项

  1. 密码安全:始终使用加密存储,禁止存储明文密码,可定期更换加密算法和密钥;

  2. Token安全:SECRET_KEY需使用随机字符串,避免硬编码在代码中,生产环境建议放在环境变量中;

  3. 参数校验:严格校验用户输入的参数(如手机号格式、密码长度),避免恶意请求;

  4. 异常处理:所有业务逻辑均需添加异常捕获,返回清晰的错误提示,避免暴露系统内部信息。

总结

本文通过分层设计,完整实现了FastAPI用户登录注册功能,涵盖数据库模型设计、密码加密、Token生成与验证、接口开发等核心环节,代码规范且可直接用于实际开发。整个实现过程遵循“分层解耦”原则,将业务逻辑与路由层分离,工具函数单独封装,提升了代码的可维护性和复用性。

该方案不仅满足基础的登录注册需求,还通过密码加密、Token验证等机制保障了系统安全,适配前后端分离场景,前端可通过获取的Token,实现后续接口的身份验证,完美解决了用户身份识别的核心问题。