fastapi

1. 快速demo

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Hello World"}

if __name__ == "__main__":
    import uvicorn

    uvicorn.run("app:app", host="0.0.0.0", port=8000,reload=True)

(1)导入 FastAPI。
(2)创建一个 app 实例。
(3)编写一个路径操作装饰器(如 @app.get(“/”))。
(4)编写一个路径操作函数(如上面的 def root(): …)
(5)定义返回值
(6)运行开发服务器

fastapi有交互式 的API 文档,跳转到 http://127.0.0.1:8000/docs。你将会看到自动生成的交互式 API 文档。

测试demo

from fastapi.testclient import TestClient
from fastapi.websockets import WebSocket
from app import app 

client = TestClient(app)

# 模拟 GET
def test_get()
    response = client.get("/items/", params={"id": "123"})
    print(response.json())

# 模拟 Post
def test_login():
    response = client.post("/users/", json={"username": "charlie", "password": "xyz"})
    print(response.json())

# websocket
def test_websocket():
    client = TestClient(app)
    with client.websocket_connect("/ws") as websocket:
        data = websocket.receive_json()
        assert data == {"msg": "Hello WebSocket"}

2. 路由请求

2.1路径参数

2.1.1基本用法

@app.get("/user/{user_id}") 
def get_user(user_id:int): 
    return {"user_id": user_id}

路径参数 user_id 的值将作为参数 user_id 传递给你的函数,比如使用http://127.0.0.1:8000/user/123,user_id就会自动赋值成123

注意:fastapi会对类型做检查,如果输入/user/ab,则会报错,因为ab不是int类型

    {
        "detail": [
            {
                "type": "int_parsing",
                "loc": [
                    "path",
                    "user_id"
                ],
                "msg": "Input should be a valid integer, unable to parse string as an integer",
                "input": "ab"
            }
        ]
    }

如果存在两个路由,则按照代码书写顺序从上到下进行匹配,前者会把后者进行覆盖。

@app.get("/user/me")
@app.get("/user/{user_id}")

2.1.2预定义enum

如果希望有效路径参数值是预定义的,可以使用Enum。

from enum import Enum

from fastapi import FastAPI


class ModelName(str, Enum):
    alexnet = "alexnet"
    resnet = "resnet"
    lenet = "lenet"


app = FastAPI()


@app.get("/models/{model_name}")
async def get_model(model_name: ModelName):
    if model_name is ModelName.alexnet:
        return 1
    if model_name.value == "lenet":
        return 2
    return 3

2.1.3含有path的路由

你可能需要参数包含 /home/johndoe/myfile.txt,带有一个前导斜杠(/)。

在这种情况下,参数的名称是 file_path,最后一部分 :path 告诉它该参数应该匹配任何路径。

@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
    return {"file_path": file_path}

此时URL 将是:/files//home/johndoe/myfile.txt,在 files 和 home 之间有一个双斜杠(//)。

2.2查询参数

查询参数是 URL 中 ? 之后,用 & 字符分隔的键值对集合

比如http://127.0.0.1:8000/items/?skip=0&limit=10

@app.get("/items/{item_id}")
async def read_item(item_id: str, skip: int,limit: int | None = None):
    return {"item_id": item_id, "skip": skip}

FastAPI 足够智能,能识别出路径参数 item_id 是路径参数而 skip 是查询参数

当非路径参数声明默认值时,它不是必需要传递的,比如这里的limit。

2.3请求体

要声明请求体,可以使用 Pydantic模型

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None

@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, q: str | None = None):
    result = {"item_id": item_id, **item.dict(),"q": q}
    return result

仅仅通过这个 Python 类型声明,FastAPI 将会

  1. 将请求体读取为 JSON。

  2. 转换相应的类型(如果需要)。

  3. 验证数据。如果数据无效,它将返回一个清晰的错误,精确指出错误数据的位置和内容。

  4. 将接收到的数据提供给参数item

  5. 由于你在函数中将其声明为 Item 类型,你将获得所有属性及其类型的所有编辑器支持(补全等)。

函数参数将被识别为如下:

  • 如果参数也在路径中声明,它将被用作路径参数。
  • 如果参数是单一类型(如 int、float、str、bool 等),它将被解释为查询参数。
  • 如果参数声明为 Pydantic 模型的类型,它将被解释为请求体。

