在FastAPI后端开发里,依赖注入 是最核心、也最容易被低估的能力之一。很多开发者习惯在每个路由函数里重复写鉴权、打日志、开数据库连接、解密参数。接口越写越胖,改一处要动一片,单元测试也几乎没法写。FastAPI的Depends()机制,正是为了把这类横切关注点从业务逻辑里剥离出来。
依赖注入的本质可以用一句话概括:把怎么拿到某个对象抽成独立函数,路由只声明我需要什么 。框架会在进入路由之前自动解析依赖树、按顺序执行、在同一请求内缓存结果,并把依赖里用到的Query、Header等参数写进Swagger文档。相比Flask里装饰器加g对象的写法,FastAPI的依赖注入在组合性、可测试性和类型安全上都更成熟。下文会从最基础的分页依赖讲起,逐步走到类依赖、子依赖链、全局与局部挂载、缓存与异步,最后用鉴权、日志、解密、数据库会话四个场景串起来,并与Flask装饰器方案对照说明差异。
一、依赖注入基础:从函数依赖开始
什么是依赖注入
先看一种很常见的写法:每个接口都自己从request里抠分页参数、自己校验Token,业务代码被挤到函数末尾。
python
@app.get("/users")
async def list_users(request: Request):
page = int(request.query_params.get("page", 1))
page_size = int(request.query_params.get("page_size", 10))
token = request.headers.get("Authorization", "").replace("Bearer ", "")
if token not in VALID_TOKENS:
raise HTTPException(401)
# 业务逻辑从这里才开始......
问题在于:分页和鉴权与列出用户毫无关系,却在每个路由里复制粘贴。依赖注入的改法,是把解析分页解析当前用户各自写成独立函数,路由参数里用Depends(...)声明需求即可。
python
@app.get("/users")
async def list_users(
pagination: Pagination = Depends(get_pagination),
user: UserInfo = Depends(get_current_user),
):
# pagination和user已经就绪,直接写业务
...
FastAPI看到Depends(get_pagination)时,会先调用get_pagination,把返回值赋给pagination,再进入路由函数体。路由函数不再碰Request的原始字段,签名本身也变成了文档:一眼能看出这个接口需要分页信息和登录用户。
基础函数依赖:分页与请求ID
任何可调用对象都可以当依赖,最常用的是普通函数。下面这个get_pagination把分页规则集中在一处:page默认1且不小于1,page_size默认10、范围1~100,并顺带算出SQL常用的offset。
python
def get_pagination(
page: int = Query(default=1, ge=1),
page_size: int = Query(default=10, ge=1, le=100),
) -> Pagination:
return Pagination(
page=page,
page_size=page_size,
offset=(page - 1) * page_size,
)
注意依赖函数参数上的Query(...):它们不是路由函数的参数,却会被FastAPI同样解析。也就是说,谁声明Depends(get_pagination),谁就自动拥有page和page_size两个查询参数,Swagger也会展示出来,无需额外配置。 路由侧只需一行声明:
python
@app.get("/demo/basic-dep")
async def demo_basic_dep(
pagination: Pagination = Depends(get_pagination),
request_id: str = Depends(get_request_id),
):
return {"pagination": pagination.model_dump(), "request_id": request_id}
get_request_id则从HeaderX-Request-ID读取追踪ID,客户端没传时用uuid生成一个。两个依赖互不干扰,FastAPI按各自需要的参数从请求里取值。
接口调用后返回信息:
访问GET /demo/basic-dep?page=2&page_size=5时,响应里的pagination.offset应为5(第二页、每页5条,跳过前5条)。若带上HeaderX-Request-ID: trace-abc,响应中的request_id会原样返回;不带则得到自动生成的UUID。调用返回:
json
{"pagination":{"page":2,"page_size":5,"offset":5},"request_id":"e17a6ecc-5fe9-4001-b021-b7e6eaf8c219"}
二、类依赖与子依赖
类依赖:可配置的限流策略
有时依赖本身需要构造参数------比如A接口30秒内最多3次,B接口60秒内最多100次。把限流写成一个类,实现__call__,实例就可以当作依赖使用:
python
class RateLimiter:
def __init__(self, max_calls: int = 5, window_seconds: float = 60.0):
self.max_calls = max_calls
self.window_seconds = window_seconds
self._calls: list[float] = []
def __call__(self, request: Request) -> None:
now = time.time()
# 清掉滑动窗口外的旧记录
self._calls = [t for t in self._calls if now - t < self.window_seconds]
if len(self._calls) >= self.max_calls:
raise HTTPException(status_code=429, detail="请求过于频繁,请稍后再试")
self._calls.append(now)
rate_limit_strict = RateLimiter(max_calls=3, window_seconds=30)和rate_limit_loose = RateLimiter(max_calls=100, window_seconds=60)是同一套逻辑的两个配置实例。路由通过dependencies=[Depends(rate_limit_strict)]挂载,限流失败时在__call__里直接抛429,路由函数甚至不会被执行。
python
@app.get("/demo/class-dep")
async def demo_class_dep(_: None = Depends(rate_limit_strict)):
return {"message": "限流通过"}
参数写成_: None = Depends(...)是因为我们只需要限流的副作用(检查通过),不需要返回值。连续快速请求同一接口超过3次后,第4次应收到429。如图所示,我连续点击了4次收到了请求频繁提示:

