FastAPI 从入门到实战

第1章:现代 Python 语法与 HTTP 基础

1.Python 类型提示 (Type Hints)

代码示例:

# 传统写法
def get_full_name(first_name, last_name):
    return first_name.title() + " " + last_name.title()


# 现代写法 (Type Hints)
def get_full_name_typed(first_name: str, last_name: str) -> str:#返回的是字符串
    #编辑器知道 first_name 是 str,会自动提示 .title() 方法
    return first_name.title() + " " + last_name.title()
1.1 复杂类型 (List, Dict, Optional, Union)

内置类型写法(List Dict 小写,这样子就不需要导入包):

a: list[int] = [1, 2, 3]
b: dict[str, int] = {"age": 18}
c: str | None = None          # 等价 Optional[str]
d: int | str = 123            # 等价 Union[int, str](Python 3.10+)
from typing import Optional,List,Dict,Union
# 定义一个处理分数的函数
def process_scores(scores: List[int]) -> Dict[str, float]:
    return {"average": sum(scores) / len(scores)}

# 定义一个允许缺省的搜索函数
def search_item(query: Optional[str] = None) -> Union[str, List[str]]:#返回字符串或者字符串列表
    if query:
        return f"Searching for {query}"
    return ["item1", "item2"]
1.2 Pydantic 基础简介

代码示例

from pydantic import BaseModel
from typing import Optional

class Item(BaseModel):
    name: str
    price: float
    is_offer: bool = False # 可选字段,默认为 False
    
def test(name : str = "World" , price : float = 0.0, is_offer : bool = False) -> Item:
    return Item(name=name, price=price)
  1. 2.Python 异步编程基础

2.3 阻塞等待和非阻塞等待
import asyncio
import time


async def make_coffee():
    print("开始煮咖啡...")
    #time.sleep(3)
    await asyncio.sleep(3)  # 煮咖啡需要 3 秒
    print("咖啡好了!")

async def make_toast():
    print("开始烤面包...")
    await asyncio.sleep(2)  # 烤面包需要 2 秒
    print("面包好了!")

async def main():
    # 同时开始煮咖啡和烤面包
    await asyncio.gather(make_coffee(), make_toast())
    print("早餐准备完毕!")

asyncio.run(main())

第2章:初识 FastAPI

2.1 环境搭建
# 安装 FastAPI 框架
pip install fastapi

# 安装 ASGI 服务器 (Uvicorn)# "uvicorn[standard]" 包含了一些推荐的依赖,如 uvloop (高性能循环)
pip install "uvicorn[standard]"


# 使用清华源安装 FastAPI
pip install fastapi -i https://pypi.tuna.tsinghua.edu.cn/simple/
# 使用清华源安装 Uvicorn
pip install "uvicorn[standard]" -i https://pypi.tuna.tsinghua.edu.cn/simple/
2.3 最简应用:Hello World
from fastapi import FastAPI

# 1. 实例化 FastAPI 对象
app = FastAPI()

# 2. 路径操作装饰器
# 含义:当客户端以 GET 方法访问根路径 "/" 时,运行下方的函数
@app.get("/")
async def read_root() -> dict:
    # 3. 路径操作函数
    # 直接返回字典,FastAPI 会自动转换为 JSON
    return {"message": "Hello World"}
    
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)
2.4 运行与调试
uvicorn main:app --reload
或者
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
2.6 实战练习
from fastapi import FastAPI

   # 1. 实例化 FastAPI 对象
   app = FastAPI()

   # 2. 路径操作装饰器
   # 含义:当客户端以 GET 方法访问根路径 "/" 时,运行下方的函数
   @app.get("/")
   async def read_root() -> dict:
       # 3. 路径操作函数
       # 直接返回字典,FastAPI 会自动转换为 JSON
       return {"message": "Hello World" +  res}

   # 新增 /info 路由
   @app.get("/info")
   def get_info() -> Dict:
       return {
           "name": "Your Name",
           "role": "Student",
           "language": "Python 3.10"
       }
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)   

第 3 章:参数接收与验证

1.2 路径参数
 from fastapi import FastAPI
 
 app = FastAPI()
 
 # 路径参数 item_id 被定义为 int 类型
 @app.get("/items/{item_id}")
 async def read_item(item_id: int):
     # 如果访问 /items/pig, FastAPI 会自动报错, 因为 pig 不是 int
     return {"item_id": item_id, "type": str(type(item_id))}
1.3 查询参数
from typing import Union
from fastapi import FastAPI

app = FastAPI()

@app.get("/users/")# 从哪开始 + 拿多少条
async def read_users(skip: int = 0, limit: int = 10, q: Union[str, None] = None):
    # URL 示例: /users/?skip=20&limit=5&q=admin
    return {"skip": skip, "limit": limit, "q": q}
1.4 核心重点:请求体 与 Pydantic 模型
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

# 1. 定义数据模型
class Item(BaseModel):
    name: str
    price: float
    is_offer: bool = None  # 可选字段,默认为 None
    description: Optional[str] = None# 将模型作为类型提示用于参数
    tax: Optional[float] = None
    price_with_tax: Optional[float] = None
    
@app.post("/items/")
async def create_item(item: Item):# item 此时已经是 Item 类的实例,拥有属性提示
    item_dict = item.model_dump()#这一步就是把复杂的 Pydantic 对象转成简单的字典,以便 FastAPI 自动处理和返回。
    if item.tax:
        price_with_tax = item.price + item.tax
        item_dict.update({"price_with_tax": price_with_tax})
        #item_dict["price_with_tax"] = price_with_tax
    return item_dict