多个请求体参数

FastAPI 会注意到函数中有多个请求体参数,比如

{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    },
    "user": {
        "username": "dave",
        "full_name": "Dave Grohl"
    }
}

则可以这样拆分

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None


class User(BaseModel):
    username: str
    full_name: str | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, user: User):
    results = {"item_id": item_id, "item": item, "user": user}
    return results

如果请求体中还有单一值,可以参考Body参数

3. 数据模型和验证

3.1Query

Query 是一个声明参数约束和元数据的特殊函数,它既是语法糖,又是一个提示给 FastAPI 的机制,让框架知道“这个参数应该来自查询字符串(query string)”。可以把它看作是 参数描述器,用来定义:

  1. 默认值
  2. 校验规则(比如长度、数值范围、正则等)
  3. 元信息(例如文档描述、示例值)

核心理念:它并不是运行时的业务逻辑对象,而是声明式的配置,FastAPI 解析后会交给 Pydantic 做数据验证,并在生成 OpenAPI 文档时展示出来。

基本形式

from fastapi import Query

@app.get("/items/")
async def read_items(q: str = Query(default="default", min_length=3, max_length=10)):
    return {"q": q}

当访问 /items/?q=hello 时:

  • q 会自动从 URL 查询字符串中提取,比如 ?q=hello
  • FastAPI 会校验 q 是否符合 min_length 和 max_length 的规则
  • 如果校验失败,自动返回 HTTP 422 错误和详细的 JSON 错误信息

这里的 Query 实际上返回的是一个 特殊标记对象,告诉 FastAPI:

这个参数应该从 query string 中取值,并应用这些规则。

主要参数

参数 作用
default 默认值;设为 ... 表示必填
alias 定义别名,比如 URL 用 item-id,代码中用 item_id
title / description 给自动生成的文档添加标题和描述
min_length / max_length 字符串长度限制
ge / le / gt / lt 数值范围限制
regex 正则表达式验证
deprecated 标记参数已废弃,文档会有提示
example / examples 给文档生成示例

Annotated写法

from typing import Annotated
from fastapi import Query

@app.get("/items/")
async def read_items(q: Annotated[str | None, Query(max_length=50)] = None):
    return {"q": q}

当在 Annotated 内部使用 Query 时,不能为使用 default 参数。下面是错误的

q: Annotated[str, Query(default="rick")] = "morty"

查询参数列表

使用类似urlhttps://:8000/items/?q=foo&q=bar

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/items/")
async def read_items(q: Annotated[list[str] | None, Query()] = None):
    query_items = {"q": q}
    return query_items

此时响应是

{  "q": [    "foo",    "bar"  ] }

别名参数

如果希望使用这种urlhttp://127.0.0.1:8000/items/?item-query=foobaritems,但是 item-query 不是一个有效的 Python 变量名。此时可以使用alias参数

@app.get("/items/")
async def read_items(q: Annotated[str | None, Query(alias="item-query")] = None):
    ...

自定义验证

如果需要进行一些自定义验证,可以使用一个自定义验证器函数,该函数在正常验证之后应用(例如,在验证值是 str 之后)。

或者使用 Pydantic 的 AfterValidator

from typing import Annotated
from pydantic import AfterValidator

data = {
    "isbn-9781529046137": "The Hitchhiker's Guide to the Galaxy",
    "imdb-tt0371724": "The Hitchhiker's Guide to the Galaxy",
    "isbn-9781439512982": "Isaac Asimov: The Complete Stories, Vol. 2",
}


def check_valid_id(id: str):
    if not id.startswith(("isbn-", "imdb-")):
        raise ValueError('Invalid ID format, it must start with "isbn-" or "imdb-"')
    return id


@app.get("/items/")
async def read_items(
    id: Annotated[str | None, AfterValidator(check_valid_id)] = None,
):
    return {"id": id, "name": data[id]}

查询参数模型

如果您有一组相关的查询参数,您可以创建一个 Pydantic 模型来声明它们。

from typing import Annotated, Literal

from fastapi import FastAPI, Query
from pydantic import BaseModel, Field

app = FastAPI()


