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 32.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 将会
将请求体读取为 JSON。
转换相应的类型(如果需要)。
验证数据。如果数据无效,它将返回一个清晰的错误,精确指出错误数据的位置和内容。
将接收到的数据提供给参数
item由于你在函数中将其声明为
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)”。可以把它看作是 参数描述器,用来定义:
- 默认值
- 校验规则(比如长度、数值范围、正则等)
- 元信息(例如文档描述、示例值)
核心理念:它并不是运行时的业务逻辑对象,而是声明式的配置,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_query3.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_id3.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)]):
...3.4Cookie
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 cookies3.5Header
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 headers3.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 data3.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 responsecommons: 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项目结构
其中curd、models、schemas是数据库相关的,api放路由,static放静态页面,views放静态页面相关的视图路由
1.2 app实例化
FastAPI 应用有两个重要的阶段:
- 启动(Startup)
应用刚启动时,通常需要做一些初始化操作,比如:
- 连接数据库
- 启动后台任务
- 加载配置文件或模型
- 关闭(Shutdown)
应用关闭时,需要做一些清理工作,比如:
- 断开数据库连接
- 停止后台任务
- 释放资源
lifespan 这个 async context manager
就是把这两个阶段打包成一个上下文,在 yield
之前执行启动逻辑,在 yield 之后执行停止逻辑。
1.3 自定义异常处理
在实例化后,可以给app添加自定义的异常处理机制,来统一拦截应用中的不同类型异常,并返回更清晰、标准化的响应
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)优点:
统一响应格式,让所有错误返回类似格式,便于前端解析。
记录和追踪,你可以在 handler 里记录日志,方便排查问题。
屏蔽敏感信息,避免将内部错误直接暴露给用户。
灵活扩展业务逻辑,对于业务级异常,可以实现统一兜底逻辑,比如自动回滚或报警。
1.4 中间件
中间件(Middleware)就像一条“拦截管道”,所有请求和响应都必须经过它们。
中间件的常见用途
- 认证与鉴权:检查请求头里的 Token,决定是否允许继续访问。
- 请求日志记录:记录请求路径、耗时、响应状态码。
- 全局错误处理:捕获所有未处理异常,统一返回格式。
- Session 管理:通过 Cookie 保持用户状态,比如
SessionMiddleware。 - 性能监控:统计请求耗时,打点埋点。
这里我们加入三个组件
# 自己写的组件
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.viewsapp.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 decorator3. 认证方式
3.1 session_id
3.1.1工作流程
- 用户登录时,服务器验证用户名和密码。
- 服务器生成一个随机的 session_id(比如
abc123xyz),并在服务器内存或数据库里保存这段 session 相关的数据,比如:- 用户 ID
- 登录时间
- 权限信息
- 服务器将这个 session_id 返回给客户端,通常存放在浏览器的 cookie 里。
- 后续请求中,浏览器自动携带这个 session_id,服务器通过查找内存或数据库,确认用户身份。
3.1.2 特点
- 状态存储在服务器,每个活跃用户都占用服务器内存或数据库。
- session_id 本身没有实际信息,只是一个指针或索引。
- 如果服务器挂了,或者 session 数据被清理,用户就需要重新登录。
- 横向扩展难:如果你有多台服务器,需要通过共享数据库或 Redis 同步 session 数据。
3.1.3 参考实现
- 参考中间件的
Middleware写法,就是以SessionMiddleware为基础,往request.session里面写入了session_id,SessionMiddleware会使用Set-Cookie把相关数据放入前端的cookie中, - 后端则以
session_id为key,用户信息为value存储和读取相关信息。
3.2 JWT(JSON Web Token):无状态认证
3.2.1 工作流程
用户登录时,服务器验证用户名和密码。
服务器生成一个 JWT Token,这个 Token 内含用户信息,并用服务器私钥签名,保证不能被篡改。 典型 JWT 长这样:
xxxxx.yyyyy.zzzzzxxxxx:Header(算法、类型)yyyyy:Payload(用户 ID、过期时间、权限等)zzzzz:Signature(签名,用来验证 Token 未被篡改)
服务器把 JWT 返回给客户端,客户端存储在 cookie 或 localStorage。
后续请求中,客户端带上这个 JWT,服务器只需验证签名,无需查数据库,就能确认身份。
3.2.2 特点
- 服务器无需存储状态,只需持有签名密钥。
- 横向扩展更容易,任何服务器节点都能直接验证 Token。
- Token 内含用户信息,太大可能影响传输效率。
- 安全风险:Token 泄漏后,除非密钥轮换或 Token 过期,攻击者就能直接伪装成用户。
3.2.3 参考实现
编码
- 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- jwt库在解码时会识别”exp”参数,并检查是否已过期,如果过期,会报
后端把用户信息编码
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})后端可以自动获取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获取tokenfrom 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)前端以下面代码为例,用户在登陆成功后会拿到
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>