1.5 进阶参数验证
from fastapi import Query, Path, FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class CalculatorInput(BaseModel):
    # Field 用于模型内部校验:必须大于 0,且小于 10000
    num_a: float = Field(..., gt=0, lt=10000, description="第一个数字")
    num_b: float = Field(..., description="第二个数字")

@app.get("/items/{item_id}")
async def read_items(
    # Path 用于路径参数校验:必须大于等于 1
    item_id: int = Path(..., ge=1, title="The ID of the item"),
    # Query 用于查询参数校验:限制最大长度为 50
    q: str | None = Query(None, max_length=50)
):
    return {"item_id": item_id, "q": q}
1.6 实践任务:项目 1-简易计算器 API
from enum import Enum

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Operation(str, Enum):
    add = "+"
    subtract = "-"
    multiply = "*"
    divide = "/"

class CalcRequest(BaseModel):
    a: float
    b: float
    op: Operation

# 使用枚举限制运算符
@app.post("/calculate")
async def calculate(request: CalcRequest):
    if request.op == Operation.divide and request.b == 0:
        return {"error": "除数不能为0"}
    result = 0
    if request.op == Operation.add:
        result = request.a + request.b
    # ... 其他逻辑
    return {"result": result}

第 4 章:响应模型与错误处理

2.1 响应模型

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserIn(BaseModel):
    username: str
    password: str
    email: str
class UserOut(BaseModel):
    username: str
    email: str
# password 字段未在此定义,返回时会被自动过滤
    @app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn):
    # 即使这里返回了包含 password 的对象
    return user

2.3 异常处理

2.3.1 使用 HTTPException
from fastapi import HTTPException

items = {"foo": "The Foo Wrestlers"}

@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item": items[item_id]}
2.3.2 自定义全局异常处理器
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse

# 1. 初始化 FastAPI 应用
app = FastAPI()

# 2. 注册你写的异常处理器
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    """
    自定义 HTTP 异常处理:统一返回格式
    - code: HTTP 状态码
    - message: 异常详情
    - data: 固定为 None(也可根据需求自定义)
    """
    return JSONResponse(
        status_code=exc.status_code,  # 保持原异常的 HTTP 状态码
        content={
            "code": exc.status_code,
            "message": exc.detail,
            "data": None
        },
    )

# 3. 示例接口:主动抛出 HTTPException
@app.get("/items/{item_id}", summary="根据 ID 获取商品")
async def read_item(item_id: int):
    """
    示例场景:
    - 如果 item_id 小于 1,抛出 400 异常(参数错误)
    - 如果 item_id 等于 999,抛出 404 异常(资源不存在)
    - 否则返回正常数据
    """
    if item_id < 1:
        # 主动抛出 HTTPException,会被上面的处理器捕获
        raise HTTPException(status_code=400, detail="商品 ID 不能小于 1")
    if item_id == 999:
        raise HTTPException(status_code=404, detail=f"未找到 ID 为 {item_id} 的商品")
    # 正常响应(也可以自定义格式,和异常格式统一)
    return {
        "code": 200,
        "message": "success",
        "data": {"item_id": item_id, "name": "测试商品", "price": 99.9}
    }

# 4. 启动入口
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)

2.4 实践任务:升级计算器 API

# 1. 定义泛型响应模型
class StandardResponse(BaseModel, Generic[T]):
    code: int = 200
    message: str = "Success"
    data: Optional[T] = None
# 2. 业务逻辑
@app.post("/calculate/v2", response_model=StandardResponse[float])
async def calculate_v2(request: CalcRequest):
    if request.op == Operation.divide and request.b == 0:
        # 优雅地抛出错误
        raise HTTPException(status_code=400, detail="除数不能为零")

    # ... 计算逻辑 ...
    result = ...

    # 返回对象,FastAPI 会自动填充到 StandardResponse 的结构中
    # 注意:如果手动构造 StandardResponse,则不需要 response_model 自动过滤
    return StandardResponse(data=result)

第 5 章:数据库 ORM 集成

1.3 环境搭建与模型定义

1.3.1 安装依赖
# 安装 SQLModel(包含 SQLAlchemy 和 Pydantic)
pip install sqlmodel

# 安装异步 SQLite 驱动
pip install aiosqlite

# 安装异步 Pytest-asyncio 插件
pip install pytest-asyncio

# 如果使用清华源
pip install sqlmodel aiosqlite -i https://pypi.tuna.tsinghua.edu.cn/simple/

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

1.3.2 定义数据模型

from typing import Optional
from sqlmodel import SQLModel, Field

class Book(SQLModel, table=True):
    """
    图书模型
    
    参数说明:
    - table=True: 标记这是一个数据库表模型
    - Field(): SQLModel 专用的字段定义工具
    """
    
    # 主键字段
    id: Optional[int] = Field(
        default=None,           # 新建时不需要提供,数据库自动生成
        primary_key=True,       # 标记为主键
        description="图书 ID"
    )
    
    # 标题字段
    title: str = Field(
        index=True,             # 创建索引,加速查询
        nullable=False,         # 不允许为空
        description="图书标题"
    )
    
    # 作者字段
    author: str = Field(
        description="作者名称"
    )
    
    # 价格字段
    price: float = Field(
        gt=0,                   # 大于 0
        description="图书价格"
    )
    
    # 描述字段(可选)
    description: Optional[str] = Field(
        default=None,
        description="图书描述"
    )

1.4 数据库连接与异步 Session

1.4.2 配置数据库连接
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession,async_sessionmaker
from sqlmodel import SQLModel

# 数据库连接 URL
# sqlite+aiosqlite:/// 表示使用异步 SQLite 驱动
DATABASE_URL = "sqlite+aiosqlite:///./books.db"