class FilterParams(BaseModel):
    # model_config = {"extra": "forbid"} # 禁止出现额外的查询参数,如果查询参数有额外数据则发送错误响应
    limit: int = Field(100, gt=0, le=100)
    offset: int = Field(0, ge=0)
    order_by: Literal["created_at", "updated_at"] = "created_at"
    tags: list[str] = []


@app.get("/items/")
async def read_items(filter_query: Annotated[FilterParams, Query()]):
    return filter_query

3.2Path

和Query的基本用法差不多,但这是用在路径参数上的

from typing import Annotated
from fastapi import Path


@app.get("/items/{item_id}")
async def read_items(
    item_id: Annotated[int, Path(title="The ID of the item to get")], q: str
):
    return item_id

3.3Body

单一值

如果请求体是这样的

{
    "importance": 5
}

可以这样声明来获得importance

@app.put("/items/{item_id}")
async def update_item(importance: Annotated[int, Body()]):
    return importance

嵌入单个请求体参数

如果希望得到的请求体是

{
    "item": {
        "name": "Foo",
        "description": "The pretender",
        "price": 42.0,
        "tax": 3.2
    }
}

而不是

{
    "name": "Foo",
    "description": "The pretender",
    "price": 42.0,
    "tax": 3.2
}

直接使用pytantic模型会把Item类解耦出来,所以得加上embed参数

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]):
    ...

Cookie 是 Path 和 Query 的“姐妹”类。它也继承自相同的通用 Param 类

from typing import Annotated

from fastapi import Cookie, FastAPI

app = FastAPI()


@app.get("/items/")
async def read_items(ads_id: Annotated[str | None, Cookie()] = None):
    return {"ads_id": ads_id}

如果有多个参数,可以使用pydantic模型

from typing import Annotated

from fastapi import Cookie, FastAPI
from pydantic import BaseModel

app = FastAPI()


class Cookies(BaseModel):
    session_id: str
    fatebook_tracker: str | None = None
    googall_tracker: str | None = None


@app.get("/items/")
async def read_items(cookies: Annotated[Cookies, Cookie()]):
    return cookies
from typing import Annotated

from fastapi import FastAPI, Header

app = FastAPI()


@app.get("/items/")
async def read_items(user_agent: Annotated[str | None, Header()] = None):
    return {"User-Agent": user_agent}

大多数标准请求头都由“连字符”字符分隔,也称为“减号”(-)。

但是像 user-agent 这样的变量在 Python 中是无效的。

因此,默认情况下,Header 会将参数名称中的下划线(_)字符转换为连字符(-),以提取和生成请求头文档。

此外,HTTP 请求头不区分大小写,因此你可以用标准的 Python 风格(也称为“snake_case”)声明它们。

所以,你可以在 Python 代码中像往常一样使用 user_agent,而不是需要将首字母大写为 User_Agent 或类似的形式。

如果出于某种原因你需要禁用下划线到连字符的自动转换,请将 Header 的参数 convert_underscores 设置为 False。

如果有多个参数,可以使用pydantic模型

from typing import Annotated

from fastapi import FastAPI, Header
from pydantic import BaseModel

app = FastAPI()


class CommonHeaders(BaseModel):
    host: str
    save_data: bool
    if_modified_since: str | None = None
    traceparent: str | None = None
    x_tag: list[str] = []


@app.get("/items/")
async def read_items(headers: Annotated[CommonHeaders, Header()]):
    return headers

3.6 Form

HTML 表单(<form></form>)向服务器发送数据的方式通常使用一种“特殊”编码,它与 JSON 不同。表单数据通常使用“媒体类型”application/x-www-form-urlencoded,当表单包含文件时,它会被编码为 multipart/form-data。

FastAPI 默认会把 POST body 当成 JSON 来解析,此时,不能直接使用pydantic模型去接收,你定义参数为 userinfo: UserCreate(Pydantic 模型)时,FastAPI 以为前端发的是 JSON,而你发的是表单 → 格式不匹配

此时可以使用Form组件来接受

from typing import Annotated
from fastapi import Form


@app.post("/login/")
async def login(username: Annotated[str, Form()], password: Annotated[str, Form()]):
    return {"username": username}

表单转json

如果一定想用pydantic模型,可以定义转换函数和依赖注入,这种适用于想自己控制细节的,可扩展

