用户登录注册是绝大多数后端系统的基础功能,其核心需求是保障用户身份合法性、密码安全性,以及登录后的身份验证。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. 接口测试要点
-
注册测试:验证用户名重复、手机号重复、密码长度不足等场景,确保参数校验生效;
-
登录测试:验证用户名不存在、密码错误、正确登录等场景,确保Token正常生成;
-
身份验证测试:未携带Token访问/info接口、携带无效Token、携带有效Token,验证权限控制生效。
2. 安全注意事项
-
密码安全:始终使用加密存储,禁止存储明文密码,可定期更换加密算法和密钥;
-
Token安全:SECRET_KEY需使用随机字符串,避免硬编码在代码中,生产环境建议放在环境变量中;
-
参数校验:严格校验用户输入的参数(如手机号格式、密码长度),避免恶意请求;
-
异常处理:所有业务逻辑均需添加异常捕获,返回清晰的错误提示,避免暴露系统内部信息。
总结
本文通过分层设计,完整实现了FastAPI用户登录注册功能,涵盖数据库模型设计、密码加密、Token生成与验证、接口开发等核心环节,代码规范且可直接用于实际开发。整个实现过程遵循“分层解耦”原则,将业务逻辑与路由层分离,工具函数单独封装,提升了代码的可维护性和复用性。
该方案不仅满足基础的登录注册需求,还通过密码加密、Token验证等机制保障了系统安全,适配前后端分离场景,前端可通过获取的Token,实现后续接口的身份验证,完美解决了用户身份识别的核心问题。