# 1. 创建异步引擎
engine = create_async_engine(
    DATABASE_URL,
    echo=True,          # 打印 SQL 语句(开发时有用,生产环境建议关闭)
    future=True         # 启用 SQLAlchemy 2.0 特性
)
# 2. 创建异步 Session 工厂
# expire_on_commit=False: 提交后对象仍然可用
async_session = async_sessionmaker(
    engine,
    class_=AsyncSession, #指定会话类型为异步会话(同步会话是 Session),必须与异步引擎匹配。
    
    # 默认情况下,会话提交(commit)后,查询出来的对象会 “过期”(无法再访问属性),
    # 设为 False 后,提交后对象仍可正常使用(开发更友好)
    expire_on_commit=False #
)

# 3. 初始化数据库表结构
async def init_db():
    """
    创建所有表
    这个函数应该在应用启动时调用一次
    """
    #通过异步引擎开启一个事务连接(engine.begin() 会自动管理事务,退出上下文时提交),
    #conn 是数据库连接实例。
    async with engine.begin() as conn:
        # 如果需要重建表,可以先删除
        # await conn.run_sync(SQLModel.metadata.drop_all)
        
        # 创建所有表(如果不存在)
        await conn.run_sync(SQLModel.metadata.create_all)
# 数据库类型+驱动://[用户名:密码@]主机[:端口]/数据库资源路径?参数

# 网络服务型,需要指定服务器地址
DATABASE_URL = "mysql+aiomysql://root:123456@localhost:3306/books_db?charset=utf8mb4"

# 文件型,直接定位本地文件路径
DATABASE_URL = "sqlite+aiosqlite:///./books.db"

1.5 CRUD 实战

1.5.1 Create(新增)
from sqlmodel import select
from orm.database import async_session
from orm.model import Book

async def create_book(book_data: Book) -> Book:
    """
    创建新图书
    
    参数:
        book_data: Book 对象(不包含 id)
    
    返回:
        包含自动生成 id 的 Book 对象
    """
    async with async_session() as session:
        # 1. 将对象添加到会话
        session.add(book_data)
        
        # 2. 提交事务,写入数据库
        await session.commit()
        
        # 3. 刷新对象,获取数据库生成的 ID
        await session.refresh(book_data)
        
        return book_data
1.5.2 Read (查询)
async def get_books() -> list[Book]:
    """
    查询所有图书
    
    返回:
        Book 对象列表
    """
    async with async_session() as session:
        # 1. 构建 SELECT 语句
        statement = select(Book)
        
        # 2. 执行查询
        result = await session.execute(statement)
        
        # 3. 获取所有结果
        books = result.all()
        
        return books
1.5.3 Update (修改)
async def update_book(book_id: int, new_data: Book) -> Optional[Book]:
    """
    更新图书信息
    
    参数:
        book_id: 要更新的图书 ID
        new_data: 新的图书数据
    
    返回:
        更新后的 Book 对象,如果不存在返回 None
    """
    async with async_session() as session:
        # 1. 查询要更新的图书
        book = await session.get(Book, book_id)
        
        if not book:
            return None
        
        # 2. 更新属性
        book.title = new_data.title
        book.author = new_data.author
        book.price = new_data.price
        
        if new_data.description:
            book.description = new_data.description
        
        # 3. 添加到会话(已 attached 的对象可以省略)
        session.add(book)
        
        # 4. 提交更改
        await session.commit()
        
        # 5. 刷新对象
        await session.refresh(book)
        
        return book
1.5.4 Delete (删除)
async def delete_book(book_id: int) -> bool:
    """
    删除图书
    
    参数:
        book_id: 要删除的图书 ID
    
    返回:
        True 表示删除成功,False 表示图书不存在
    """
    async with async_session() as session:
        # 1. 查询要删除的图书
        book = await session.get(Book, book_id)
        
        if not book:
            return False
        
        # 2. 删除对象
        await session.delete(book)
        
        # 3. 提交事务
        await session.commit()
        
        return True
1.5.5 完整测试代码
import pytest
from orm.database import init_db
from orm.models import Book

@pytest.mark.asyncio
async def test_book_crud_operations():
    """测试 Book 模型的完整 CRUD 操作"""
    
    # ========== 初始化数据库 ==========
    await init_db()
    print("✅ 数据库初始化完成")
    
    # ========== 测试 CREATE ==========
    print("\n--- 测试创建图书 ---")
    book_data = Book(
        title="《Python 编程指南》",
        author="Guido van Rossum",
        price=68.0,
        description="一本关于 Python 编程的权威指南"
    )
    
    created_book = await create_book(book_data)
    print(f"✅ 创建成功: ID={created_book.id}, 标题={created_book.title}")
    assert created_book.id is not None
    
    # ========== 测试 READ(查询所有)==========
    print("\n--- 测试查询所有图书 ---")
    books = await get_books()
    print(f"✅ 共查询到 {len(books)} 本图书")
    assert len(books) > 0
    
    # ========== 测试 READ(按 ID 查询)==========
    print("\n--- 测试按 ID 查询 ---")
    found_book = await get_book_by_id(created_book.id)
    print(f"✅ 查询成功: {found_book.title}")
    assert found_book is not None
    
    # ========== 测试 UPDATE ==========
    print("\n--- 测试更新图书 ---")
    update_data = Book(
        title="《Python 编程指南(第 2 版)》",
        author="Guido van Rossum",
        price=78.0,
        description="更新版的 Python 编程权威指南"
    )
    
    updated_book = await update_book(created_book.id, update_data)
    print(f"✅ 更新成功: 新标题={updated_book.title}, 新价格={updated_book.price}")
    assert updated_book.price == 78.0
    
    # ========== 测试 DELETE ==========
    print("\n--- 测试删除图书 ---")
    delete_result = await delete_book(created_book.id)
    print(f"✅ 删除成功")
    assert delete_result is True
    
    # 验证删除后不存在
    deleted_book = await get_book_by_id(created_book.id)
    assert deleted_book is None
    print("✅ 确认图书已被删除")
    
    print("\n🎉 所有测试通过!")