class UserCreate(SQLModel):

    username: str = Field(index=True, max_length=20, description="用户名")
    password: str = Field(max_length=255, description="密码")


    @classmethod
    def as_form( 
            cls,
            username: str = Form(...),
            password: str = Form(...)
        ):
            return cls(username=username, password=password)

    

然后路由函数为

# 用 Depends 注入
@app.post("/login/")
async def result_page(
     req: Request,
     userinfo: UserCreate = Depends(UserCreate.as_form),
 ):
 ...

表单模型

推荐使用Annotated让Form适配pydantic模型,更加现代的写法,但定制空间较小

from typing import Annotated

from fastapi import Form
from pydantic import BaseModel

class FormData(BaseModel):
    username: str
    password: str


@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
    return data

3.7File

可以使用 File 来定义客户端上传的文件

from typing import Annotated

from fastapi import FastAPI, File, UploadFile

app = FastAPI()


@app.post("/files/")
async def create_file(file: Annotated[bytes, File()]):
    return {"file_size": len(file)}


@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
    return {"filename": file.filename}

使用 UploadFile 相比 bytes 有以下几个优点

  • 你无需在参数的默认值中使用 File()。
  • 它使用 “缓冲” 文件,文件在内存中存储,直到达到最大大小限制,超过此限制后将存储在磁盘上。
  • 这意味着它能很好地处理大文件,如图像、视频、大型二进制文件等,而不会消耗所有内存。
  • 你可以从上传的文件中获取元数据。
  • 它具有 文件类 async 接口。
  • 它暴露了一个实际的 Python SpooledTemporaryFile 对象,你可以直接将其传递给其他期望文件类对象的库。

多文件上传

from typing import Annotated

from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse

app = FastAPI()


@app.post("/files/")
async def create_files(files: Annotated[list[bytes], File()]):
    return {"file_sizes": [len(file) for file in files]}


@app.post("/uploadfiles/")
async def create_upload_files(files: list[UploadFile]):
    return {"filenames": [file.filename for file in files]}


