FastAPI 新手紧急避坑:10分钟搞定用户认证4大坑,代码复制即用

42 阅读7分钟

fIGXnPvCM

在做 TodoApp(待办事项)项目时,你是不是有过这种困惑:

“一个用户有多个待办,两个表怎么关联?”

“查询用户的待办时,怎么精准过滤,不拿到别人的数据?”

很多新手会卡在 “多表关联” 这一步,要么数据混乱,要么查询报错,这些坑其实都能避免。

今天就把 “用户表 - 待办表” 的一对多关系设计讲透,附上修正后的代码,直接复制就能跑!

核心需求

接下来,用户表使用 “Users” 表示,待办事项表使用 “Todos” 表示。

需求描述:

  1. 一个用户可以有多个待办事项,一个待办事项只能属于一个用户(即 “一对多” 关系)。
  2. 密码不能明文存储,防止数据库泄露;
  3. 创建用户后,数据要永久保存到数据库;
  4. 用户能通过用户名和密码,进行登录认证。

新手常见错误「重点」:

  1. 外键没设对:Todos 表没关联 Users 表主键,导致 “一个用户多个待办” 变成空谈,数据混乱;
  2. 明文存密码:黑客攻破数据库后,用户账号直接被盗,面临合规风险;
  3. 数据存储错误:创建用户时没调用db.add()db.commit(),数据只停留在内存,重启就丢;
  4. 认证函数错误:数据库存的是hashed_password,但使用 user.password 对比密码,永远认证失败;

数据库中一对多关系与外键关联

如何正确理解一对多关系?

让我们回到基本面。在TodoApp 应用中,关系很简单:

  • 一个用户多个待办事项
  • 一个待办事项 → 只能属于一个用户
01、先明确表结构

Users 表(用户表)

字段名类型说明
id(PK)Integer主键,自增
emailString(unique=True)邮箱,唯一约束
usernameString(unique=True)用户名,唯一约束
first_nameString
last_nameString
hashed_passwordString加密后的密码
is_activeBoolean是否活跃,默认 True
roleString角色(管理员 / 普通用户

Todos 表(待办表)

字段名类型说明
id(PK)Integer主键,自增
titleString待办标题
descriptionString待办描述
priorityInteger优先级(1-5)
completeBoolean是否完成,默认 False
owner(FK)Integer关联 Users 表的 id
02、代码实现

第一步、配置数据库(database.py)

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

# 规范URL格式,数据库名改为todosapp.db(避免混淆)
SQLALCHEMY_DATABASE_URL = 'sqlite:///./todosapp.db'

engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

第二步、定义数据模型(models.py)

from database import Base
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey

# 定义用户表
class Users(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True)
    username = Column(String, unique=True)
    first_name = Column(String)
    last_name = Column(String)
    hashed_password = Column(String)  # 重要:存储哈希值而非明文密码
    is_active = Column(Boolean, default=True)
    role = Column(String)

# 定义待办事项表
class Todos(Base):

    __tablename__ = "todos"

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String)
    description = Column(String)
    priority = Column(Integer)
    complete = Column(Boolean, default=False)
    # 外键关联 Users 表的 id,明确约束
    owner_id = Column(Integer, ForeignKey("users.id"))

关键设计

  1. 使用 owner_id 字段作为外键(Foreign Key)指向用户表的主键
  2. 通过这种关联,每个待办事项明确归属于特定用户
  3. 数据库层级实现数据隔离,更加安全可靠

第三步、创建用户接口(auth.py)

首先,需要加个 CreateUserRequest(BaseModel) 类,主要用来验证 POST 请求接口参数。

from pydantic import BaseModel

class CreateUserRequest(BaseModel):
    username: str
    email: str
    first_name: str
    last_name: str
    password: str
    role: str

添加接口代码。

@router.post("/auth", status_code=status.HTTP_201_CREATED)
async def create_user(create_user_request: CreateUserRequest):
    create_user_model = Users(
        username=create_user_request.username,
        email=create_user_request.email,
        first_name=create_user_request.first_name,
        last_name=create_user_request.last_name,
        role=create_user_request.role,
        hashed_password=create_user_request.password,
        is_active=True
    )
    return create_user_model

注意这里的 hashed_password=create_user_request.password,我们暂时还没有对密码进行加密处理。

密码加密存储

在 Users 表中,我们使用 hashed_password 字段,来存储加密后的用户密码。

为什么要加密存储呢?

核心还是安全!试想一下,如果线上数据库被泄漏出去,密码还是明文存储。这相当于,整个系统用户都在裸奔。这将是多大的事故~

01、安装依赖
# 密码加密依赖
pip install passlib -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install bcrypt==4.0.1 -i https://pypi.tuna.tsinghua.edu.cn/simple  # 固定版本避免兼容问题
02、密码加密(auth.py)

先导入 CryptContext ,并实例化。

from passlib.context import CryptContext

bcrypt_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

修改 create_user 方法,给明文密码加密。

@router.post("/auth", status_code=status.HTTP_201_CREATED)
async def create_user(create_user_request: CreateUserRequest):
    create_user_model = Users(
        username=create_user_request.username,
        email=create_user_request.email,
        first_name=create_user_request.first_name,
        last_name=create_user_request.last_name,
        role=create_user_request.role,
        # 修改:加密处理
        hashed_password=bcrypt_context.hash(
            create_user_request.password),
        is_active=True
    )
    return create_user_model

切回浏览器的 Swagger UI 页面,刷新页面。找到 POST /auth 接口,给 Request body 传入如下内容,点击 “Execute”,执行请求。

{
  "username": "wangerge",
  "email": "wangerge@qq.com",
  "first_name": "wang",
  "last_name": "erge",
  "password": "test1234",
  "role": "admin"
}