第 6 章:依赖注入系统

2.2 基本用法:Depends

from fastapi import Depends, FastAPI

app = FastAPI()

# 定义一个依赖项函数
async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}

# 在路径操作中使用依赖
@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons
from fastapi import Depends,Header,FastAPI
app = FastAPI()

# 第一层依赖
async def get_token(token: str = Header(...)):
    return token

# 第二层依赖(依赖于第一层)
async def get_current_user(token: str = Depends(get_token)):
    # 模拟验证 token
    user = "decode_token " +  token
    return user

# 路由函数(依赖于第二层)
@app.get("/me")
async def read_current_user(user: dict = Depends(get_current_user)):
    return user

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8001)

2.3 核心场景:数据库 Session 的生命周期管理

2.3.2 使用 yield 实现依赖
from typing import Generator

# 同步生成器:模拟给资源+清资源
def get_sync_conn() -> Generator[str, None, None]:
    print("1. 创建同步数据库连接(给资源)")
    conn = "同步连接对象"
    yield conn  # 暂停,把连接交给调用方
    print("2. 关闭同步数据库连接(清资源)")  # 再次唤醒才执行

# 原生调用:必须手动写next(),否则代码不执行
gen = get_sync_conn()  # 仅创建生成器对象,无任何打印
res1 = next(gen)       # 第一次唤醒:执行到yield,打印1,拿到连接
print(f"调用方:拿到{res1},开始用...")
# 调用方用完资源,必须再次next()触发清理
try:
    next(gen)          # 第二次唤醒:执行yield后代码,打印2
except StopIteration:  # 执行完必触发此异常,正常现象
    print("end")
from typing import AsyncGenerator
import asyncio

# 异步生成器:模拟给异步资源+清资源(FastAPI最常用)
async def get_async_conn() -> AsyncGenerator[str, None]:#yield产出的值的类型,生成器最终的返回值类型。
    print("1. 创建异步数据库连接(给资源)")
    conn = "异步连接对象"
    yield conn  # 暂停,把连接交给调用方
    print("2. 关闭异步数据库连接(清资源)")  # 再次唤醒才执行

# 异步代码必须在异步函数中运行
async def main():
    gen = get_async_conn()  # 仅创建异步生成器对象,无打印
    res1 = await gen.__anext__()  # 第一次唤醒:await+__anext__()
    print(f"调用方:拿到{res1},开始用...")
    # 再次唤醒触发清理
    try:
        await gen.__anext__()     # 第二次唤醒:执行yield后代码
    except StopAsyncIteration:
        print("end")

# 运行异步主函数
asyncio.run(main())
from fastapi import FastAPI, Depends
from typing import AsyncGenerator

app = FastAPI()

# 异步生成器依赖(FastAPI自动调__anext__())
async def get_async_session() -> AsyncGenerator[str, None]:
    print("1. 创建异步数据库会话(给资源)")
    session = "异步Session对象"
    yield session  # 只需要写:给什么资源
    print("2. 关闭异步数据库会话(清资源)")  # 只需要写:怎么清

# 路由函数:注入依赖,直接用资源
@app.get("/test")
async def test(session: str = Depends(get_async_session)):
    print(f"路由:用{session}处理业务")
    return {"msg": "处理完成", "session": session}#return → 直接结束生成器,啥也不做,啥也不打印

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8001)
from typing import AsyncGenerator
from sqlmodel.ext.asyncio.session import AsyncSession

# 修改这个 DATABASE_URL 
DATABASE_URL = "sqlite+aiosqlite:///./orm/books.db"

# 其他不变 ....

# 新增这个方法
async def get_session() -> AsyncGenerator[AsyncSession, None]:
    """
    数据库会话依赖
    
    使用 yield 确保:
    1. 请求前创建 session
    2. 请求后自动关闭 session
    3. 异常时也能正确清理
    """
    async with async_session() as session:
        yield session  # 暂停执行,把session交给路由函数
        # 路由函数用完后,才会执行下面的代码(如果有的话)
2.3.4 在路由中使用
from fastapi import FastAPI, Depends
from sqlmodel.ext.asyncio.session import AsyncSession

from orm.models import Book
from orm.database import get_session

app = FastAPI()

@app.post("/books/")
async def create_book_api(
    book: Book,
    # 注入 Session!
    session: AsyncSession = Depends(get_session)
):
    session.add(book)
    await session.commit()
    await session.refresh(book)
    return book

2.4 FastAPI 模块化路由神器:APIRouter

2.4.2 APIRouter 核心三步骤
# 从 fastapi 中导入 APIRouter
from fastapi import APIRouter

# 创建一个名为 book_router 的子路由对象
# 可以把它想象成一个“书籍管理”专属书架
book_router = APIRouter()
# 1. 新增书籍接口(POST 请求)
@book_router.post("/books/")
async def create_book(title: str, author: str) -> Dict:
    """新增一本图书"""
    return {"message": "书籍创建成功", "data": {"title": title, "author": author}}

# 2. 查询所有书籍接口(GET 请求)
@book_router.get("/books/")
async def get_all_books() -> Dict:
    """查询图书馆里所有的书"""
    # 实际项目中这里会从数据库查询
    return {"message": "查询成功", "data": [{"id": 1, "title": "Python入门", "author": "张三"}]}