子依赖:鉴权链如何一层层拼装
依赖可以依赖其他依赖,FastAPI会构建一棵依赖树并按拓扑顺序执行。鉴权是最典型的例子,链路如下:HTTPBearer解析Header-get_token取出Bearer字符串-verify_token查表得到user_id-get_current_user加载用户-require_admin再校验角色。
python
bearer_scheme = HTTPBearer(auto_error=False)
def get_token(
credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
) -> str:
if credentials is None or credentials.scheme.lower() != "bearer":
raise HTTPException(status_code=401, detail="缺少 Bearer Token")
return credentials.credentials
def verify_token(token: str = Depends(get_token)) -> int:
user_id = _TOKENS.get(token)
if user_id is None:
raise HTTPException(status_code=401, detail="Token 无效或已过期")
return user_id
def get_current_user(user_id: int = Depends(verify_token)) -> UserInfo:
row = _DB.get(user_id)
if row is None:
raise HTTPException(status_code=404, detail="用户不存在")
return UserInfo(id=row["id"], username=row["username"], role=row["role"])
def require_admin(user: UserInfo = Depends(get_current_user)) -> UserInfo:
if user.role != "admin":
raise HTTPException(status_code=403, detail="需要管理员权限")
return user
演示用的Token表很简单:admin-token-001对应管理员alice,user-token-002对应普通用户bob。路由若只声明Depends(get_current_user),任意合法Token即可;管理接口改用Depends(require_admin),bob的Token会在最后一环收到403,而不需要在每个admin路由里手写if user.role != "admin"。
python
@app.get("/demo/sub-dep")
async def demo_sub_dep(user: UserInfo = Depends(get_current_user)):
return user
用curl测试时,带上Authorization: Bearer admin-token-001应返回alice的用户信息;不带Header则401。这条链的价值在于:每一环只做一件事,上层依赖下层的结果,组合而不是复制。这里使用APIFox测试:

三、全局依赖与路径局部依赖
依赖可以挂在不同粒度上,效果范围不同。全局依赖在创建FastAPI()时声明,对该应用下所有路由生效;Router级依赖只影响该模块下的接口;单路由的dependencies=参数则只约束某一个端点。还有一种常见写法是把依赖写在路由参数 里如user: UserInfo = Depends(get_current_user),这时不仅执行依赖逻辑,还把返回值注入函数,适合业务需要用到依赖结果的场景。
全局计时是一个典型副作用依赖:它不返回值给路由,只在request.state上记开始时间,供后续中间件或日志使用。
python
async def global_request_timer(request: Request) -> None:
request.state.start_time = time.perf_counter()
app = FastAPI(dependencies=[Depends(global_request_timer)])
secure_router则在Router级别同时挂上日志和鉴权,该前缀下所有接口自动记录请求、自动要求登录,路由函数里一行日志代码都不用写:
python
secure_router = APIRouter(
prefix="/secure",
dependencies=[Depends(log_request), Depends(get_current_user)],
)
@secure_router.get("/profile")
async def secure_profile(user: UserInfo = Depends(get_current_user)):
return user
@secure_router.get("/admin/stats")
async def admin_stats(admin: UserInfo = Depends(require_admin)):
return {"admin": admin.username, "user_count": len(_DB)}
访问GET /secure/profile需要任意有效Token;GET /secure/admin/stats还需要admin角色。Router级的Depends(get_current_user)保证未登录进不来,参数级的Depends(require_admin)在需要管理员的地方再收紧一层。两种情况有无管理员权限:


