FastAPI上线实战:认证、限流、零停机,一套代码搞定

key request 认证 限流 令牌
发布于 2026-06-12
1

我们非常重视原创文章,为尊重知识产权并避免潜在的版权问题,我们在此提供文章的摘要供您初步了解。如果您想要查阅更为详尽的内容,访问作者的公众号页面获取完整文章。

扫码阅读
手机扫码阅读

昨天还在为开发效率沾沾自喜,今天就在为生产环境焦头烂额。FastAPI 让你 5 分钟写出 Hello World,但真正让它扛住真实流量,完全是另一回事。

这篇文章来自一线实战,包含可直接复用的代码和经过验证的部署策略。读完你会得到一套完整的“生产环境生存指南”:谁可以进来(认证)、能做多少事(限流)、以及如何在不中断服务的情况下升级系统(零停机部署)

核心概念类比:就像开一家餐厅

想象你开了一家很火的餐厅:

  • 认证 = 门口保安,只让有会员卡的客人进来
  • 限流 = 厨房产能有限,高峰期只能一桌一桌上菜,不能把所有客人都塞进来
  • 零停机部署 = 餐厅在营业时间更换菜单,但客人完全感觉不到,菜还在继续上

下面我们就按这三个维度,一步步搭起来。

1. 认证:让每一次请求都有“身份证”

JWT(JSON Web Token)是当下最主流的认证方案。但生产环境需要特别注意几个细节。

为什么不能只用 JWT 字符串?

很多新手会犯一个错误:直接在代码里写死一个 JWT 密钥,验证通过就行。这相当于给餐厅配了个只认一张会员卡的保安——哪天换卡了(密钥轮换),所有客人都被拒之门外。

正确的做法是使用 OIDC(OpenID Connect)标准,让服务从认证服务器的 JWKS 端点动态获取公钥。

核心依赖:JWKS 自动更新

# auth.py from fastapi import Depends, HTTPException, status, Request from jose import jwt from httpx import AsyncClient import time import asyncio  # 缓存 JWKS 公钥,避免每次请求都去拉取 JWKS_CACHE = {"keys": [], "exp": 0} ISSUER = "https://accounts.example.com"# 替换为你的认证服务 AUD = "api"# 预期的 audience ALGOS = ["RS256"] # 只允许 RS256 JWKS_URL = f"{ISSUER}/.well-known/jwks.json"  asyncdef load_jwks():  """从认证服务器加载公钥,带缓存"""  global JWKS_CACHE  if time.time() < JWKS_CACHE["exp"]:  return JWKS_CACHE["keys"]    asyncwith AsyncClient(timeout=3) as c:  resp = await c.get(JWKS_URL)  resp.raise_for_status()  data = resp.json()    # 缓存 10 分钟,避免频繁拉取  JWKS_CACHE = {"keys": data["keys"], "exp": time.time() + 600}  return JWKS_CACHE["keys"]  asyncdef current_user(request: Request):  """依赖注入:从请求中提取并验证当前用户"""  auth = request.headers.get("authorization", "")    ifnot auth.startswith("Bearer "):  raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Missing or invalid token format")    token = auth.split(" ", 1)[1]  keys = await load_jwks()    try:  claims = jwt.decode(  token,   keys,   algorithms=ALGOS,   audience=AUD,   issuer=ISSUER  )  return {  "sub": claims["sub"],   "scopes": claims.get("scope", "").split()  }  except Exception as e:  # ⚠️ 这里不要打印完整 token,只打印错误类型  raise HTTPException(status.HTTP_401_UNAUTHORIZED, f"Invalid token: {type(e).__name__}") 

在路由中使用

from fastapi import FastAPI, Depends from auth import current_user  app = FastAPI()  @app.get("/me") async def me(user=Depends(current_user)):  """获取当前用户信息"""  return {"user": user["sub"]} 

生产级认证检查清单

⚠️ 注意:90% 的人会在这里踩坑

要点
正确做法
常见错误
Token 有效期
Access ≤ 15分钟,Refresh 7-30天
Access 设置 7 天,泄露风险极高
密钥管理
从 JWKS 动态获取
代码里写死一个密钥
吊销处理
Redis 存 jti(JWT ID),TTL 设 token 有效期
不做吊销,用户登出后 token 依然可用
Cookie vs Header
用 HttpOnly Cookie + CSRF token
单纯用 Cookie 不做防护
日志安全
只记录错误类型,不记录 token
日志里打印完整 token,敏感信息泄露