# 3. 查询单本书籍接口(GET 请求,带路径参数)
@book_router.get("/books/{book_id}")
async def get_book(book_id: int) -> Dict:
    """根据ID查询一本书"""
    return {"message": "查询成功", "data": {"id": book_id, "title": "特定书籍", "author": "李四"}}
# main.py (主应用文件)
from fastapi import FastAPI
# 从我们定义的路由模块中导入子路由
from routers.book import book_router  # 假设子路由写在 routers/book.py 文件中
from typing import Dict

# 1. 创建主应用实例(图书馆管理员)
app = FastAPI()

# 2. 将子路由(书籍书架)注册到主应用中
# 这一步告诉管理员:“我有一个管理书籍的书架,请把它加入图书馆!”
app.include_router(book_router)

# 主应用也可以有自己的根路径接口
@app.get("/")
async def read_root():
    return {"message": "欢迎来到我的图书馆!"}

# 运行服务器
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)
2.4.3 完整项目结构示例

routers/book.py 文件内容:

from typing import Dict

from fastapi import APIRouter

# 步骤1: 创建子路由
book_router = APIRouter()

# 步骤2: 定义接口
# 1. 新增书籍接口(POST 请求)
@book_router.post("/books/")
async def create_book(title: str, author: str) -> Dict:
    """新增一本图书"""
    return {"message": "书籍创建成功", "data": {"title": title, "author": author}}

# 2. 查询所有书籍接口(GET 请求)
@book_router.get("/books/")
async def get_all_books() -> Dict:
    """查询图书馆里所有的书"""
    # 实际项目中这里会从数据库查询
    return {"message": "查询成功", "data": [{"id": 1, "title": "Python入门", "author": "张三"}]}

# 3. 查询单本书籍接口(GET 请求,带路径参数)
@book_router.get("/books/{book_id}")
async def get_book(book_id: int) -> Dict:
    """根据ID查询一本书"""
    return {"message": "查询成功", "data": {"id": book_id, "title": "特定书籍", "author": "李四"}}

main.py 文件内容:

from fastapi import FastAPI
from routers.book import book_router
from typing import Dict

app = FastAPI()

# 步骤3: 注册子路由
app.include_router(book_router)

@app.get("/")
async def read_root() -> Dict:
    return {"message": "欢迎来到我的图书馆!"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)
2.4.4 APIRouter 的高级玩法:让路由更强大

修改 routers/book.py

from fastapi import APIRouter

# 创建子路由时,指定统一的路径前缀 /books
book_router = APIRouter(prefix="/books")

# 定义接口时,路径就可以省略前缀了!
@book_router.post("/")  # 实际访问路径是: POST /books/
async def create_book(title: str, author: str):
    return {"message": "书籍创建成功", "data": {"title": title, "author": author}}

@book_router.get("/")   # 实际访问路径是: GET /books/
async def get_all_books():
    return {"message": "查询成功", "data": [{"id": 1, "title": "Python入门", "author": "张三"}]}

@book_router.get("/{book_id}") # 实际访问路径是: GET /books/{book_id}
async def get_book(book_id: int):
    return {"message": "查询成功", "data": {"id": book_id, "title": "特定书籍", "author": "李四"}}

修改 routers/book.py

from fastapi import APIRouter

# 创建子路由时,指定标签
book_router = APIRouter(prefix="/books", tags=["书籍管理"])

公共依赖(dependencies)

from fastapi import APIRouter, Depends, HTTPException
from fastapi import HTTPException
# 模拟一个登录验证依赖
async def verify_token(token: str):
    if token != "my-secret-token":
        raise HTTPException(status_code=401, detail="未授权的访问")
    return True

# 为整个书籍路由模块添加公共依赖:所有接口都需要验证token
book_router = APIRouter(
    prefix="/books",
    tags=["书籍管理"],
    dependencies=[Depends(verify_token)] # 所有接口都会先执行这个依赖
)

@book_router.get("/")
async def get_all_books():
    # 这个接口会自动应用 verify_token 依赖
    return {"message": "查询成功", "data": []}

2.5 企业级衔接:架构分层

重构后的 Service 层 (services/book_crud.py):

from sqlalchemy.ext.asyncio import AsyncSession
from orm import Book

async def create_book(session: AsyncSession, book: Book):# 纯粹的业务逻辑,不依赖 HTTP 请求上下文
    session.add(book)
    await session.commit()
    await session.refresh(book)
    return book

重构后的 Router 层 (routers/books.py):

@router.post("/", response_model=Book)
async def create_book_endpoint(
    book: Book, 
    session: AsyncSession = Depends(get_session)
):
    # 路由只负责“指挥”
    return await book_crud.create_book(session, book)

第7章:用户认证与安全性

1.1 密码安全:不要明文存储密码

1.1.3 代码实现

工具类封装 utils.py

import bcrypt  # 导入 bcrypt 加密库

def get_password_hash(password: str) -> str:
    """
    功能:将明文密码加密成哈希值(密文)
    :param password: 传入的明文密码(如用户注册时输入的 123456)
    :return: 加密后的哈希字符串(可直接存入数据库)
    """
    # 1. 将字符串密码转换为字节类型(bcrypt 库只支持字节流处理)
    password_bytes = password.encode('utf-8')
    
    # 2. 生成随机盐值(默认加密难度)
    salt = bcrypt.gensalt()
    
    # 3. 核心加密:将密码和盐值拼接后哈希,生成密文(字节类型)
    hashed_password_bytes = bcrypt.hashpw(password_bytes, salt)
    
    # 4. 将字节类型的密文转换为字符串,方便存入数据库
    return hashed_password_bytes.decode('utf-8')

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """
    功能:验证用户输入的明文密码,与数据库中的密文是否匹配
    :param plain_password: 用户登录时输入的明文密码
    :param hashed_password: 从数据库中取出的密文(哈希值)
    :return: 匹配返回 True,不匹配返回 False
    """
    # 1. 将明文密码和密文都转换为字节类型
    plain_pwd_bytes = plain_password.encode('utf-8')
    hashed_pwd_bytes = hashed_password.encode('utf-8')
    
    # 2. 核心验证:bcrypt 会自动从密文中提取盐值,与明文密码重新哈希对比
    # 无需手动处理盐值,直接返回对比结果(True/False)
    return bcrypt.checkpw(plain_pwd_bytes, hashed_pwd_bytes)