四、依赖缓存与异步依赖
同一请求内只算一次
默认情况下,同一个依赖函数在同一请求里被声明多次,只会执行一次 ,结果被缓存后复用。示例里expensive_sync_service内部有一个计数器,每被调用一次就加一:
python
_call_counter = {"expensive_sync": 0}
def expensive_sync_service() -> dict:
_call_counter["expensive_sync"] += 1
return {"engine": "sync", "calls_in_request": _call_counter["expensive_sync"]}
@app.get("/demo/cache")
async def demo_cache(
a: dict = Depends(expensive_sync_service),
b: dict = Depends(expensive_sync_service),
c: dict = Depends(expensive_sync_service, use_cache=False),
):
return {"cached_twice": a, "cached_again": b, "no_cache": c}
调用一次/demo/cache后,a和b里的calls_in_request都会是1说明第二次声明命中了缓存;而c使用了use_cache=False,强制重新执行,计数器变为2,所以c里会看到2。若你的依赖代表实时配额,这类同一请求内必须多次刷新的值,才需要关掉缓存;大多数鉴权、会话、配置读取场景,默认缓存正是期望行为。如下图:

异步依赖与带清理的yield
依赖函数可以是async def,FastAPI会自动await。模拟远程IO时:
python
async def expensive_async_service() -> dict:
await asyncio.sleep(0.01)
return {"engine": "async"}
数据库会话则常用yield:请求期间把session交给路由,请求结束后执行finally关闭连接。同步用@contextmanager,异步用@asynccontextmanager,FastAPI两种都支持:
python
@contextmanager
def get_db_session():
session = {"data": _DB}
try:
yield session
finally:
session.clear()
@asynccontextmanager
async def get_async_db_session():
session = {"data": _DB}
try:
await asyncio.sleep(0.005) # 模拟从连接池取连接
yield session
finally:
session.clear()
/demo/async-dep可以同时注入同步与异步依赖;/demo/async-db则演示异步session在async路由中的用法。模式与同步版一致:路由只消费session,创建与释放由依赖负责。
五、实战:四个高频依赖场景
接口鉴权
上一节的get_token - verify_token - get_current_user - require_admin链,就是生产里鉴权依赖的标准形状。普通业务接口参数写user: UserInfo = Depends(get_current_user);管理后台写admin: UserInfo = Depends(require_admin);完全公开的接口不声明任何鉴权依赖即可。Token校验、用户不存在、权限不足分别在链的不同节点抛出401、404、403,错误语义清晰,Swagger里AuthorizationHeader也会自动出现。
请求日志
日志依赖log_request接收Request和request_id,把method、path、客户端 IP、时间戳写入内存列表。挂在Router的dependencies上后,该模块每个请求都会自动记一条,路由函数保持零日志代码。管理员通过GET /demo/request-logs需admin Token查看最近 20 条,便于对照排查。
python
async def log_request(
request: Request,
request_id: str = Depends(get_request_id),
) -> None:
_REQUEST_LOGS.append({
"request_id": request_id,
"method": request.method,
"path": request.url.path,
"client": request.client.host if request.client else "unknown",
"at": datetime.now().isoformat(),
})
先访问几次 /secure/profile,再打开request-logs,应能看到对应path的记录。如图所示

