FastAPI 极速开发指南 --- 从零到生产级 API 实战
版本 : v1.0 | 日期 : 2026-06-13 | FastAPI : 0.136.3 | Python : 3.12.3
参考 : 菜鸟教程 FastAPI
实操环境: 华为云 ECS Ubuntu 24.04 LTS (4节点集群)
目录
- [一、FastAPI 概述](#一、FastAPI 概述)
- 二、环境搭建
- [三、第一个 FastAPI 应用](#三、第一个 FastAPI 应用)
- 四、路径参数与查询参数
- [五、Pydantic 请求体模型](#五、Pydantic 请求体模型)
- 六、响应模型与数据校验
- [七、OAuth2 + JWT 安全认证](#七、OAuth2 + JWT 安全认证)
- [八、CORS 跨域配置](#八、CORS 跨域配置)
- 九、部署与生产优化
- 十、踩坑记录
一、FastAPI 概述
1.1 什么是 FastAPI
FastAPI 是一个现代、高性能的 Python Web 框架,基于 Starlette (异步框架)和 Pydantic(数据校验库)构建。
┌─────────────────────────────────────────────────────────────┐
│ FastAPI 技术栈架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ 开发者代码 │ ← 你的路由、模型、业务逻辑 │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ FastAPI │ ← 路由注册、依赖注入、自动文档 │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Starlette │ ← ASGI 框架,异步请求/响应处理 │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Pydantic │ ← 数据校验、序列化、类型转换 │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Uvicorn (ASGI) │ ← 高性能异步服务器 │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
1.2 核心优势
| 特性 | FastAPI | Flask | Django REST |
|---|---|---|---|
| 性能 | ⭐⭐⭐⭐⭐ (与 Node.js/Go 同级) | ⭐⭐⭐ | ⭐⭐⭐ |
| 自动文档 | ⭐⭐⭐⭐⭐ (Swagger + ReDoc) | ❌ (需插件) | ⭐⭐⭐ (Django REST Docs) |
| 类型校验 | ⭐⭐⭐⭐⭐ (原生 Pydantic) | ❌ (需 Marshmallow) | ⭐⭐⭐ (Serializer) |
| 异步支持 | ⭐⭐⭐⭐⭐ (原生 async/await) | ⭐⭐⭐ (Flask 2.0+) | ⭐⭐⭐ (Django 3.1+) |
| 学习曲线 | ⭐⭐⭐⭐ (Python 类型注解即可) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
1.3 适用场景
- ✅ 微服务 API 开发 --- 高性能、低延迟
- ✅ 前后端分离项目 --- 自动生成 OpenAPI 文档
- ✅ 机器学习模型部署 --- 快速将模型封装为 REST API
- ✅ 数据接口服务 --- Pydantic 自动校验保障数据质量
二、环境搭建
2.1 服务器环境
本次实操使用华为云 ECS 集群(ecs-eb17):
| 节点 | 公网 IP | 私网 IP | 角色 |
|---|---|---|---|
| fastapi-01 | 120.46.183.221 | 192.168.0.152 | 主节点(API 服务) |
| fastapi-02 | 1.92.122.183 | 192.168.0.134 | 节点-2 |
| fastapi-03 | 1.92.69.229 | 192.168.0.58 | 节点-3 |
| fastapi-04 | 124.70.69.224 | 192.168.0.59 | 节点-4 |
集群规格: 2vCPUs | 4GiB | ac9.large.2
操作系统: Ubuntu 24.04 server 64bit
Python: 3.12.3 (系统自带)
2.2 安装 Python 虚拟环境
Ubuntu 24.04 默认启用 PEP 668 外部管理环境,直接 pip install 会报错:
bash
root@ecs-eb17-0001:~# pip3 install fastapi
error: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.
正确做法:创建虚拟环境(venv):
bash
# 安装 python3-venv(如未安装)
root@ecs-eb17-0001:~# apt-get update -qq && apt-get install -y -qq python3-venv python3-full
# 创建项目目录和虚拟环境
root@ecs-eb17-0001:~# mkdir -p /opt/fastapi-demo && cd /opt/fastapi-demo
root@ecs-eb17-0001:/opt/fastapi-demo# python3 -m venv venv
# 激活虚拟环境
root@ecs-eb17-0001:/opt/fastapi-demo# source venv/bin/activate
(venv) root@ecs-eb17-0001:/opt/fastapi-demo#
# 升级 pip
(venv) root@ecs-eb17-0001:/opt/fastapi-demo# pip install --upgrade pip
2.3 安装 FastAPI 和 Uvicorn
bash
(venv) root@ecs-eb17-0001:/opt/fastapi-demo# pip install fastapi uvicorn
Successfully installed annotated-doc-0.0.4 annotated-types-0.7.0 anyio-4.13.0
click-8.4.1 fastapi-0.136.3 h11-0.16.0 idna-3.18 pydantic-2.13.4
pydantic-core-2.46.4 starlette-1.3.1 typing-extensions-4.15.0
typing-inspection-0.4.2 uvicorn-0.49.0
2.4 安装完整依赖(后续章节使用)
bash
(venv) root@ecs-eb17-0001:/opt/fastapi-demo# pip install \
fastapi uvicorn[standard] \
python-multipart \
python-jose[cryptography] \
passlib[bcrypt] \
sqlalchemy \
pydantic \
email-validator
依赖说明:
uvicorn[standard]:包含 uvloop、httptools 等性能优化组件python-multipart:支持文件上传(multipart/form-data)python-jose[cryptography]:JWT 令牌签发与验证passlib[bcrypt]:密码哈希(bcrypt 算法)sqlalchemy:ORM 数据库操作email-validator:Pydantic EmailStr 类型校验
三、第一个 FastAPI 应用
3.1 最简应用代码
python
# /opt/fastapi-demo/main.py
from fastapi import FastAPI
# 步骤1:导入 FastAPI 类
# 步骤2:创建应用实例
app = FastAPI()
# 步骤3:定义路径操作装饰器
# 步骤4:定义路径操作函数
@app.get("/")
async def root():
# 步骤5:返回响应内容
return {"message": "Hello World"}
3.2 代码逐步说明
| 步骤 | 代码 | 说明 |
|---|---|---|
| 1 | from fastapi import FastAPI |
导入 FastAPI 类,继承自 Starlette |
| 2 | app = FastAPI() |
创建应用实例,无需传 __name__(与 Flask 不同) |
| 3 | @app.get("/") |
路径操作装饰器,声明 GET 方法监听根路径 / |
| 4 | async def root(): |
路径操作函数,可用 async def 或普通 def |
| 5 | return {"message": "Hello World"} |
返回字典,FastAPI 自动转换为 JSON 响应 |
3.3 启动服务器
bash
# 方式一:开发模式(带热重载)
(venv) root@ecs-eb17-0001:/opt/fastapi-demo# uvicorn main:app --reload --host 0.0.0.0 --port 8000
# 方式二:生产模式(多 worker)
(venv) root@ecs-eb17-0001:/opt/fastapi-demo# uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
参数说明:
main:app:main.py文件中的app对象--reload:代码修改后自动重启(仅开发使用)--host 0.0.0.0:监听所有网络接口(默认仅 127.0.0.1)--workers 4:启动 4 个 worker 进程(生产环境推荐)
3.4 服务器实操输出
bash
root@ecs-eb17-0001:/opt/fastapi-demo# setsid bash -c 'cd /opt/fastapi-demo && source venv/bin/activate && uvicorn main:app --host 0.0.0.0 --port 8000 > /tmp/uvicorn.log 2>&1' &
root@ecs-eb17-0001:/opt/fastapi-demo# python3 -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8000/').read().decode())"
{"message":"Hello World"}
3.5 自动生成的 API 文档
FastAPI 自动生成两套交互式文档:
| 文档类型 | URL | 特点 |
|---|---|---|
| Swagger UI | http://localhost:8000/docs |
交互式测试,可发送请求 |
| ReDoc | http://localhost:8000/redoc |
静态文档,适合阅读 |
| OpenAPI JSON | http://localhost:8000/openapi.json |
机器可读规范 |
┌─────────────────────────────────────────────────────────────┐
│ FastAPI 自动文档机制 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 你的代码 │
│ ├── @app.get("/") ← 装饰器提取路径、方法 │
│ ├── async def root() ← 函数签名提取参数 │
│ └── return {...} ← 返回类型推断响应模型 │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ FastAPI 内部路由注册表 │ │
│ │ - 路径 / 方法 / 参数 / 响应模型 │ │
│ └──────────────┬───────────────────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ /docs │ │ /redoc │ │
│ │ Swagger │ │ ReDoc │ │
│ │ UI │ │ │ │
│ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
四、路径参数与查询参数
4.1 路径参数(Path Parameters)
路径参数从 URL 路径的 {} 占位符中提取:
python
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
item_id声明为int类型- FastAPI 自动将 URL 中的字符串转换为整数
- 传入非整数时自动返回 422 校验错误
4.2 查询参数(Query Parameters)
查询参数从 URL 的 ?key=value 中提取:
python
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str = None):
return {"item_id": item_id, "q": q}
q有默认值None,表示可选参数- 若声明为
q: str(无默认值),则变为必填查询参数
4.3 实操输出
bash
# 路径参数自动类型转换
root@ecs-eb17-0001:~# python3 -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8000/items/5').read().decode())"
{"item_id":5,"q":null}
# 路径参数 + 查询参数
root@ecs-eb17-0001:~# python3 -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8000/items/5?q=runoob').read().decode())"
{"item_id":5,"q":"runoob"}
# 类型错误:传入非整数路径参数
root@ecs-eb17-0001:~# python3 -c "
import urllib.request, urllib.error
try:
urllib.request.urlopen('http://localhost:8000/items/foo')
except urllib.error.HTTPError as e:
print(f'Status: {e.code}')
print(e.read().decode())
"
Status: 422
{
"detail":[
{
"type":"int_parsing",
"loc":["path","item_id"],
"msg":"Input should be a valid integer, unable to parse string as an integer",
"input":"foo"
}
]
}
4.4 FastAPI 参数识别规则
| 参数来源 | 识别条件 | 示例 |
|---|---|---|
| 路径参数 | 参数名出现在路由路径的 {} 中 |
item_id 在 /items/{item_id} 中 |
| 查询参数 | 参数是单一类型(int、str、bool 等) |
q: str = None |
| 请求体 | 参数类型是 Pydantic 模型 | item: Item |
五、Pydantic 请求体模型
5.1 什么是 Pydantic
Pydantic 是 FastAPI 的核心依赖,用于数据校验和序列化 。核心思想:用 Python 类型注解定义数据结构,Pydantic 自动负责校验和转换。
┌─────────────────────────────────────────────────────────────┐
│ Pydantic 工作流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 客户端 JSON │
│ {"name":"Foo","price":35.4} │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Pydantic 解析 │ ← 字段类型校验、必填检查、自动转换 │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Item 对象 │ ← 具有类型安全的 Python 对象 │
│ │ item.name: str │ │
│ │ item.price: float│ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ 业务逻辑处理 │ ← 你的代码操作类型安全的对象 │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
5.2 定义模型
python
from pydantic import BaseModel
class Item(BaseModel):
name: str # 必填:商品名称
description: str = None # 可选:商品描述
price: float # 必填:商品价格
tax: float = None # 可选:税费
字段是否必填取决于是否有默认值:
| 字段 | 声明方式 | 是否必填 |
|---|---|---|
name |
name: str |
✅ 必填 |
description |
description: str = None |
❌ 可选 |
price |
price: float |
✅ 必填 |
tax |
tax: float = None |
❌ 可选 |
5.3 作为请求体使用
python
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
@app.post("/items/")
async def create_item(item: Item):
# FastAPI 自动校验请求体,校验通过后赋值给 item 参数
return item
5.4 实操输出
bash
# POST 请求创建资源
root@ecs-eb17-0001:~# python3 -c "
import urllib.request, json
item = {'name': 'Foo', 'price': 35.4}
req = urllib.request.Request('http://localhost:8000/items/', data=json.dumps(item).encode(), headers={'Content-Type': 'application/json'})
print(urllib.request.urlopen(req).read().decode())
"
{"name":"Foo","description":null,"price":35.4,"tax":null}
# PUT 请求更新资源
root@ecs-eb17-0001:~# python3 -c "
import urllib.request, json
item = {'name': 'Bar', 'price': 45.2}
req = urllib.request.Request('http://localhost:8000/items/1', data=json.dumps(item).encode(), headers={'Content-Type': 'application/json'}, method='PUT')
print(urllib.request.urlopen(req).read().decode())
"
{"item_id":1,"item_name":"Bar"}
5.5 模型校验错误示例
当缺少必填字段时,FastAPI 自动返回详细的校验错误:
bash
# 缺少必填字段 name 和 price
root@ecs-eb17-0001:~# python3 -c "
import urllib.request, json
item = {'description': '缺少 name 和 price'}
req = urllib.request.Request('http://localhost:8000/items/', data=json.dumps(item).encode(), headers={'Content-Type': 'application/json'})
try:
urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
print(e.read().decode())
"
{
"detail": [
{
"type": "missing",
"loc": ["body", "name"],
"msg": "Field required",
"input": {"description": "缺少 name 和 price"}
},
{
"type": "missing",
"loc": ["body", "price"],
"msg": "Field required",
"input": {"description": "缺少 name 和 price"}
}
]
}
5.6 Pydantic v2 常用方法
| 说明 | v2(推荐) | v1(已弃用) |
|---|---|---|
| 序列化为字典 | item.model_dump() |
item.dict() |
| 序列化为 JSON | item.model_dump_json() |
item.json() |
| 从字典创建 | Item.model_validate(data) |
Item.parse_obj(data) |
| 从 JSON 创建 | Item.model_validate_json(json_str) |
Item.parse_raw(json_str) |
| 获取 JSON Schema | Item.model_json_schema() |
Item.schema() |
5.7 模型继承
Pydantic 模型支持继承,可以方便地创建输入模型和输出模型:
python
from pydantic import BaseModel, EmailStr
# 基础模型
class UserBase(BaseModel):
username: str
email: EmailStr # 自动校验邮箱格式
full_name: str = None
# 创建用户时的输入模型(包含密码)
class UserCreate(UserBase):
password: str
# 返回用户信息时的输出模型(不包含密码)
class UserOut(UserBase):
id: int # 由服务器生成
# 使用示例
@app.post("/users/", response_model=UserOut)
async def create_user(user: UserCreate):
return {"id": 1, **user.model_dump(exclude={"password"})}
⚠️ 安全提示:使用不同的输入和输出模型是保护敏感数据的重要手段。永远不要在 API 响应中返回密码等敏感字段。
六、响应模型与数据校验
6.1 response_model 参数
使用 response_model 参数可以控制 API 的响应结构:
python
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
class ItemOut(BaseModel):
name: str
price_with_tax: float
@app.post("/items/", response_model=ItemOut)
async def create_item(item: Item):
price_with_tax = item.price * 1.1 if item.tax else item.price
return {"name": item.name, "price_with_tax": price_with_tax}
6.2 响应模型作用
| 作用 | 说明 |
|---|---|
| 数据过滤 | 仅返回模型中定义的字段 |
| 类型转换 | 自动将返回数据转换为模型定义的类型 |
| 文档生成 | 在 OpenAPI 文档中展示响应结构 |
| IDE 支持 | 编辑器中获得自动补全和类型检查 |
七、OAuth2 + JWT 安全认证
7.1 认证流程
┌──────────┐ ┌──────────┐
│ 客户端 │ ① POST /token │ 服务端 │
│ │ username=alice&password=secret │ │
│ │─────────────────────────────────>│ │
│ │ │ │
│ │ ② {access_token: "eyJ...", │ │
│ │ token_type: "bearer"} │ │
│ │<─────────────────────────────────│ │
│ │ │ │
│ │ ③ GET /users/me │ │
│ │ Authorization: Bearer eyJ... │ │
│ │─────────────────────────────────>│ │
│ │ │ │
│ │ ④ {username: "alice", ...} │ │
│ │<─────────────────────────────────│ │
└──────────┘ └──────────┘
7.2 安装依赖
bash
(venv) root@ecs-eb17-0001:/opt/fastapi-demo# pip install "python-jose[cryptography]" passlib[bcrypt]
7.3 完整实现代码
python
from datetime import datetime, timedelta
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
# ===== 配置 =====
SECRET_KEY = "your-secret-key-keep-it-secret" # 生产环境使用环境变量
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# ===== 密码哈希 =====
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# ===== OAuth2 方案 =====
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# ===== 数据模型 =====
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str = None
class User(BaseModel):
username: str
email: str = None
full_name: str = None
disabled: bool = None
class UserInDB(User):
hashed_password: str
# ===== 模拟数据库 =====
fake_users_db = {
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "alice@example.com",
"hashed_password": pwd_context.hash("secret"),
"disabled": False,
}
}
# ===== 工具函数 =====
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def get_user(db: dict, username: str) -> UserInDB | None:
if username in db:
return UserInDB(**db[username])
return None
def authenticate_user(db: dict, username: str, password: str):
user = get_user(db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
def create_access_token(data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无法验证凭据",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = get_user(fake_users_db, username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(
current_user: Annotated[User, Depends(get_current_user)],
):
if current_user.disabled:
raise HTTPException(status_code=400, detail="用户已被禁用")
return current_user
# ===== 路由 =====
app = FastAPI()
@app.post("/token")
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/users/me")
async def read_users_me(
current_user: Annotated[User, Depends(get_current_active_user)],
):
return current_user
@app.get("/users/me/items")
async def read_own_items(
current_user: Annotated[User, Depends(get_current_active_user)],
):
return [{"item_id": "Foo", "owner": current_user.username}]
7.4 关键组件解析
| 组件 | 说明 |
|---|---|
OAuth2PasswordBearer |
从 Authorization: Bearer <token> 请求头中获取令牌 |
CryptContext |
密码哈希工具,使用 bcrypt 算法 |
jwt.encode() |
创建 JWT 令牌 |
jwt.decode() |
解析并验证 JWT 令牌 |
Depends() |
依赖注入,实现认证逻辑的复用 |
7.5 在 Swagger UI 中测试
配置 OAuth2 后,Swagger UI (/docs) 会出现 "Authorize" 按钮:
- 点击 "Authorize"
- 输入用户名和密码(如
alice/secret) - 点击 "Authorize" 获取令牌
- 之后的所有请求都会自动携带令牌完成认证
八、CORS 跨域配置
8.1 什么是跨域
浏览器的同源策略限制了网页向不同域名发送请求。前后端分离开发时,前端和后端通常运行在不同端口上,需要配置 CORS。
| 前端地址 | 后端 API 地址 | 是否跨域 |
|---|---|---|
http://localhost:3000 |
http://localhost:8000 |
✅ 跨域(端口不同) |
http://example.com |
http://api.example.com |
✅ 跨域(域名不同) |
https://example.com |
http://example.com |
✅ 跨域(协议不同) |
http://example.com |
http://example.com/api |
❌ 同源 |
8.2 基础 CORS 配置
python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[ # 允许的源(域名列表)
"http://localhost:3000",
"http://localhost:8080",
],
allow_credentials=True, # 允许携带 Cookie
allow_methods=["*"], # 允许的 HTTP 方法
allow_headers=["*"], # 允许的请求头
)
8.3 CORS 配置参数详解
| 参数 | 类型 | 说明 | 推荐值 |
|---|---|---|---|
allow_origins |
list[str] |
允许跨域的源列表 | 生产环境用具体域名 |
allow_methods |
list[str] |
允许的 HTTP 方法 | ["GET", "POST", "PUT", "DELETE"] |
allow_headers |
list[str] |
允许的请求头 | 按需配置 |
allow_credentials |
bool |
是否允许携带 Cookie 和认证信息 | 需要认证时设为 True |
expose_headers |
list[str] |
前端可以访问的响应头 | 按需配置 |
max_age |
int |
预检请求的缓存时间(秒) | 600 |
⚠️ 安全提示 :
allow_origins使用["*"]表示允许所有源,但这与allow_credentials=True不兼容。如果需要携带 Cookie,必须指定具体的源。生产环境务必指定具体的域名,不要滥用["*"]。
8.4 开发环境 vs 生产环境
python
# 开发环境(仅用于开发!)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 生产环境
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://example.com",
"https://www.example.com",
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
max_age=600,
)
九、部署与生产优化
9.1 使用 systemd 管理 uvicorn
ini
# /etc/systemd/system/fastapi.service
[Unit]
Description=FastAPI Application
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/fastapi-demo
Environment="PATH=/opt/fastapi-demo/venv/bin"
ExecStart=/opt/fastapi-demo/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
Restart=always
[Install]
WantedBy=multi-user.target
9.2 使用 Nginx 反向代理
nginx
# /etc/nginx/sites-available/fastapi
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
9.3 Docker 部署
dockerfile
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
yaml
# docker-compose.yml
version: '3.8'
services:
fastapi:
build: .
ports:
- "8000:8000"
environment:
- SECRET_KEY=${SECRET_KEY}
restart: always
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- fastapi
十、踩坑记录
坑 1:Ubuntu 24.04 的 externally-managed-environment
现象:
bash
pip3 install fastapi
# error: externally-managed-environment
原因:Ubuntu 24.04 引入 PEP 668,禁止直接 pip 安装到系统 Python。
解决:使用虚拟环境:
bash
python3 -m venv venv
source venv/bin/activate
pip install fastapi uvicorn
坑 2:uvicorn 端口被占用
现象:
bash
ERROR: [Errno 98] error while attempting to bind on address ('0.0.0.0', 8000): address already in use
原因:之前的 uvicorn 进程未正确退出。
解决:
bash
# 查找并杀掉占用端口的进程
ss -tlnp | grep 8000
kill <PID>
# 或使用不同端口
uvicorn main:app --port 8080
坑 3:SSH 会话中 nohup 后台启动卡住
现象 :使用 SSH 执行 nohup uvicorn ... & 后,SSH 会话不返回。
原因:nohup 的标准输出仍连接到 SSH 会话的伪终端。
解决 :使用 setsid 或重定向输出:
bash
# 方法1:使用 setsid(推荐)
setsid bash -c 'cd /app && source venv/bin/activate && uvicorn main:app --host 0.0.0.0 --port 8000 > /tmp/uvicorn.log 2>&1' &
# 方法2:完整重定向
nohup uvicorn main:app > /tmp/uvicorn.log 2>&1 </dev/null &
坑 4:Pydantic v1 vs v2 方法变更
现象 :代码中使用 .dict() 或 .json() 报错。
原因:Pydantic v2 废弃了 v1 的方法名。
解决:
| v1(废弃) | v2(推荐) |
|---|---|
item.dict() |
item.model_dump() |
item.json() |
item.model_dump_json() |
Item.parse_obj(data) |
Item.model_validate(data) |
Item.schema() |
Item.model_json_schema() |
坑 5:CORS allow_origins="\*" 与 allow_credentials=True 冲突
现象:配置 CORS 后,携带 Cookie 的请求仍被拒绝。
原因 :浏览器安全策略禁止 * 与 credentials 同时使用。
解决:指定具体的源:
python
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # ❌ 不要用 ["*"]
allow_credentials=True,
)
附录:完整应用示例
python
# main.py --- 综合示例
from datetime import datetime, timedelta
from typing import Annotated, List
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel, EmailStr
# ========== 配置 ==========
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# ========== 安全组件 ==========
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# ========== Pydantic 模型 ==========
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
class UserBase(BaseModel):
username: str
email: EmailStr = None
full_name: str = None
class UserCreate(UserBase):
password: str
class UserOut(UserBase):
id: int
class Token(BaseModel):
access_token: str
token_type: str
# ========== 模拟数据 ==========
fake_items_db = []
fake_users_db = {
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "alice@example.com",
"hashed_password": pwd_context.hash("secret"),
"disabled": False,
}
}
# ========== FastAPI 应用 ==========
app = FastAPI(
title="FastAPI Demo API",
description="FastAPI 完整示例项目",
version="1.0.0",
)
# CORS 配置
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ========== 认证工具函数 ==========
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def authenticate_user(username: str, password: str):
user = fake_users_db.get(username)
if not user or not verify_password(password, user["hashed_password"]):
return False
return user
def create_access_token(data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无法验证凭据",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = fake_users_db.get(username)
if user is None:
raise credentials_exception
return user
# ========== 路由 ==========
@app.get("/")
async def root():
return {"message": "Hello World", "docs": "/docs"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str = None):
return {"item_id": item_id, "q": q}
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
fake_items_db.append(item.model_dump())
return item
@app.get("/items/", response_model=List[Item])
async def list_items():
return fake_items_db
@app.post("/token", response_model=Token)
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user["username"]}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/users/me", response_model=UserOut)
async def read_users_me(current_user: Annotated[dict, Depends(get_current_user)]):
return {"id": 1, "username": current_user["username"], "email": current_user.get("email")}
实操验证环境:
- 服务器:华为云 ECS ecs-eb17-0001 (120.46.183.221)
- OS:Ubuntu 24.04 LTS
- Python:3.12.3
- FastAPI:0.136.3
- Pydantic:2.13.4
- Uvicorn:0.49.0
- 创建日期:2026-06-13 17:44 GMT+8