# ------------------- 测试代码 -------------------
if __name__ == "__main__":
    # 1. 模拟用户注册:加密密码
    original_password = "my_secure_password"  # 用户设置的明文密码
    hashed_pwd = get_password_hash(original_password)
    print(f"加密后的密文:{hashed_pwd}")  # 输出类似:$2b$12$EixZaYb4xU58Gpq1R0yWbeb00LU5qUaK6x6h9s6Q0hW8XQd6cR5u
    
    # 2. 模拟用户登录:验证密码
    # 正确密码验证
    is_correct = verify_password(original_password, hashed_pwd)
    print(f"密码验证结果(正确密码):{is_correct}")  # 输出:True
    
    # 错误密码验证
    wrong_password = "wrong_password"
    is_wrong = verify_password(wrong_password, hashed_pwd)
    print(f"密码验证结果(错误密码):{is_wrong}")  # 输出:False

1.2 JWT (JSON Web Token) 详解

1.2.4 生成 Token 代码示例

安装依赖:

pip install python-jose[cryptography]
# 注:[cryptography] 是补充加密依赖,避免后续运行报错

utils/security.py

# 导入时间相关模块:处理令牌过期时间
from datetime import datetime, timedelta, timezone
# 导入jwt核心库:生成JWT令牌
from jose import jwt

# 1. 配置项(项目中建议单独放在配置文件里)
SECRET_KEY = "YOUR_SUPER_SECRET_KEY"  # 服务器专属密钥,生产环境绝对不能硬编码!
ALGORITHM = "HS256"  # 生成JWT的加密算法,本次用对称加密HS256
ACCESS_TOKEN_EXPIRE_MINUTES = 30  # 令牌有效期,30分钟后自动失效


def create_access_token(data: dict):
    """
    核心功能:生成JWT访问令牌
    :param data: 要存入令牌的用户信息(字典类型,如{"user_id": 1, "username": "zhangsan"})
    :return: 生成的JWT令牌字符串
    """
    # 1. 复制传入的用户信息,避免修改原字典(开发规范,防止副作用)
    to_encode = data.copy()

    # 2. 计算令牌的过期时间:当前UTC时间 + 有效期(30分钟)
    # 用timezone.utc避免本地时间时区问题,保证跨服务器/跨地区时间一致
    expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)

    # 3. 给要加密的字典,添加过期时间(key固定为"exp",JWT规范要求)
    to_encode.update({"exp": expire})

    # 4. 生成JWT令牌核心步骤
    # 参数说明:要加密的字典 + 服务器密钥 + 加密算法
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

    # 5. 返回生成的令牌字符串,给前端使用
    return encoded_jwt


# ------------------- 测试代码 -------------------
if __name__ == "__main__":
    # 模拟用户登录成功,获取的用户信息(仅存非敏感信息!)
    user_info = {"user_id": 1, "username": "zhangsan"}
    # 生成JWT令牌
    token = create_access_token(user_info)
    print("生成的JWT令牌:")
    print(token)

1.3 路由保护与依赖注入

1.3.3 实现 get_current_user
# 导入FastAPI核心组件:依赖注入、异常抛出、状态码
from fastapi import Depends, HTTPException, status
# 导入FastAPI的OAuth2密码模式工具:自动提取Bearer Token
from fastapi.security import OAuth2PasswordBearer
# 导入jwt库:解析/验证JWT令牌,捕获令牌相关异常
from jose import JWTError, jwt

# 1. 实例化OAuth2PasswordBearer,指定令牌获取接口地址
# 作用:告诉FastAPI自动从请求头提取Bearer Token,提取失败直接返回401
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")  # tokenUrl为前端登录获取令牌的接口(如/token)

# 2. 配置项(与上面jwt令牌生成的配置完全一致,必须统一!)
SECRET_KEY = "YOUR_SUPER_SECRET_KEY"  # 服务器专属密钥,生产环境严禁硬编码
ALGORITHM = "HS256"  # 验证算法必须与生成令牌的算法一致
ACCESS_TOKEN_EXPIRE_MINUTES = 30  # 与生成令牌的有效期一致(仅作配置,本次代码未直接使用)

async def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
    """
    核心功能:FastAPI依赖注入函数,验证JWT令牌并解析当前用户信息
    :param token: 由Depends(oauth2_scheme)自动注入的JWT令牌字符串
    :return: 解析出的用户信息字典(真实场景返回数据库查询的用户对象)
    :raise: 令牌无效/解析失败时,抛出401未授权异常
    """
    # 定义标准化的401异常:令牌验证失败时统一抛出此异常
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,  # 401未授权状态码
        detail="无法验证凭据",  # 错误提示信息
        headers={"WWW-Authenticate": "Bearer"},  # 告诉前端需要携带Bearer Token
    )
    try:
        # 核心步骤1:解析JWT令牌,验证其有效性
        # 参数说明:令牌字符串 + 服务器密钥 + 允许的加密算法(与生成时一致)
        # 验证逻辑:自动检查签名是否被篡改、令牌是否过期、算法是否匹配
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        
        # 核心步骤2:从令牌的载荷(payload)中提取用户名(key为"sub",JWT规范字段)
        # "sub"是JWT通用规范字段,代表“主题”,通常存储用户唯一标识(用户名/用户ID)
        username: str = payload.get("sub")
        
        # 校验:如果载荷中没有用户名,说明令牌无效,抛出401异常
        if username is None:
            raise credentials_exception

    # 捕获所有JWT相关异常:令牌篡改、过期、密钥错误、格式错误等
    except JWTError:
        raise credentials_exception

    # 打印当前用户(调试用),返回用户信息(供路由函数使用)
    print("当前访问用户:", username)
    return {"username": username}  # 真实场景返回查询到的user对象

