FastAPI 微服务实战:构建独立的用户认证与业务服务
大家好!欢迎回到我们的 FastAPI 项目实战系列!🚀
你是否曾听说过"微服务"这个时髦的词,但感觉它听起来很复杂,离自己很遥远?今天,我们将揭开它的神秘面纱,用 FastAPI 构建一个简单而实用的微服务 Demo,让你亲身体验微服务架构的魅力。
什么是微服务?
简单来说,微服务是一种软件架构风格。它将一个庞大的单体应用程序,拆分成一组小而独立的服务。每个服务都只负责一件事情,比如"用户管理"或"订单处理"。它们之间通过轻量级的 API 进行通信。
微服务的核心特点包括:
- 单一职责:每个服务都像一个"专家",只精通自己的领域。
- 独立部署:一个服务的更新或部署,不会影响到其他服务。
- 松耦合**:服务之间通过 API 沟通,像打电话一样,而不是直接捆绑在一起。
- 技术多样性:你可以为用户服务选择 Python,为支付服务选择 Go,灵活多变。
- 高可用与可扩展性:当某个功能(如"商品推荐")流量暴增时,你可以只扩展这一个服务,而不用把整个应用都复制一遍。
今天我们要构建什么?
我们将构建一个由两个独立 FastAPI 应用组成的微服务系统。同时,我们会用上 uv
这个现代化包管理工具的 workspace 功能,让这两个项目在开发阶段能共享同一个虚拟环境,但在部署时又能拥有各自独立的依赖。
我们的两个微服务分别是:
-
用户服务 (
user_service
) : - • 完全基于 FastAPI-Users 构建。 - • 唯一职责:处理用户的注册、登录和身份验证。它就是我们系统的"门卫"。 -
待办事项服务 (
todo_service
) : - • 一个经典的 Todos 应用,用户可以在其中创建待办事项列表和具体的待办事项。 - • 它完全不处理用户逻辑 ,但它的所有 API 都需要被保护起来,只有登录用户才能访问。 - • 它通过一个user_id
字段来区分不同用户的数据。
最终的交互流程是:一个新用户首先通过 user_service
注册账号,然后使用 todo_service
的登录接口(该接口会代理到 user_service
)获取令牌,最后凭借这个令牌来访问 todo_service
中受保护的 API,进行 Todos 的增删改查。
项目整体结构
一个清晰的文件结构是良好开端。我们的微服务项目将组织如下:
css
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 项目,它们共享同一个顶层虚拟环境,非常适合微服务开发。
-
创建并进入项目根目录:
bashmkdir microservice cd microservice
-
在根目录初始化
uv
项目,这将创建顶层的pyproject.toml
和.venv
虚拟环境:csharpuv init
-
创建第一个微服务
user_service
并将其添加为 workspace 成员:bashmkdir user_service cd user_service uv init
你会看到
uv
提示:Adding 'user_service' as a member of workspace ...
。它会自动在根目录的pyproject.toml
中添加user_service
作为成员。 -
同样地,创建
todo_service
:bashcd .. 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
:
python
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
:
python
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
:
python
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
:
python
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
参数中即可:
python
# 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 中间件:
ini
# 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)拆分到了两个独立的服务中,实现了单一职责。
uv
Workspace : 学会了使用uv
的 workspace 功能来高效管理多项目开发环境。- 跨服务认证: 掌握了在一个服务中,通过向另一个认证服务发起 API 请求来验证用户身份的核心模式。
- 性能优化: 利用 Redis 对用户信息进行缓存,显著减少了服务间的通信次数,提升了系统性能和响应速度。
- 依赖注入: 深度运用了 FastAPI 的依赖注入系统,使代码结构清晰、易于测试和维护。
这个模式是你构建更复杂微服务系统的绝佳起点。现在,你可以尝试将你自己的任何项目改造成这种架构,享受微服务带来的灵活性和可扩展性吧!
详细代码参考:github.com/acelee0621/...