API 响应内容如下

{
  "username": "wangerge",
  "email": "wangerge@qq.com",
  "first_name": "wang",
  "last_name": "erge",
  "role": "admin",
  "hashed_password": "$2b$12$Hit2IP1l4REwWnPJDdtZfOpQpxKo8SzWraavGVleAxG19k1YivAqG",
  "is_active": true
}

可以看到 hashed_password 字段已经被加密成一个长字符串了,不再是 “test1234” 明文了。

03、保存用户信息(auth.py)

接下来,我们将获取数据库的连接,并将用户信息保存到数据库。

我们使用 FastAPI 的Depends实现依赖注入,所有接口都可复用。

在 auth.py 中添加如下定义:

from database import SessionLocal
from typing import Annotated
from fastapi import Depends


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


db_dependency = Annotated[Session, Depends(get_db)]

这样,就可以在 create_user 方法中拿到数据库连接了。代码如下:

@router.post("/auth", status_code=status.HTTP_201_CREATED)
async def create_user(db: db_dependency,
                      create_user_request: CreateUserRequest):
    create_user_model = Users(
        username=create_user_request.username,
        email=create_user_request.email,
        first_name=create_user_request.first_name,
        last_name=create_user_request.last_name,
        role=create_user_request.role,
        hashed_password=bcrypt_context.hash(create_user_request.password),
        is_active=True
    )
    db.add(create_user_model)
    db.commit()

切回浏览器 Swagger UI 页面,刷新页面,找到 POST /auth 接口,给 “Request body ”传入如下内容,执行请求。返回响应码 200。

{
  "username": "wangerge",
  "email": "wangerge@163.com",
  "first_name": "wang",
  "last_name": "erge",
  "password": "test1234",
  "role": "admin"
}

如何在数据库中,查询新加的用户信息呢?

我们可以在 sqlite 命令行,使用 sql 语句来查询新创建的用户信息:

(.venv) wangerge_notes: TodoApp$ sqlite3 todosapp.db 
SQLite version 3.37.0 2021-12-09 01:34:53
Enter ".help" for usage hints.
sqlite> select * from users;
1|wangerge@163.com|wangerge|wang|erge|$2b$12$NmS1fzfGgS0BZXO6/9VB.OcVwZ5S/r/tHvAJ5TG83CkpdQSVtHLv6|1|admin
sqlite> 

现在, 我们创建了一个完整的、合格的用户信息在数据库中。

验证用户信息

这里需求是这样的:用户通过 API 接口传入用户名和密码,FastAPI 服务器拿到用户名和密码,并将它与数据库中的用户信息进行比对,最后返回一个验证状态。

首先,需要安装依赖库:

pip install python-multipart -i https://pypi.tuna.tsinghua.edu.cn/simple

安装完成,就可以向应用程序提交表单。这里使用 OAuth2PasswordRequestForm,这是一种更安全的表单。能够从接口处获取请求中的用户名和密码。

添加 POST /token 接口代码:

from fastapi.security import OAuth2PasswordRequestForm

@router.post("/token")
async def login_for_access_token(
    form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
    db: db_dependency):
    user = authenticate_user(form_data.username, form_data.password, db)
    if not user:
        return "Failed Authentication"
    return "Successful Authentication"


def authenticate_user(username: str, password: str, db):
    user = db.query(Users).filter(Users.username == username).first()
    if not user:
        return False
    if not bcrypt_context.verify(password, user.hashed_password):
        return False
    return True

代码中的 authenticate_user 方法,接收用户名、密码、数据库连接。并对密码进行加密验证。

代码中的 login_for_access_token 方法中的 form_data 参数,接收用户提交的表单数据。

测试

切回浏览器 Swagger UI 页面,刷新页面,打开 POST /token 接口。

传入 username="wangerge" password="test1234"(其他参数,先略过),执行请求。返回响应码 200,响应体 "Successful Authentication"。

表示用户名和密码输入正确,接口验证成功。

传入 username="wangerge" password="test"(其他参数,先略过),执行请求。返回响应码 200,响应体 "Failed Authentication"

表示用户名和密码输入错误,接口验证失败。

下期预告

我们将引入 JWT(JSON Web Token)。 它是 FastAPI 生态中「用户认证 + 接口鉴权」的事实标准方案,也是 FastAPI 官方文档中主推的认证方式,更是 99% 的 FastAPI 后端项目必用的核心技术。

我们将就用「通俗语言 + 实战视角」,把 JWT 彻底讲透:是什么、为什么要用、核心优缺点、正确使用姿势、绝对不能踩的坑,所有内容贴合 FastAPI 开发场景,无废话、无冗余,新手也能一次吃透!

想要获取本章完整代码,请在评论区回复 【FastAPI】,代码直接复制就能跑。

关于 FastAPI 的其他疑问

FastAPI极速上手:从API到全栈,高薪后端必备技能

10分钟用Python搭个接口,还能自动生成文档?

3分钟搞定Python虚拟环境和FastAPI安装

FastAPI实战:3步搞定增删改查,你的第一个完整API来了!

别再堆if-else验参数了!FastAPI自带的参数验证器,至少省一半调试时间

3分钟搞定FastAPI的数据库设置,把数据存储玩明白,复制代码就能用

10分钟搞定FastAPI中“数据库连接管理、参数校验、文档维护”三大核心难题,让新手也能轻松地写出可落地的API

“警惕!FastAPI接口一夜「消失」” 95%程序员靠这招自救:我的路由分离血泪史

相关内容我都给大家做好了,感兴趣的朋友来「我的主页」找一找,直接就可以看到。

关注我,每天分享「Python」、「职场」有趣干货,千万不要错过!