@app.get("/")
async def main():
    content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
    """
    return HTMLResponse(content=content)

4. 依赖注入

依赖注入是一种在处理请求时“自动提供依赖对象”的机制。它的核心思想是:把资源或功能的获取与业务逻辑解耦,让代码更简洁、易维护、易测试

FastAPI 的依赖注入 = “声明需要什么 → FastAPI 自动给你” 通过 Depends:

  • 只需声明参数依赖
  • 框架会在请求生命周期内负责实例化、传入和清理

4.1 创建

通过 Depends 关键字实现注入,如下例所示

from typing import Annotated

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: Annotated[dict, Depends(common_parameters)]):
    return commons

这里的功能是Depends会收集查询参数中是否有q skip limit,并且把它们的值送入common_parameters函数,并把函数结果赋值给形参commons

可以进行推广,发现类进行__init__的返回也是可以赋值的,所以可以改写成依赖类

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()

fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]

class CommonQueryParams:
    def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit


@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response
commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]

为了减少代码重复,上面可以简写成

commons: Annotated[CommonQueryParams, Depends()]

fastapi是可以自动识别这个写法

也可以写成下面形式,但不推荐,因为有类型检查很重要

commons: Annotated[Any, Depends(CommonQueryParams)]

4.2 多重依赖

如果注入的依赖本身就有依赖项,如下所示,这种是允许的

from typing import Annotated

from fastapi import Cookie, Depends, FastAPI

app = FastAPI()


def query_extractor(q: str | None = None):
    return q


def query_or_cookie_extractor(
    q: Annotated[str, Depends(query_extractor)],
    last_query: Annotated[str | None, Cookie()] = None,
):
    if not q:
        return last_query
    return q


@app.get("/items/")
async def read_query(
    query_or_default: Annotated[str, Depends(query_or_cookie_extractor)],
):
    return {"q_or_cookie": query_or_default}

注意:

某个依赖在同一个路径操作中被多次声明,例如,多个依赖有一个共同的子依赖,则该子依赖只会被调用一次,然后进行缓存。为了避免使用缓存值,需要使用use_cache=False字段

async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_cache=False)]):
    return {"fresh_value": fresh_value}

4.3 路由装饰器的dependencies

如果有在进入路由函数之前,进行相关检查的需求,比如权限、jwt。可以使用dependencies参数。此时被依赖的函数的的结果不会被使用,不return也可以。

from typing import Annotated

from fastapi import Depends, FastAPI, Header, HTTPException

app = FastAPI()


async def verify_token(x_token: Annotated[str, Header()]):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def verify_key(x_key: Annotated[str, Header()]):
    if x_key != "fake-super-secret-key":
        raise HTTPException(status_code=400, detail="X-Key header invalid")
    return x_key


@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
    return [{"item": "Foo"}, {"item": "Bar"}]

5. 后台任务

创建一个函数作为后台任务运行。它只是一个可以接收参数的标准函数。它可以是 async def 或普通的 def 函数,FastAPI 将知道如何正确处理它。适用于发送电子邮件、缓慢处理数据任务

from fastapi import BackgroundTasks, FastAPI

app = FastAPI()


def write_notification(email: str, message=""):
    with open("log.txt", mode="w") as email_file:
        content = f"notification for {email}: {message}"
        email_file.write(content)


@app.post("/send-notification/{email}")
async def send_notification(email: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(write_notification, email, message="some notification")
    return {"message": "Notification sent in the background"}

6. 请求和响应

6.1 Request

fastapi基于starlette写的,可以直接使用starlette的request,如果想获得请求头、请求体等原始信息,可以直接访问request.headers、request.body()等属性。

from fastapi import FastAPI, Request

app = FastAPI()


@app.get("/items/{item_id}")
def read_root(item_id: str, request: Request):
    client_host = request.client.host
    return {"client_host": client_host, "item_id": item_id}

6.2 Response

fastapi同样基于starlette的response

如果想给响应添加cookie或者自定义响应头,可以直接使用Response对象

from fastapi import FastAPI, Response

app = FastAPI()


@app.post("/cookie-and-object/")
def create_cookie(response: Response):
    response.set_cookie(key="fakesession", value="fake-cookie-session-value")
    
    response.headers["X-Cat-Dog"] = "alone in the world"
    response.set_cookie(key="fakesession", value="fake-cookie-session-value")

    return {"message": "Come to the dark side, we have cookies"}

如果想完全控制响应,可以使用Response的子类,比如JSONResponse、HTMLResponse等

from fastapi import FastAPI
from fastapi.responses import JSONResponse

app = FastAPI()


@app.get("/headers/")
def get_headers():
    content = {"message": "Hello World"}
    headers = {"X-Cat-Dog": "alone in the world", "Content-Language": "en-US"}
    response = JSONResponse(content=content, headers=headers)
    response.set_cookie(key="fakesession", value="fake-cookie-session-value")
    return response

实战重点笔记

1. 项目构建

1.1项目结构

img

其中curd、models、schemas是数据库相关的,api放路由,static放静态页面,views放静态页面相关的视图路由

1.2 app实例化

FastAPI 应用有两个重要的阶段:

  1. 启动(Startup) 应用刚启动时,通常需要做一些初始化操作,比如:
    • 连接数据库
    • 启动后台任务
    • 加载配置文件或模型
  2. 关闭(Shutdown) 应用关闭时,需要做一些清理工作,比如:
    • 断开数据库连接
    • 停止后台任务
    • 释放资源

lifespan 这个 async context manager 就是把这两个阶段打包成一个上下文,在 yield 之前执行启动逻辑,在 yield 之后执行停止逻辑。

img

1.3 自定义异常处理

在实例化后,可以给app添加自定义的异常处理机制,来统一拦截应用中的不同类型异常,并返回更清晰、标准化的响应

img
async def http_error_handler(_: Request, exc: HTTPException):
    if exc.status_code == 401:
        return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)

    return JSONResponse({
        "code": exc.status_code,
        "message": exc.detail,
        "data": exc.detail
    }, status_code=exc.status_code, headers=exc.headers)

优点:

  1. 统一响应格式,让所有错误返回类似格式,便于前端解析。

  2. 记录和追踪,你可以在 handler 里记录日志,方便排查问题。

  3. 屏蔽敏感信息,避免将内部错误直接暴露给用户。

  4. 灵活扩展业务逻辑,对于业务级异常,可以实现统一兜底逻辑,比如自动回滚或报警。

1.4 中间件

中间件(Middleware)就像一条“拦截管道”,所有请求和响应都必须经过它们。

中间件的常见用途

  1. 认证与鉴权:检查请求头里的 Token,决定是否允许继续访问。
  2. 请求日志记录:记录请求路径、耗时、响应状态码。
  3. 全局错误处理:捕获所有未处理异常,统一返回格式。
  4. Session 管理:通过 Cookie 保持用户状态,比如 SessionMiddleware。
  5. 性能监控:统计请求耗时,打点埋点。

这里我们加入三个组件

# 自己写的组件
class Middleware:
    def __init__(
            self,
            app: ASGIApp,
    ) -> None:
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] != "http":  # pragma: no cover
            await self.app(scope, receive, send)
            return
        start_time = time.time()
        req = Request(scope, receive, send)
        if not req.session.get("session"):
            req.session.setdefault("session", random_str())

        async def send_wrapper(message: Message) -> None:
            process_time = time.time() - start_time
            if message["type"] == "http.response.start":
                headers = MutableHeaders(scope=message)
                headers.append("X-Process-Time", str(process_time))
            await send(message)
        await self.app(scope, receive, send_wrapper)
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from core.middleware import Middleware  # 完全自定义

app.add_middleware(Middleware)  # 放在最前面添加 → 最内层执行(在 Session 之后)
app.add_middleware(
    SessionMiddleware,
    secret_key="session",
    session_cookie="f_id",
    # max_age=4,
    # same_site="lax",
    # https_only=True,
)
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.CORS_ORIGINS,
    allow_credentials=settings.CORS_ALLOW_CREDENTIALS,
    allow_methods=settings.CORS_ALLOW_METHODS,
    allow_headers=settings.CORS_ALLOW_HEADERS,
)

注意:添加的时候要注意顺序,上面的代码书写顺序是“Middleware,SessionMiddleware,CORSMiddleware”,

执行顺序如下:

请求进入
↓
CORSMiddleware (前处理)
↓
SessionMiddleware (前处理)
↓
Middleware (前处理)
↓
业务路由函数
↓
Middleware (后处理)
↓
SessionMiddleware (后处理)
↓
CORSMiddlewaree (后处理)
↓
响应返回

由于Middleware依赖于SessionMiddleware 生成的request.session,所以在书写时,Middleware一定要放前面

1.5 视图路由

首先要给app增加配置

from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

app.mount('/static', StaticFiles(directory=settings.STATIC_DIR), name="static")
app.state.views = Jinja2Templates(directory=settings.TEMPLATE_DIR)

app.mount作用:挂载静态文件目录

  • app.mount 相当于在 FastAPI 应用中“挂载”一个子应用,这里挂载的是 StaticFiles,用于托管静态文件,让浏览器可以直接访问 CSS/JS 等资源。
  • "/static" 这是 URL 前缀。访问路径中如果以 /static 开头,就会进入这个静态文件处理逻辑。 例如:浏览器请求 http://localhost:8000/static/style.css,FastAPI 会去 settings.STATIC_DIR 目录下寻找 style.css 文件,相反如果想访问不在static的文件,就会被拒绝。
  • StaticFiles(directory=settings.STATIC_DIR) 指定静态文件存放的物理路径,settings.STATIC_DIR 通常是一个类似 ./static 或绝对路径。
  • name="static" 给这个挂载的子应用取一个名字。主要用于 URL 反向解析,在模板中使用 url_for('static', path='xxx') 来生成静态资源 URL。

app.state.views作用:配置模板引擎,用于渲染 HTML 页面

  • Jinja2Templates 这是 FastAPI 提供的一个模板渲染工具,底层用的是 Jinja2 模板引擎(Flask 也用它)。

  • directory=settings.TEMPLATE_DIR 指定模板文件(HTML)的存放目录。通常你会把模板文件放在 templates 文件夹中。

  • app.state.views app.state 是 FastAPI 内置的一个“全局存储”,你可以把一些全局变量放在这里。 把 Jinja2Templates 实例挂在 app.state.views,之后在路由中可以直接访问,比如:

    @app.get("/")
    async def home(request: Request):
        return app.state.views.TemplateResponse("index.html", {"request": request, "title": "首页"})

2. redis

定义初始化和关闭redis的函数,并把这个过程放在Startup和Shutdown的lifespan中。

import redis.asyncio as redis
from core.config import settings

redis_client: redis.Redis | None = None


async def close_redis():
    global redis_client
    print("close redis")
    if redis_client:
        await redis_client.close()


def get_redis():
    return redis_client


async def init_redis(max_connections: int = 20):
    global redis_client
    print("load redis")
    redis_client = await redis.from_url(
        settings.REDIS_URL,
        encoding="utf-8",
        decode_responses=True,
        max_connections=max_connections,
    )

使用redis的方式多种多样,可以使用依赖注入

@redis_router.post("/redis/set", response_class=HTMLResponse)
async def redis_set(req: Request, redis: redis.Redis = Depends(get_redis)):
    data = await req.json()
    key = data.get("key")
    value = data.get("value")
    await redis.set(key, value)
    return "ok"

也可以使用装饰器或者中间件,在参数到达路由函数就进行判断,之后不走函数直接返回,比如

def cacheable(ttl: int = 3600, key_prefix: str = "cache"):
    """
    Redis 缓存装饰器
    :param ttl: 缓存过期时间(秒)
    :param key_prefix: 缓存 key 前缀
    """
    def decorator(func: Callable):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs) -> Any:
            key = f"{key_prefix}:{func.__name__}:{args}:{kwargs}"

            cached = await redis_client.get(key)
            if cached:
                return json.loads(cached)

            result = await func(*args, **kwargs)

            if result is not None:
                await redis_client.set(key, json.dumps(result, default=str), ex=ttl)

            return result
        return wrapper
    return decorator

3. 认证方式

3.1 session_id

3.1.1工作流程

  1. 用户登录时,服务器验证用户名和密码。
  2. 服务器生成一个随机的 session_id(比如 abc123xyz),并在服务器内存或数据库里保存这段 session 相关的数据,比如:
    • 用户 ID
    • 登录时间
    • 权限信息
  3. 服务器将这个 session_id 返回给客户端,通常存放在浏览器的 cookie 里。
  4. 后续请求中,浏览器自动携带这个 session_id,服务器通过查找内存或数据库,确认用户身份。

3.1.2 特点

  • 状态存储在服务器,每个活跃用户都占用服务器内存或数据库。
  • session_id 本身没有实际信息,只是一个指针或索引。
  • 如果服务器挂了,或者 session 数据被清理,用户就需要重新登录。
  • 横向扩展难:如果你有多台服务器,需要通过共享数据库或 Redis 同步 session 数据。

3.1.3 参考实现

  1. 参考中间件的Middleware写法,就是以SessionMiddleware为基础,往request.session里面写入了session_id,SessionMiddleware会使用Set-Cookie把相关数据放入前端的cookie中,
  2. 后端则以session_id为key,用户信息为value存储和读取相关信息。

3.2 JWT(JSON Web Token):无状态认证

3.2.1 工作流程

  1. 用户登录时,服务器验证用户名和密码。

  2. 服务器生成一个 JWT Token,这个 Token 内含用户信息,并用服务器私钥签名,保证不能被篡改。 典型 JWT 长这样:

    xxxxx.yyyyy.zzzzz
    • xxxxx:Header(算法、类型)
    • yyyyy:Payload(用户 ID、过期时间、权限等)
    • zzzzz:Signature(签名,用来验证 Token 未被篡改)
  3. 服务器把 JWT 返回给客户端,客户端存储在 cookie 或 localStorage。

  4. 后续请求中,客户端带上这个 JWT,服务器只需验证签名,无需查数据库,就能确认身份。

3.2.2 特点

  • 服务器无需存储状态,只需持有签名密钥。
  • 横向扩展更容易,任何服务器节点都能直接验证 Token。
  • Token 内含用户信息,太大可能影响传输效率。
  • 安全风险:Token 泄漏后,除非密钥轮换或 Token 过期,攻击者就能直接伪装成用户。

3.2.3 参考实现

  1. 编码

    • jwt库在解码时会识别”exp”参数,并检查是否已过期,如果过期,会报jwt.ExpiredSignatureError的错误,可以用try...catch去捕捉。
    • 在创建 Token 时添加 “exp” 参数,这样就不用额外处理过期问题。
    def create_access_token(data: dict):
        token_data = data.copy()
    
        expire = datetime.now(UTC) + timedelta(
            minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
        )
    
        token_data.update({"exp": expire})
    
        jwt_token = jwt.encode(
            token_data, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM
        )
    
        return jwt_token
  2. 后端把用户信息编码jwt_token然后传给前端,传给前端的形式不限,主要取决于前端如何存放

    @login_router.post("/login", summary="用户登陆接口", response_class=JSONResponse)
    async def account_login(post: AccountLogin, session: SessionDep):
    
        get_user: User = await curd.user.get_user(session,username=post.username)
        if not get_user:
            return fail(msg=f"用户{post.username}密码验证失败!")
        if not check_password(post.password, get_user.password):
            return fail(msg=f"用户{post.username}密码验证失败!")
        if not get_user.user_status:
            return fail(msg=f"用户{post.username}已被管理员禁用!")
        jwt_data = {
            "user_id": get_user.id,
            "user_type": get_user.user_type
        }
        jwt_token = create_access_token(data=jwt_data)
    
        return JSONResponse({
            "code": 200,
            "message": "登陆成功😄",
            "data": {"token": "Bearer "+jwt_token}
        }, status_code=200, headers={"Set-Cookie": "X-token=Bearer "+jwt_token})
  3. 后端可以自动获取jwt_token

    from fastapi.security import OAuth2PasswordBearer
    
    OAuth2 = OAuth2PasswordBearer(tokenUrl="")
    
    async def check_permissions(
        req: Request, security_scopes: SecurityScopes, session: SessionDep, token=Depends(OAuth2),
    ): ...

    token=Depends(OAuth2)注入的过程等价于下面,相当于试图从请求头的Authorization获取token

    from fastapi.security.oauth2 import get_authorization_scheme_param
    
    authorization: str = req.headers.get("Authorization")
    scheme, param = get_authorization_scheme_param(authorization)
    
    if not authorization or scheme.lower() != "bearer":
        raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Not authenticated",
                headers={"WWW-Authenticate": "Bearer"},
            )
    
    token = param

    如果能获取token,则还需要解码

    payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
    
    if payload:
        user_id = payload.get("user_id", None)
        user_type = payload.get("user_type", None)
  4. 前端以下面代码为例,用户在登陆成功后会拿到token,此时如果用户想使用/v1/user/add,则需要在请求头中放入{ "Authorization": token }以来验证login的操作和add的操作来自同一个人

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>注册页面</title>
</head>

<body>
    <form id="loginForm">
        <label>
            用户名:
            <input type="text" placeholder="请输入用户名" name="username">
        </label>
        <label>
            密码:
            <input type="password" placeholder="请设置密码" name="password">
        </label>
        <button type="submit">登录</button>
    </form>
    <form id="regForm">
        <label>
            用户名:
            <input type="text" placeholder="请输入用户名" name="username">
        </label>
        <label>
            密码:
            <input type="password" placeholder="请设置密码" name="password">
        </label>
        <button type="submit">add</button>
    </form>
    <script>
        let token;
        document.getElementById("loginForm").addEventListener("submit", function (e) {
            e.preventDefault();

            const data = {
                username: e.target.username.value,
                password: e.target.password.value
            };

            fetch("/v1/user/login", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify(data)
            }).then(res => res.json())
                .then(data => {
                    if (data.code === 200) {
                        token = data.data.token;
                        console.log("Login successful", token);

                    } else {
                        console.log("Login failed");
                        alert(data.message);
                    }
                });
        });
        document.getElementById("regForm").addEventListener("submit", function (e) {
            e.preventDefault();

            const data = {
                username: e.target.username.value,
                password: e.target.password.value,
                user_type: false
            };


            fetch("/v1/user/add", {
                method: "POST",
                credentials: 'include',
                headers: { "Content-Type": "application/json", "Authorization": token },
                body: JSON.stringify(data)
            }).then(res => res.json())
                .then(data => {
                    if (data.code === 200) {

                        console.log("add successful");

                    } else {
                        console.log("add failed");
                        alert(data.message);
                    }
                });
        });
    </script>
</body>
</html>

4. RBAC权限

5. WebSockets