入门
第一个 API
FastAPI 基于 Starlette 和 Pydantic。uvicorn 是 ASGI 服务器。--reload 用于在开发期间自动重载。
# install FastAPI and uvicorn
pip install fastapi uvicorn
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
# run: uvicorn main:app --reload使用 Uvicorn 运行
应用实例 'main:app' 表示模块 'main' 中的变量 'app'。--reload 仅用于开发;生产环境中不要使用,应改用进程管理器。
# development with auto-reload
uvicorn main:app --reload
# specify host and port
uvicorn main:app --host 0.0.0.0 --port 8000
# run programmatically
import uvicorn
if __name__ == "__main__":
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)异步路径操作
路径操作可以用 'def'(在线程池中运行)或 'async def' 声明。当函数通过异步库执行 I/O 时使用 async def;在 async def 中混用阻塞调用会阻塞事件循环。
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"message": "hello"}
@app.get("/sync")
def read_sync():
return {"message": "sync also works"}
# use async when calling async libs (httpx, databases)
@app.get("/data")
async def fetch_data():
import httpx
async with httpx.AsyncClient() as client:
r = await client.get("https://api.example.com")
return r.json()路径操作装饰器
FastAPI 支持所有 HTTP 方法:GET、POST、PUT、PATCH、DELETE、OPTIONS、HEAD。每个方法对应一个装饰器。在装饰器上使用 status_code= 可覆盖默认的成功状态码。
from fastapi import FastAPI
app = FastAPI()
@app.get("/items")
def list_items(): ...
@app.post("/items")
def create_item(): ...
@app.put("/items/{item_id}")
def replace_item(item_id: int): ...
@app.patch("/items/{item_id}")
def update_item(item_id: int): ...
@app.delete("/items/{item_id}")
def delete_item(item_id: int): ...
@app.options("/items")
def options_items(): ...
@app.head("/items")
def head_items(): ...JSON 响应与状态码
返回 dict/list 会自动转换为 JSON,状态码为 200(或你指定的 status_code)。若需完全控制状态码、头部或内容,请直接返回 JSONResponse 或 Response。
from fastapi import FastAPI, status
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/plain")
def plain():
return {"key": "value"} # auto JSON, 200 OK
@app.post("/created", status_code=status.HTTP_201_CREATED)
def created():
return {"id": 1}
@app.get("/custom")
def custom():
return JSONResponse(
status_code=418,
content={"error": "I'm a teapot"},
headers={"X-Custom": "yes"},
)项目结构
使用 APIRouter 将不断增长的应用拆分为模块,然后通过 app.include_router() 注册。将 Pydantic 模型、数据库设置和依赖项放在单独的文件中以便维护。
myapp/
main.py # creates the FastAPI app
routers/
users.py # APIRouter for /users
items.py # APIRouter for /items
models/
schemas.py # Pydantic models
database.py # DB engine & session
dependencies.py # shared Depends functions
tests/
test_main.py
# main.py
from fastapi import FastAPI
from routers import users, items
app = FastAPI()
app.include_router(users.router)
app.include_router(items.router, prefix="/items")路径参数
基本路径参数
从路径中捕获的值默认是字符串,除非添加类型注解。路径中的参数名必须与函数参数名完全匹配。
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id):
return {"item_id": item_id}
# GET /items/42 -> {"item_id": "42"} (string by default)类型转换
添加类型注解(int、float、bool、str、Enum)后,FastAPI 会验证并转换路径值。无效值返回 422 响应和清晰的错误信息。
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
# GET /items/3 -> {"item_id": 3}
# GET /items/abc -> 422 Validation Error (not an integer)路径验证
Path() 为路径参数添加元数据和约束。数值约束:ge (>=)、gt (>)、le (<=)、lt (<)。路径参数总是必填的,因此 Path() 不能设置默认值。
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(
item_id: int = Path(
title="The ID of the item",
description="Must be a positive integer",
ge=1,
le=1000,
),
):
return {"item_id": item_id}数值约束
对 int 或 float 路径参数使用 ge/gt/le/lt 来强制数值范围。组合约束可提供精确的边界验证并自动返回 422 错误。
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/ratio/{value}")
def ratio(
value: float = Path(gt=0, lt=1), # 0 < value < 1
):
return {"value": value}
@app.get("/page/{page}")
def page(
page: int = Path(ge=1), # >= 1
size: int = Path(le=100), # <= 100
):
return {"page": page, "size": size}路由顺序很重要
路由按声明顺序匹配。在动态模式(如 /users/{user_id})之前声明具体路径(如 /users/me),否则动态路由会遮蔽具体路由。
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/me")
def read_current_user():
return {"user": "me"}
@app.get("/users/{user_id}")
def read_user(user_id: str):
return {"user_id": user_id}
# /users/me MUST be declared before /users/{user_id}
# otherwise 'me' would match the {user_id} pattern枚举路径参数
使用 Enum 类型作为路径参数可将其限制为预定义值,并在 OpenAPI 文档中生成枚举 schema。继承 (str, 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}")
def get_model(model_name: ModelName):
if model_name is ModelName.alexnet:
return {"model": model_name, "layers": 5}
return {"model": model_name}
# GET /models/resnet -> {"model": "resnet"}查询参数
基本查询参数
不在路径中的函数参数会成为查询参数。提供默认值使其变为可选;当查询键缺失时使用默认值。
from fastapi import FastAPI
app = FastAPI()
fake_items = [{"item": "a"}, {"item": "b"}, {"item": "c"}]
@app.get("/items")
def list_items(skip: int = 0, limit: int = 10):
return fake_items[skip : skip + limit]
# GET /items?skip=0&limit=2
# GET /items -> skip=0, limit=10可选查询参数
使用 Optional[str] = None(或 Python 3.10+ 的 str | None = None)表示可省略的查询参数。必填参数只需不设默认值。
from typing import Optional
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: str, q: Optional[str] = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}
# GET /items/1?q=hello
# GET /items/1 (q is None)查询验证
Query() 添加验证:字符串用 min_length/max_length,数字用 ge/gt/le/lt,字符串格式用 pattern(正则)。使用 default= 设置值的同时应用约束。
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/search")
def search(q: str = Query(min_length=3, max_length=50, pattern="^[a-zA-Z]+$")):
return {"q": q}
@app.get("/page")
def page(limit: int = Query(default=10, ge=1, le=100)):
return {"limit": limit}布尔值转换
布尔查询参数接受多种真值:true、1、yes、on(不区分大小写)。其他值被解释为 false。无效类型如 ?active=maybe 返回 422。
from fastapi import FastAPI
app = FastAPI()
@app.get("/flags")
def flags(active: bool = False):
return {"active": active}
# GET /flags?active=true -> {"active": true}
# GET /flags?active=1 -> {"active": true}
# GET /flags?active=yes -> {"active": true}
# GET /flags?active=off -> {"active": false}列表查询参数
List[str] 参数接受查询字符串中重复的相同键。使用 Query(default=[]) 默认为空列表,或 Query(default=None) 允许缺失值。
from typing import List
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items")
def list_items(q: List[str] = Query(default=[])):
return {"q": q}
# GET /items?q=a&q=b&q=c -> {"q": ["a", "b", "c"]}
# declare default=None to accept a list or null必填查询参数
使用 Query(...)(省略号)声明一个仍带验证元数据的必填查询参数。没有默认值且没有 Query(...) 的普通注解参数也是必填的。
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items")
def read_item(q: str = Query(...)):
return {"q": q}
# GET /items?q=hello -> ok
# GET /items -> 422 (q is required)
# Query(...) (Ellipsis) marks a parameter as required请求体(Pydantic)
BaseModel 基础
声明为函数参数的 Pydantic BaseModel 成为 JSON 请求体。FastAPI 验证载荷、转换类型,无效数据返回 422。带默认值的字段是可选的。
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
in_stock: bool = True
app = FastAPI()
@app.post("/items")
def create_item(item: Item):
return item
# POST /items body: {"name": "Apple", "price": 0.5}
# -> {"name": "Apple", "price": 0.5, "in_stock": true}字段类型与约束
Field() 为模型属性添加约束和元数据:字符串用 min_length/max_length,数字用 gt/ge/lt/le,可变默认值用 default_factory。model_config 的 json_schema_extra 为文档添加示例。
from pydantic import BaseModel, Field
from typing import Optional
class Item(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(gt=0, description="must be positive")
tax: Optional[float] = None
tags: list[str] = Field(default_factory=list)
model_config = {
"json_schema_extra": {
"examples": [{"name": "Apple", "price": 0.5, "tags": ["fruit"]}]
}
}嵌套 模型
模型可以嵌套:将另一个 BaseModel 声明为字段类型。FastAPI 递归验证整个嵌套结构,生成的 OpenAPI schema 也反映嵌套对象。
from pydantic import BaseModel
from typing import Optional
class Image(BaseModel):
url: str
name: str
class Item(BaseModel):
name: str
description: Optional[str] = None
image: Optional[Image] = None
images: list[Image] = []
# valid body:
# {"name": "Phone", "image": {"url": "http://x/a.png", "name": "a"}}字段验证
使用 @field_validator(Pydantic v2)为每个字段添加自定义验证逻辑。抛出 ValueError 可拒绝输入(FastAPI 将其转为 422)。返回(可能转换后的)值以保留它。
from pydantic import BaseModel, field_validator
class User(BaseModel):
name: str
email: str
@field_validator("email")
@classmethod
def email_must_contain_at(cls, v: str) -> str:
if "@" not in v:
raise ValueError("must contain @")
return v.lower()
@field_validator("name")
@classmethod
def name_stripped(cls, v: str) -> str:
return v.strip()可选字段与默认值
只有没有默认值且不是 Optional 的字段才是必填的。Optional[str] = None 是可选可空字段的标准模式;像 0.0 这样的普通默认值使字段可选并使用该值。
from pydantic import BaseModel
from typing import Optional
class Item(BaseModel):
name: str # required
description: Optional[str] = None # optional, defaults to None
price: float = 0.0 # optional, defaults to 0.0
tags: list[str] = [] # optional, defaults to empty list
# All of these are valid:
# {"name": "X"}
# {"name": "X", "price": 9.99, "tags": ["a"]}Body 与 Path、Query 共存
路径、查询和请求体参数可在同一操作中共存。FastAPI 判断依据:在路径中为路径参数,Pydantic 模型(或 Body())为请求体,否则为查询。可选请求体需设默认值为 None。
from fastapi import FastAPI, Path, Query
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
app = FastAPI()
@app.put("/items/{item_id}")
def update_item(
item_id: int = Path(ge=1),
q: Optional[str] = Query(default=None),
item: Item = None,
):
result = {"item_id": item_id, **item.model_dump()}
if q:
result["q"] = q
return result响应模型
response_model 基础
response_model 将返回数据过滤为仅模型中定义的字段,即使返回的对象有额外字段。这是隐藏密码等敏感字段的标准方式。
from fastapi import FastAPI
from pydantic import BaseModel
class User(BaseModel):
username: str
email: str
hashed_password: str
class UserOut(BaseModel):
username: str
email: str
app = FastAPI()
@app.get("/users/me", response_model=UserOut)
def read_user():
return User(username="bob", email="[email protected]", hashed_password="secret")
# response: {"username": "bob", "email": "[email protected]"} (no password)排除字段
response_model_include 和 response_model_exclude 让你按路由白名单或黑名单字段,无需定义新模型。传入字段名集合。它们在 response_model 之上额外应用。
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
secret: str
app = FastAPI()
@app.get("/items/{id}", response_model=Item, response_model_exclude={"secret"})
def read_item(id: int):
return {"name": "Apple", "price": 0.5, "secret": "hidden"}
@app.get("/items", response_model=list[Item], response_model_include={"name", "price"})
def list_items():
return [{"name": "Apple", "price": 0.5, "secret": "x"}]列表响应
使用 response_model=list[Model](或 List[Model])声明端点返回对象数组。FastAPI 会验证并序列化列表中的每个元素。
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
app = FastAPI()
@app.get("/items", response_model=list[Item])
def list_items():
return [
{"name": "Apple", "price": 0.5},
{"name": "Banana", "price": 0.3},
]
# response: [{"name": "Apple", "price": 0.5}, {"name": "Banana", "price": 0.3}]排除未设置/默认值
exclude_unset 省略客户端从未发送的字段(只显示显式设置的)。exclude_defaults 省略等于默认值的字段;exclude_none 省略为 None 的字段。适用于稀疏响应。
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str | None = None
price: float = 0.0
tax: float = 0.0
app = FastAPI()
@app.get("/items/{id}", response_model=Item, response_model_exclude_unset=True)
def read_item(id: int):
return {"name": "Apple", "price": 0.5}
# -> {"name": "Apple", "price": 0.5} (description, tax omitted)
# also: response_model_exclude_defaults=True
# response_model_exclude_none=True按别名响应
Field(alias=...) 让传入 JSON 使用与 Python 属性不同的键名。response_model_by_alias=True 输出时也使用别名。populate_by_name=True 还允许输入时使用原始属性名。
from fastapi import FastAPI
from pydantic import BaseModel, ConfigDict, Field
class Item(BaseModel):
model_config = ConfigDict(populate_by_name=True)
name: str = Field(alias="itemName")
price: float
app = FastAPI()
@app.post("/items", response_model=Item, response_model_by_alias=True)
def create(item: Item):
return item
# input: {"itemName": "Apple", "price": 0.5}
# output: {"itemName": "Apple", "price": 0.5}纯字典响应
返回 dict/list 会自动序列化为 JSON。使用 response_class 更改默认响应类型(HTMLResponse、PlainTextResponse),或直接返回 Response 子类以获得完全控制。
from fastapi import FastAPI
from fastapi.responses import JSONResponse, PlainTextResponse
app = FastAPI()
@app.get("/dict")
def dict_resp():
return {"a": 1, "b": 2} # auto JSON
@app.get("/text", response_class=PlainTextResponse)
def text_resp():
return "plain text response"
@app.get("/raw")
def raw_resp():
return JSONResponse(content={"a": 1}, status_code=200)表单与文件
表单数据
Form() 声明从 application/x-www-form-urlencoded 或 multipart 表单数据解析的参数。不能在同一操作中将 Form() 与 JSON 请求体(BaseModel)混用——只能选一种内容类型。
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login")
def login(username: str = Form(), password: str = Form()):
return {"username": username}
# Content-Type: application/x-www-form-urlencoded
# body: username=bob&password=secretUploadFile 基础
UploadFile 是流式文件表示,具有 .filename、.content_type、.read()、.write()、.close()。在 async def 中使用 'await file.read()'。大文件建议使用 file.file(SpooledTemporaryFile)避免全部加载到内存。
from fastapi import FastAPI, UploadFile, File
app = FastAPI()
@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
contents = await file.read()
return {
"filename": file.filename,
"size": len(contents),
"content_type": file.content_type,
}多个文件
声明 list[UploadFile] 可在一个请求中接受多个文件。每个文件以相同字段名发送。列表中文件的顺序与发送顺序一致。
from fastapi import FastAPI, UploadFile
app = FastAPI()
@app.post("/uploads")
async def upload_many(files: list[UploadFile]):
return [
{"filename": f.filename, "size": len(await f.read())}
for f in files
]
# curl: curl -F "[email protected]" -F "[email protected]" http://localhost:8000/uploads文件与表单同时使用
Form() 和 UploadFile(File())可在同一端点组合使用,因为两者都使用 multipart/form-data。但仍不能将它们与 JSON BaseModel 请求体组合。
from fastapi import FastAPI, UploadFile, File, Form
app = FastAPI()
@app.post("/items")
async def create_item(
name: str = Form(),
description: str = Form(),
file: UploadFile = File(),
):
return {
"name": name,
"description": description,
"file": file.filename,
}保存上传文件
对于小文件,shutil.copyfileobj 高效地将 SpooledTemporaryFile 流式传输到磁盘。对于超大文件,分块读取(如 1MB)以降低内存。务必清理 file.filename 以防止路径遍历攻击。
import shutil
from pathlib import Path
from fastapi import FastAPI, UploadFile
app = FastAPI()
@app.post("/save")
async def save_file(file: UploadFile):
dest = Path("uploads") / file.filename
dest.parent.mkdir(exist_ok=True)
with dest.open("wb") as buf:
shutil.copyfileobj(file.file, buf)
return {"saved_to": str(dest)}
# async alternative for large files:
# async with dest.open("wb") as buf:
# while chunk := await file.read(1024 * 1024):
# buf.write(chunk)文件元数据与大小
在处理前验证 file.content_type 和大小。read() 后光标在末尾;如需再次读取,调用 'await file.seek(0)' 回退。真正的类型检查应检查文件魔数,而不仅仅是头部。
from fastapi import FastAPI, UploadFile, File
app = FastAPI()
@app.post("/validate")
async def validate(file: UploadFile = File(...)):
# allowed types
allowed = {"image/png", "image/jpeg"}
if file.content_type not in allowed:
return {"error": "unsupported type"}
contents = await file.read()
max_size = 5 * 1024 * 1024 # 5 MB
if len(contents) > max_size:
return {"error": "file too large"}
await file.seek(0) # rewind so later reads work
return {"filename": file.filename, "size": len(contents)}依赖注入
基本 Depends
Depends() 将函数的返回值注入到路径操作中。共享逻辑(分页、解析、数据库会话)集中在一处并在多个端点复用。依赖项的参数也会在 OpenAPI 中记录。
from fastapi import FastAPI, Depends
app = FastAPI()
def common_params(q: str | None = None, skip: int = 0, limit: int = 10):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items")
def list_items(commons: dict = Depends(common_params)):
return commons
@app.get("/users")
def list_users(commons: dict = Depends(common_params)):
return commons类依赖
带 __init__ 的类可作为依赖项;FastAPI 像调用函数一样调用它。使用 Depends(CommonQueryParams) 或在参数标注为该类时使用简写 Depends()。实例会被注入。
from fastapi import FastAPI, Depends
class CommonQueryParams:
def __init__(self, q: str | None = None, skip: int = 0, limit: int = 10):
self.q = q
self.skip = skip
self.limit = limit
app = FastAPI()
@app.get("/items")
def list_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
return {"q": commons.q, "skip": commons.skip, "limit": commons.limit}
# shorthand: commons = Depends() infers the type嵌套依赖
依赖项本身可以依赖其他依赖项,形成依赖图。FastAPI 解析整棵树,每个结果按请求缓存,并传播异常。这非常适合分层认证(token -> user -> permissions)。
from fastapi import FastAPI, Depends, Header, HTTPException
def get_token(authorization: str = Header()):
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="invalid header")
return authorization.removeprefix("Bearer ")
def get_current_user(token: str = Depends(get_token)):
if token != "admin-token":
raise HTTPException(status_code=403, detail="forbidden")
return {"username": "admin"}
app = FastAPI()
@app.get("/me")
def me(user: dict = Depends(get_current_user)):
return userYield 依赖(清理)
依赖生成器(yield)非常适合需要清理的资源:数据库会话、文件句柄、事务。yield 后的代码作为终结器运行,即使路由抛出异常也会执行。每个依赖项只能有一个 yield。
from fastapi import FastAPI, Depends
def get_db():
db = open_db_session() # acquire resource
try:
yield db # injected into the route
finally:
db.close() # always runs after the request
app = FastAPI()
@app.get("/items")
def list_items(db = Depends(get_db)):
return db.query("SELECT * FROM items")全局依赖
向 FastAPI()(或 APIRouter)传入 dependencies=[...] 可在每条路由上强制执行它们,而无需注入其返回值。适用于全局认证、限流或请求日志。
from fastapi import FastAPI, Depends, Header, HTTPException
def verify_token(x_token: str = Header()):
if x_token != "supersecret":
raise HTTPException(status_code=400, detail="X-Token missing")
def verify_key(x_key: str = Header()):
if x_key != "realkey":
raise HTTPException(status_code=400, detail="X-Key missing")
# applied to every route in the app
app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])
@app.get("/items")
def list_items():
return [{"item": "a"}] # token & key already checked按路由依赖
在装饰器(或 APIRouter)上使用 dependencies=[Depends(...)] 为特定路由要求依赖项而不使用其返回值。这样可以在保持路由签名整洁的同时仍强制执行检查。
from fastapi import FastAPI, Depends, Header, HTTPException
def verify_token(x_token: str = Header()):
if x_token != "secret":
raise HTTPException(status_code=401, detail="bad token")
app = FastAPI()
@app.get("/public")
def public():
return {"open": True}
@app.get("/private", dependencies=[Depends(verify_token)])
def private():
return {"secret": "data"}安全(HTTPBasic/OAuth2/JWT)
HTTP 基本认证
HTTPBasic 将 Authorization: Basic 头解析为用户名/密码。使用 secrets.compare_digest(而非 ==)以防止时序攻击。始终返回带 WWW-Authenticate: Basic 的 401,以便浏览器提示输入凭据。
import secrets
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
security = HTTPBasic()
app = FastAPI()
def get_current_user(credentials: HTTPBasicCredentials = Depends(security)):
correct_user = secrets.compare_digest(credentials.username, "admin")
correct_pass = secrets.compare_digest(credentials.password, "secret")
if not (correct_user and correct_pass):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/profile")
def profile(user: str = Depends(get_current_user)):
return {"user": user}OAuth2 密码承载
OAuth2PasswordBearer 声明路由需要 Bearer token;依赖项从 Authorization 头提取它。tokenUrl 告诉文档 UI 在哪里发送凭据。token 本身在验证之前只是一个字符串。
from fastapi import FastAPI, Depends
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI()
@app.get("/me")
def me(token: str = Depends(oauth2_scheme)):
return {"token": token}
# client POSTs username+password to /token, receives an access token,
# then sends it as: Authorization: Bearer <token>创建 JWT Token
使用 python-jose(或 PyJWT)签名 token。'sub'(主题)声明通常保存用户名或用户 ID,'exp' 设置过期时间。保持 SECRET_KEY 长、随机并从环境加载——切勿提交到代码库。
from datetime import datetime, timedelta, timezone
from jose import jwt
SECRET_KEY = "keep-this-secret"
ALGORITHM = "HS256"
def create_access_token(data: dict, expires_minutes: int = 30) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(minutes=expires_minutes)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
# token = create_access_token({"sub": "alice"})
# -> eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...解码 JWT Token
jwt.decode 自动验证签名和 'exp' 声明,失败时抛出 JWTError。将依赖项映射为带 WWW-Authenticate: Bearer 的 401,以便客户端知道需提供有效 token。
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
SECRET_KEY = "keep-this-secret"
ALGORITHM = "HS256"
def get_current_user(token: str = Depends(oauth2_scheme)) -> str:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str | None = payload.get("sub")
if username is None:
raise credentials_exception
return username
except JWTError:
raise credentials_exception
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)密码哈希
切勿存储明文密码。passlib 配合 bcrypt(或 argon2)处理加盐和哈希。verify() 将明文输入与存储的哈希比较。pip install passlib[bcrypt] 安装 bcrypt 后端。
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(plain: str) -> str:
return pwd_context.hash(plain)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
# usage when issuing tokens:
def authenticate_user(username: str, password: str):
user = fake_db.get(username)
if not user or not verify_password(password, user.hashed_password):
return False
return userOAuth2 登录流程
OAuth2PasswordRequestForm 从表单编码的 POST 读取用户名/密码(标准 OAuth2 密码流程)。返回 access_token 和 token_type: 'bearer'。此端点就是 OAuth2PasswordBearer 中 tokenUrl 指向的端点。