FastAPI 微服务实战:构建独立的用户认证与业务服务
大家好!欢迎回到我们的 FastAPI 项目实战系列!🚀
你是否曾听说过“微服务”这个时髦的词,但感觉它听起来很复杂,离自己很遥远?今天,我们将揭开它的神秘面纱,用 FastAPI 构建一个简单而实用的微服务 Demo,让你亲身体验微服务架构的魅力。
什么是微服务?
简单来说,微服务是一种软件架构风格。它将一个庞大的单体应用程序,拆分成一组小而独立的服务。每个服务都只负责一件事情,比如“用户管理”或“订单处理”。它们之间通过轻量级的 API 进行通信。
微服务的核心特点包括:
- 单一职责:每个服务都像一个“专家”,只精通自己的领域。
- 独立部署:一个服务的更新或部署,不会影响到其他服务。
- 松耦合**:服务之间通过 API 沟通,像打电话一样,而不是直接捆绑在一起。
- 技术多样性:你可以为用户服务选择 Python,为支付服务选择 Go,灵活多变。
- 高可用与可扩展性:当某个功能(如“商品推荐”)流量暴增时,你可以只扩展这一个服务,而不用把整个应用都复制一遍。
今天我们要构建什么?
我们将构建一个由两个独立 FastAPI 应用组成的微服务系统。同时,我们会用上 uv 这个现代化包管理工具的 workspace 功能,让这两个项目在开发阶段能共享同一个虚拟环境,但在部署时又能拥有各自独立的依赖。
我们的两个微服务分别是:
1. 用户服务 (user_service) :
- • 完全基于 FastAPI-Users 构建。
- • 唯一职责:处理用户的注册、登录和身份验证。它就是我们系统的“门卫”。
2. 待办事项服务 (todo_service) :
- • 一个经典的 Todos 应用,用户可以在其中创建待办事项列表和具体的待办事项。
- • 它完全不处理用户逻辑,但它的所有 API 都需要被保护起来,只有登录用户才能访问。
- • 它通过一个 user_id 字段来区分不同用户的数据。
最终的交互流程是:一个新用户首先通过 user_service 注册账号,然后使用 todo_service 的登录接口(该接口会代理到 user_service)获取令牌,最后凭借这个令牌来访问 todo_service 中受保护的 API,进行 Todos 的增删改查。
项目整体结构
一个清晰的文件结构是良好开端。我们的微服务项目将组织如下:
microservice/
├── todo_service/
│ ├── src/
│ ├── .env.example
│ ├── Dockerfile
│ └── pyproject.toml
├── user_service/
│ ├── src/
│ ├── .env.example
│ ├── Dockerfile
│ └── pyproject.toml
├── .gitignore
└── pyproject.toml # <-- uv-workspace 的根配置文件
第一步:使用 uv Workspace 管理项目**
uv 的 workspace 功能让我们可以在一个父目录下管理多个相关的 Python 项目,它们共享同一个顶层虚拟环境,非常适合微服务开发。
-
创建并进入项目根目录:
mkdir microservice cd microservice -
在根目录初始化
uv项目,这将创建顶层的pyproject.toml和.venv虚拟环境:uv init -
创建第一个微服务
user_service并将其添加为 workspace 成员:mkdir user_service cd user_service uv init你会看到
uv提示:Adding 'user_service' as a member of workspace ...。它会自动在根目录的pyproject.toml中添加user_service作为成员。 -
同样地,创建
todo_service:cd .. mkdir todo_service cd todo_service uv init
现在,你可以在 user_service 和 todo_service 目录下分别使用 uv add <package> 来为各自的项目添加依赖。开发时,它们共享根目录的 .venv 环境。当需要为某个服务生成 requirements.txt 进行独立部署时,只需在该服务目录下运行 uv pip freeze > requirements.txt 即可。
第二步:构建 user_service
user_service 的实现非常直接,它是一个标准的 FastAPI-Users 项目。关于如何从零搭建,你可以完全参考我之前的这篇保姆级教程:
FastAPI-Users保姆级教程(七):实战篇——构建包含用户认证的项目模板
你只需按照教程搭建即可,它将为我们提供用户注册、登录 (/auth/jwt/login) 和获取当前用户信息 (/users/me) 等核心 API。
提示: 记得在
user_service的pyproject.toml中添加 FastAPI-Users 及其相关依赖。
第三步:构建 todo_service
todo_service 是一个常规的 CRUD** 应用。它的模型、数据库设置等代码我们在此略过(你可以在文末的仓库链接中找到完整代码)。我们的核心任务是:如何在 todo_service 中验证一个由 user_service 签发的令牌?
答案是:通过 httpx 客户端,让 todo_service 在需要验证用户时,主动向 user_service 发起一个 API 请求。
1. 在 lifespan 中初始化 httpx 客户端
我们使用 FastAPI 现代化的 lifespan state 模式,在应用启动时创建一个可复用的 httpx.AsyncClient 实例。
todo_service/src/core/lifespan.py:
from typing import TypedDict
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from loguru import logger
from redis.asyncio import Redis
from httpx import AsyncClient # 导入 httpx 异步客户端
# ... 其他导入 ...
class AppState(TypedDict):
"""定义应用生命周期中共享状态的结构。"""
# ... 其他状态 ...
http_client: AsyncClient # 新增 http_client 类型
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[AppState]:
# -------- 启动 --------
get_settings()
# ... 数据库和 Redis 的初始化 ...
# 初始化 httpx 客户端
http_client = AsyncClient()
logger.info("HTTPX 客户端已就绪。")
yield {
# ... 其他状态 ...
"http_client": http_client,
}
# -------- 关闭 --------
# ... 数据库和 Redis 的关闭 ...
# 优雅地关闭 httpx 客户端
await http_client.aclose()
logger.info("HTTPX 客户端已关闭。")
代码释义:
- • 我们在
AppState这个类型字典中加入了http_client,以便享受类型提示带来的好处。 - • 在
yield之前,我们创建了一个AsyncClient实例。这个实例将在整个应用的生命周期中被复用,避免了为每个请求都新建连接的开销,性能更佳。 - • 在
yield之后,我们调用await http_client.aclose()来优雅地关闭客户端,释放所有网络连接。
2. 创建 httpx 客户端的依赖项
为了能在路由函数中方便地获取到这个客户端实例,我们创建一个依赖项。
todo_service/src/core/dependencies.py:
from typing import cast
from fastapi import Request
from httpx import AsyncClient
# ... 其他依赖项 ...
# HTTP 客户端依赖
def get_http_client(request: Request) -> AsyncClient:
"""从 request.state 中安全地获取 httpx 客户端实例。"""
return cast(AsyncClient, request.state.http_client)
3. 定义用户数据模型
todo_service 需要知道它从 user_service 获取的用户数据长什么样。因此,我们需要创建一个与 user_service 中用户 Schema 完全一致的 Pydantic 模型。
todo_service/src/schemas/user.py:
from uuid import UUID
from pydantic import BaseModel, EmailStr
class UserRead(BaseModel):
id: UUID
email: EmailStr
is_active: bool
is_superuser: bool
is_verified: bool
class Config:
from_attributes = True # Pydantic v2 写法
第四步:核心!跨服务认证逻辑
这是本次教程的重中之重。我们将创建一个 get_current_user 依赖项,它将负责完成跨服务的用户认证。
todo_service/src/core/auth.py:
import json
from loguru import logger
from httpx import AsyncClient, HTTPStatusError, RequestError
from fastapi import HTTPException, Security, Depends
from fastapi.security import OAuth2PasswordBearer
from redis.asyncio import Redis
from src.core.dependencies import get_http_client, get_redis
from src.core.config import settings
from src.schemas.user import UserRead
# 从配置中读取 user_service 的地址
user_service_url = settings.user_service_url
# 关键:这里的 tokenUrl 指向的是 user_service 的登录接口!
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{user_service_url}/auth/jwt/login")
async def get_current_user(
token: str = Security(oauth2_scheme),
redis: Redis = Depends(get_redis),
http_client: AsyncClient = Depends(get_http_client),
) -> UserRead:
"""
通过 Redis 缓存和 user_service 验证令牌,获取当前用户信息。
这是一个 FastAPI 依赖项,可以保护我们的路由。
"""
# 1. 优先查询 Redis 缓存
cached_user = await redis.get(f"user:{token}")
if cached_user:
logger.debug("从 Redis 缓存命中用户。")
return UserRead.model_validate(json.loads(cached_user))
# 2. 如果缓存未命中,则向 user_service 发起请求进行验证
logger.debug("缓存未命中,正在请求 user_service...")
headers = {"Authorization": f"Bearer {token}"}
try:
response = await http_client.get(
f"{user_service_url}/users/me", headers=headers
)
response.raise_for_status() # 如果响应状态码不是 2xx,则抛出异常
except HTTPStatusError as exc:
# 如果令牌无效,user_service 会返回 401
raise HTTPException(
status_code=exc.response.status_code,
detail="Invalid authentication credentials",
)
except RequestError:
# 如果 user_service 无法访问
logger.error("User service is unavailable")
raise HTTPException(status_code=503, detail="User service unavailable")
user_data = response.json()
validated_user = UserRead.model_validate(user_data)
# 3. 将成功获取的用户信息存入 Redis 缓存,并设置 1 小时过期
await redis.setex(f"user:{token}", 3600, validated_user.model_dump_json())
logger.debug("用户信息已成功缓存到 Redis。")
return validated_user
代码释义:
这段代码是微服务通信认证的“心脏”,让我们一步步解析它的工作流程:
oauth2_scheme = OAuth2PasswordBearer(...): - • 这一行非常关键!它告诉 FastAPI 的 Swagger UI,当需要获取令牌(Token)时,应该向哪个地址发起登录请求。在这里,我们明确指向了user_service的登录接口。这意味着,当你在todo_service的 API 文档上点击 "Authorize" 时,你实际上是在和user_service对话来获取令牌。- 函数签名
get_current_user(...): - •token: str = Security(oauth2_scheme): 这个依赖项会自动从请求头中提取Authorization: Bearer <token>里的令牌部分。 - •redis: Redis = Depends(get_redis)和http_client: AsyncClient = Depends(get_http_client): 通过依赖注入,我们轻松地获取了 Redis 和httpx客户端的实例。 - 认证流程:
- • 第一步:查缓存。函数首先尝试从 Redis 中获取用户信息。键名通常是
user:<token>。如果找到了,就直接解析返回,避免了不必要的网络请求,大大提升了性能。 - • 第二步:远程验证。如果 Redis 中没有,它就会使用
httpx客户端,带着令牌向user_service的/users/me接口发起一个 GET 请求。这个接口是 FastAPI-Users 提供的标准接口,用于返回当前令牌对应的用户信息。 - • 异常处理:
-
- •
HTTPStatusError: 如果user_service返回了非 2xx 的状态码(比如 401 Unauthorized,表示令牌无效或过期),httpx会抛出这个异常。我们捕获它,并向客户端返回相应的 HTTP 错误。 - •
RequestError: 如果网络不通,或者user_service挂了,就会抛出这个异常。我们捕获它,并返回一个 503 Service Unavailable 错误,告诉客户端“门卫”服务当前不可用。
- •
- • 第三步:写缓存。如果从
user_service成功获取到了用户信息,我们会立即将其存入 Redis,并设置一个过期时间(例如 1 小时)。这样,在接下来的一小时内,同一个令牌的所有请求都将直接从 Redis 获得响应。
- • 第一步:查缓存。函数首先尝试从 Redis 中获取用户信息。键名通常是
第五步:保护你的路由
现在我们拥有了强大的 get_current_user 依赖项,保护 todo_service 的路由变得异常简单。
在你的路由文件中,只需将它添加到 APIRouter 的 dependencies 参数中即可:
# todo_service/src/api/routes/todos.py
from fastapi import APIRouter, Depends
from src.core.auth import get_current_user
from src.schemas.user import UserRead
router = APIRouter(
prefix="/todos",
tags=["Todos"],
dependencies=[Depends(get_current_user)]
)
@router.get("/")
async def get_my_todos(
# 你甚至可以在路由函数中再次注入它,以获取用户信息
current_user: UserRead = Depends(get_current_user)
):
user_id = current_user.id
# ... 根据 user_id 查询数据库 ...
return {"message": f"Todos for user {user_id}"}
现在,todos 标签下的所有路由都受到了保护。任何没有提供有效令牌的请求都将被拒绝!
别忘了跨域(CORS)
在微服务架构中,user_service 和 todo_service 通常运行在不同的端口或域名上。当你的前端应用需要同时与它们通信时,就会遇到浏览器的跨域问题。
确保在你的两个 FastAPI 应用中都配置了 CORS 中间件:
# In main.py of both services
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI(...)
origins = [
"http://localhost",
"http://localhost:3000", # 如果你的前端运行在 3000 端口
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
尾
恭喜你!通过这个项目,你已经成功地构建并理解了一个简单的微服务系统。让我们回顾一下核心收获:
- 微服务解耦: 我们将用户管理和业务逻辑(Todos)拆分到了两个独立的服务中,实现了单一职责。
-
uvWorkspace: 学会了使用uv的 workspace 功能来高效管理多项目开发环境。 - 跨服务认证: 掌握了在一个服务中,通过向另一个认证服务发起 API 请求来验证用户身份的核心模式。
- 性能优化: 利用 Redis 对用户信息进行缓存,显著减少了服务间的通信次数,提升了系统性能和响应速度。
- 依赖注入: 深度运用了 FastAPI 的依赖注入系统,使代码结构清晰、易于测试和维护。
这个模式是你构建更复杂微服务系统的绝佳起点。现在,你可以尝试将你自己的任何项目改造成这种架构,享受微服务带来的灵活性和可扩展性吧!
详细代码参考:github.com/acelee0621/…