请求参数统一解密
前端有时对敏感字段加密传输,后端若在每条业务路由里各自解密,既重复又容易漏。做法是把解密放进依赖:decrypt_order_body接收原始body模型,调用_decode_encrypted_note示例里用Base64模拟,生产可换成AES/RSA,返回业务层只认的明文模型。
python
def _decode_encrypted_note(value: str | None) -> str | None:
if value is None:
return None
try:
return base64.b64decode(value.encode()).decode()
except Exception as exc:
raise HTTPException(status_code=400, detail="encrypted_note 解密失败") from exc
def decrypt_order_body(body: OrderCreate) -> DecryptedOrderCreate:
return DecryptedOrderCreate(
product=body.product,
amount=body.amount,
note=_decode_encrypted_note(body.encrypted_note),
)
@app.post("/demo/decrypt-order")
async def demo_decrypt_order(
order: DecryptedOrderCreate = Depends(decrypt_order_body),
):
return order
请求体示例:encrypted_note为中文加急配送的Base64编码5Yqg5oCl6YWN6YCB。路由函数拿到的order.note已是解密后的明文,可以直接入库或校验,完全不知道加密字段的存在。 传入:
json
{
"product": "string",
"amount": 1,
"encrypted_note": "5Yqg5oCl6YWN6YCB"
}
返回如图: 
数据库会话与仓储子依赖
真实项目里session往往再包一层仓储。get_db_sessionyield出session,get_user_repo依赖session取出session["data"]作为仓储,路由只注入repo和分页:
python
def get_user_repo(session=Depends(get_db_session)):
return session["data"]
@app.get("/demo/db-session")
async def demo_db_session(
repo=Depends(get_user_repo),
pagination: Pagination = Depends(get_pagination),
):
users = list(repo.values())
start, end = pagination.offset, pagination.offset + pagination.page_size
return {"total": len(users), "items": users[start:end]}
这样形成 会话 - 仓储 - 业务 三层:连接生命周期在依赖里闭合,仓储构造也在依赖里完成,路由只做分页切片和返回JSON。与Flask里在视图开头db = get_db()、结尾db.close()相比,遗漏关闭或忘记rollback的概率小得多。
六、对比 Flask 装饰器:为什么依赖注入更优雅
Flask里常见做法是用装饰器在视图外包一层,把用户塞进全局g:
python
def login_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get("Authorization", "")
if not verify(token):
return jsonify({"error": "unauthorized"}), 401
g.current_user = get_user(token)
return f(*args, **kwargs)
return decorated
@app.route("/profile")
@login_required
def profile():
return jsonify(g.current_user)
能跑,但有几处长期痛点。用户信息藏在g.current_user里,IDE和类型检查器看不到类型;多个装饰器叠加时顺序敏感,@admin_required和@login_required谁先谁后容易踩坑;分页、Header等参数不会出现在函数签名里,OpenAPI文档也生成不出来;单元测试必须构造完整Request 和应用上下文,成本高。
FastAPI 的等价写法是:
python
@app.get("/secure/profile")
async def profile(user: UserInfo = Depends(get_current_user)):
return user
函数签名里的user: UserInfo既是运行时注入,也是文档和类型提示。require_admin依赖get_current_user,组合靠依赖树而不是装饰器栈。测试时可以用app.dependency_overrides替换任意依赖,无需真实Token:
python
def mock_admin():
return UserInfo(id=1, username="test", role="admin")
app.dependency_overrides[get_current_user] = mock_admin
# 此后所有使用 get_current_user 的接口都会拿到 mock 用户
同一请求内依赖默认只执行一次,装饰器方案则没有这层缓存。/demo/flask-vs-fastapi接口用JSON概括了上述对照,便于快速回顾。
总结
FastAPI的Depends()贯穿了从参数复用到鉴权、日志、解密、数据库的整条链路。函数依赖适合分页、Header解析这类无状态工具;类依赖适合带构造参数的策略,限流阈值不同;子依赖把Token校验、用户加载、角色判断拆成可复用的层级;全局与Router级挂载处理横切逻辑而业务路由保持干净;默认缓存避免同一请求重复建连或重复鉴权;async def与yield则分别适配异步IO和会话生命周期。
写好依赖的关键,是习惯在写路由之前先问一句:这件事是业务规则,还是拿到某个对象的plumbing? 后者放进依赖,路由就只剩业务。相比Flask装饰器加隐式全局状态,依赖注入提供了显式签名、自动文档、可override测试和请求级缓存,这也是FastAPI在工程化上最值得先掌握的一块。