main.py函数中完整的代码如下:

# 导入时间相关模块:处理令牌过期时间
from datetime import datetime, timedelta, timezone

from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
# 导入jwt核心库:生成JWT令牌
from jose import jwt, JWTError
from starlette import status

# 1. 配置项(项目中建议单独放在配置文件里)
SECRET_KEY = "YOUR_SUPER_SECRET_KEY"  # 服务器专属密钥,生产环境绝对不能硬编码!
ALGORITHM = "HS256"  # 生成JWT的加密算法,本次用对称加密HS256
ACCESS_TOKEN_EXPIRE_MINUTES = 30  # 令牌有效期,30分钟后自动失效

def create_access_token(data: dict):
    """
    核心功能:生成JWT访问令牌
    :param data: 要存入令牌的用户信息(字典类型,如{"user_id": 1, "username": "zhangsan"})
    :return: 生成的JWT令牌字符串
    """
    # 1. 复制传入的用户信息,避免修改原字典(开发规范,防止副作用)
    to_encode = data.copy()

    # 2. 计算令牌的过期时间:当前UTC时间 + 有效期(30分钟)
    # 用timezone.utc避免本地时间时区问题,保证跨服务器/跨地区时间一致
    expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)

    # 3. 给要加密的字典,添加过期时间(key固定为"exp",JWT规范要求)
    to_encode.update({"exp": expire})

    # 4. 生成JWT令牌核心步骤
    # 参数说明:要加密的字典 + 服务器密钥 + 加密算法
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

    # 5. 返回生成的令牌字符串,给前端使用
    return encoded_jwt


# 1. 实例化OAuth2PasswordBearer,指定令牌获取接口地址
# 作用:告诉FastAPI自动从请求头提取Bearer Token,提取失败直接返回401
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")  # tokenUrl为前端登录获取令牌的接口(如/token)

async def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
    """
    核心功能:FastAPI依赖注入函数,验证JWT令牌并解析当前用户信息
    :param token: 由Depends(oauth2_scheme)自动注入的JWT令牌字符串
    :return: 解析出的用户信息字典(真实场景返回数据库查询的用户对象)
    :raise: 令牌无效/解析失败时,抛出401未授权异常
    """
    # 定义标准化的401异常:令牌验证失败时统一抛出此异常
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,  # 401未授权状态码
        detail="无法验证凭据",  # 错误提示信息
        headers={"WWW-Authenticate": "Bearer"},  # 告诉前端需要携带Bearer Token
    )
    try:
        # 核心步骤1:解析JWT令牌,验证其有效性
        # 参数说明:令牌字符串 + 服务器密钥 + 允许的加密算法(与生成时一致)
        # 验证逻辑:自动检查签名是否被篡改、令牌是否过期、算法是否匹配
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])

        # 核心步骤2:从令牌的载荷(payload)中提取用户名(key为"sub",JWT规范字段)
        # "sub"是JWT通用规范字段,代表“主题”,通常存储用户唯一标识(用户名/用户ID)
        username: str = payload.get("sub")

        # 校验:如果载荷中没有用户名,说明令牌无效,抛出401异常
        if username is None:
            raise credentials_exception

    # 捕获所有JWT相关异常:令牌篡改、过期、密钥错误、格式错误等
    except JWTError:
        raise credentials_exception

    # 打印当前用户(调试用),返回用户信息(供路由函数使用)
    print("当前访问用户:", username)
    return {"username": username}  # 真实场景返回查询到的user对象

# ------------------- 测试代码 -------------------
if __name__ == "__main__":
    # 模拟用户登录成功,获取的用户信息(仅存非敏感信息!)
    user_info = {"user_id": 1, "username": "zhangsan"}

    # 生成JWT令牌
    token = create_access_token(user_info)
    print("生成的JWT令牌:")
    print(token)
1.3.4 模拟完整demo代码流程

这里测试需要借助 APIfoxs 这个软件进行测试,不然无法填充Header 中的 Authenticate

# main.py
from fastapi import FastAPI, Depends
from fastapi.security import OAuth2PasswordRequestForm
from utils.security import create_access_token, get_current_user

# 创建FastAPI实例
app = FastAPI(title="JWT令牌验证示例", version="1.0")

# ==================== 模拟用户数据库(真实场景替换为数据库查询) ====================
fake_users_db = {
    "zhangsan": {
        "user_id": 1,
        "username": "zhangsan",
        "password": "123456"  # 生产环境务必加密存储(如bcrypt)
    },
    "lisi": {
        "user_id": 2,
        "username": "lisi",
        "password": "654321"
    }
}