2. 限流:给你的系统装上“减压阀”

没有限流的系统,就像没有刹车的车。一个恶意用户或一个突发流量就能把整个服务拖垮。

为什么选择 Redis + Token Bucket?

Token Bucket(令牌桶)算法最适合 API 场景:

  • 允许短时突发流量(burst)
  • 长期平均速率可控
  • 分布式环境下用 Redis 保证原子性

类比:就像地铁闸机

  • 每个乘客刷卡需要消耗 1 个令牌
  • 闸机每秒自动补充固定数量的令牌(如 50 个/秒)
  • 高峰时段令牌消耗快,但桶里还有存货(允许突发)
  • 令牌用完 → 闸机关闭,提示“请稍后再试”

基于 Redis Lua 的原子性限流

100, refill_rate=50)  return limiter  asyncdef rate_limit(request: Request):  """依赖注入:限流中间件"""  limiter = await get_limiter()    # 根据场景选择限流维度  # 认证用户用 user_id,匿名用 IP  user_id = request.headers.get("x-user-id")  if user_id:  key = f"user:{user_id}"  else:  key = f"ip:{request.client.host}"    allowed = await limiter.check(key)  ifnot allowed:  raise HTTPException(  status_code=status.HTTP_429_TOO_MANY_REQUESTS,  detail="Rate limit exceeded. Please slow down.",  headers={"Retry-After": "30"} # 告诉客户端 30 秒后重试  ) 

在路由中使用

@app.get("/search", dependencies=[Depends(rate_limit)]) async def search(q: str):  """搜索接口,限制 50 请求/秒,突发 100"""  # 业务逻辑...  return {"results": []} 

限流策略矩阵

路由类型
限流维度
推荐速率
说明
匿名访问
IP
10-30 req/s
防止爬虫
认证用户
user_id
50-200 req/s
根据业务调整
API 密钥
api_key
100-1000 req/s
合作伙伴可单独配置
登录接口
IP
5 req/min
防止暴力破解
支付接口
user_id
2 req/s
敏感操作低频率

3. 零停机部署:让你的服务永不掉线

“发版就重启”是开发环境的做法。生产环境里,一个重启可能丢掉几十个正在处理的请求。

核心概念:优雅关闭

优雅关闭的意思是:收到终止信号后,不再接收新请求,但继续处理完已有的请求,然后才退出。

Uvicorn + Lifespan 最佳实践

yield# 应用运行期间    # 关闭时执行(优雅关闭时会等待)  await app.state.db_pool.close()  await app.state.redis.close()  app = FastAPI(lifespan=lifespan)  @app.get("/health/live") asyncdef liveness():  """存活探针:进程是否还在"""  return {"status": "alive"}  @app.get("/health/ready") asyncdef readiness():  """就绪探针:依赖是否就绪"""  try:  # 检查数据库  asyncwith app.state.db_pool.acquire() as conn:  await conn.execute("SELECT 1")  # 检查 Redis  await app.state.redis.ping()  return {"status": "ready"}  except Exception as e:  raise HTTPException(  status_code=status.HTTP_503_SERVICE_UNAVAILABLE,  detail=f"Not ready: {e}"  ) 

部署配置(Docker + Kubernetes)

# Dockerfile FROM python:3.11-slim  WORKDIR /app  COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt  COPY . .  # 多 worker 启动,优雅关闭超时 30 秒 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", \  "--workers", "4", "--graceful-timeout", "30"] 
# k8s-deployment.yaml apiVersion:apps/v1 kind:Deployment metadata: name:fastapi-app spec: replicas:3 strategy:  type:RollingUpdate  rollingUpdate:  maxSurge:1 # 允许多启动 1 个 pod  maxUnavailable:0# 不允许有 pod 不可用 template:  spec:  containers:  -name:app  image:fastapi:latest  ports:  -containerPort:8000  livenessProbe:  httpGet:  path:/health/live  por                  
                

数据STUDIO

点击领取《Python学习手册》,后台回复「福利」获取。『数据STUDIO』专注于数据科学原创文章分享,内容以 Python 为核心语言,涵盖机器学习、数据分析、可视化、MySQL等领域干货知识总结及实战项目。

158 篇文章
浏览 204K

还在用多套工具管项目?

一个平台搞定产品、项目、质量与效能,告别整合之苦,实现全流程闭环。

加入社区微信群
与行业大咖零距离交流学习
PMO实践白皮书
白皮书上线