# ==================== 登录接口(生成令牌) ====================
@app.post("/token", summary="用户登录,获取JWT令牌")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    """
    接收用户名密码,验证通过后返回JWT令牌
    OAuth2PasswordRequestForm会自动解析表单格式的用户名(username)和密码(password)
    """
    # 1. 验证用户是否存在
    user = fake_users_db.get(form_data.username)
    if not user:
        return {"code": 400, "message": "用户名不存在"}

    # 2. 验证密码(真实场景用加密算法验证,如bcrypt.checkpw)
    if form_data.password != user["password"]:
        return {"code": 400, "message": "密码错误"}

    # 3. 生成JWT令牌(必须包含"sub"字段,值为用户名)
    access_token = create_access_token(
        data={"sub": user["username"], "user_id": user["user_id"]}
    )

    # 4. 返回令牌(符合OAuth2规范,key为access_token和token_type)
    return {
        "code": 200,
        "message": "登录成功",
        "data": {
            "access_token": access_token,
            "token_type": "bearer",  # 固定为bearer,前端请求时需拼接为 Bearer <token>
            "expires_in": 30 * 60  # 过期时间(秒)
        }
    }


# ==================== 受保护的接口(需要验证令牌) ====================
@app.get("/protected", summary="受保护的接口(需携带有效JWT令牌)")
async def protected_route(current_user: dict = Depends(get_current_user)):
    """
    依赖get_current_user函数,只有令牌有效时才能访问
    current_user会自动接收解析后的用户信息
    """
    return {
        "code": 200,
        "message": "访问受保护接口成功",
        "data": {
            "current_user": current_user,
            "content": "这是只有登录用户才能看到的内容"
        }
    }

# ==================== 主函数(运行服务) ====================
if __name__ == "__main__":
    import uvicorn

    # 启动服务:地址0.0.0.0,端口8000,自动重载
    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)

实践任务: 构建私密笔记本认证系统

"""
私密笔记本 —— FastAPI 用户认证系统
运行方式: uvicorn main:app --reload
测试方式: 访问 http://127.0.0.1:8000/docs 使用 Swagger UI 测试
"""

import bcrypt
from datetime import datetime, timedelta, timezone
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import jwt, JWTError
from pydantic import BaseModel

# ===================== 配置 =====================
SECRET_KEY = "my-notebook-secret-key-2024"   # 生产环境应从环境变量读取
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# ===================== 应用初始化 =====================
app = FastAPI(title="私密笔记本")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# ===================== 模拟数据库 =====================
users_db: dict = {}       # {"alice": {"username": "alice", "hashed_password": "..."}}
notes_db: list = []        # [{"id": 1, "owner": "alice", "title": "...", "content": "..."}]
note_id_counter = 0

# ===================== 数据模型 =====================
class UserRegister(BaseModel):
    username: str
    password: str

class NoteCreate(BaseModel):
    title: str
    content: str

# ===================== 工具函数 =====================

def hash_password(password: str) -> str:
    """将明文密码进行 bcrypt 哈希加密"""
    password_bytes = password.encode("utf-8")      # 字符串转字节(bcrypt 要求)
    salt = bcrypt.gensalt()                         # 生成随机盐值
    hashed = bcrypt.hashpw(password_bytes, salt)    # 哈希加密
    return hashed.decode("utf-8")                   # 字节转字符串,方便存储


def verify_password(plain_password: str, hashed_password: str) -> bool:
    """验证明文密码是否与数据库中的哈希值匹配"""
    return bcrypt.checkpw(
        plain_password.encode("utf-8"),             # bcrypt 自动从哈希值中提取盐
        hashed_password.encode("utf-8"),            # 无需手动管理盐值
    )


def create_access_token(data: dict) -> str:
    """生成 JWT 访问令牌"""
    to_encode = data.copy()                         # 复制字典,避免修改原数据
    expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})               # 添加过期时间(JWT 规范字段)
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)


# ===================== 核心依赖:获取当前用户 =====================

async def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
    """
    路由保护的核心依赖函数:
    1. Depends(oauth2_scheme) 自动从请求头 Authorization: Bearer <token> 中提取令牌
    2. 用密钥解析令牌,验证签名和过期时间
    3. 从载荷中取出用户名,查询数据库返回用户信息
    """
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="令牌无效或已过期",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    user = users_db.get(username)
    if user is None:
        raise credentials_exception
    return user


# ===================== 接口路由 =====================

@app.post("/register")
def register(user: UserRegister):
    """用户注册:密码经 bcrypt 哈希后存储"""
    if user.username in users_db:
        raise HTTPException(status_code=400, detail="用户名已存在")

    users_db[user.username] = {
        "username": user.username,
        "hashed_password": hash_password(user.password),  # 绝不存储明文密码
    }
    return {"message": "注册成功", "username": user.username}


@app.post("/token")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
    """用户登录:验证密码,返回 JWT 令牌"""
    user = users_db.get(form_data.username)
    if not user or not verify_password(form_data.password, user["hashed_password"]):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="用户名或密码错误",
            headers={"WWW-Authenticate": "Bearer"},
        )
    # 令牌载荷只放用户标识,不放密码等敏感信息(Payload 是 Base64 编码,可被解码查看)
    access_token = create_access_token(data={"sub": user["username"]})
    return {"access_token": access_token, "token_type": "bearer"}


@app.post("/notes")
def create_note(note: NoteCreate, current_user: dict = Depends(get_current_user)):
    """创建笔记(需登录),笔记自动绑定当前用户"""
    global note_id_counter
    note_id_counter += 1
    new_note = {
        "id": note_id_counter,
        "owner": current_user["username"],
        "title": note.title,
        "content": note.content,
    }
    notes_db.append(new_note)
    return {"message": "笔记创建成功", "note": new_note}


@app.get("/notes")
def get_my_notes(current_user: dict = Depends(get_current_user)):
    """查看当前用户的笔记列表(需登录),只能看到自己的笔记"""
    my_notes = [n for n in notes_db if n["owner"] == current_user["username"]]
    return {"username": current_user["username"], "notes": my_notes}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值