FastAPI 微服务实战:构建独立的用户认证与业务服务

FastAPI 微服务实战:构建独立的用户认证与业务服务

大家好!欢迎回到我们的 FastAPI 项目实战系列!🚀

你是否曾听说过"微服务"这个时髦的词,但感觉它听起来很复杂,离自己很遥远?今天,我们将揭开它的神秘面纱,用 FastAPI 构建一个简单而实用的微服务 Demo,让你亲身体验微服务架构的魅力。

什么是微服务?

简单来说,微服务是一种软件架构风格。它将一个庞大的单体应用程序,拆分成一组小而独立的服务。每个服务都只负责一件事情,比如"用户管理"或"订单处理"。它们之间通过轻量级的 API 进行通信。

微服务的核心特点包括:

  1. 单一职责:每个服务都像一个"专家",只精通自己的领域。
  2. 独立部署:一个服务的更新或部署,不会影响到其他服务。
  3. 松耦合**:服务之间通过 API 沟通,像打电话一样,而不是直接捆绑在一起。
  4. 技术多样性:你可以为用户服务选择 Python,为支付服务选择 Go,灵活多变。
  5. 高可用与可扩展性:当某个功能(如"商品推荐")流量暴增时,你可以只扩展这一个服务,而不用把整个应用都复制一遍。

今天我们要构建什么?

我们将构建一个由两个独立 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 的增删改查。

项目整体结构

一个清晰的文件结构是良好开端。我们的微服务项目将组织如下:

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 项目,它们共享同一个顶层虚拟环境,非常适合微服务开发。

  1. 创建并进入项目根目录:

    bash 复制代码
    mkdir microservice
    cd microservice
  2. 在根目录初始化 uv 项目,这将创建顶层的 pyproject.toml.venv 虚拟环境:

    csharp 复制代码
    uv init
  3. 创建第一个微服务 user_service 并将其添加为 workspace 成员:

    bash 复制代码
    mkdir user_service
    cd user_service
    uv init

    你会看到 uv 提示:Adding 'user_service' as a member of workspace ...。它会自动在根目录的 pyproject.toml 中添加 user_service 作为成员。

  4. 同样地,创建 todo_service

    bash 复制代码
    cd ..
    mkdir todo_service
    cd todo_service
    uv init

现在,你可以在 user_servicetodo_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_servicepyproject.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
代码释义:

这段代码是微服务通信认证的"心脏",让我们一步步解析它的工作流程:

  1. oauth2_scheme = OAuth2PasswordBearer(...) : - • 这一行非常关键!它告诉 FastAPI 的 Swagger UI,当需要获取令牌(Token)时,应该向哪个地址发起登录请求。在这里,我们明确指向了 user_service 的登录接口 。这意味着,当你在 todo_service 的 API 文档上点击 "Authorize" 时,你实际上是在和 user_service 对话来获取令牌。
  2. 函数签名 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 客户端的实例。
  3. 认证流程 :
    • 第一步:查缓存 。函数首先尝试从 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 获得响应。

第五步:保护你的路由

现在我们拥有了强大的 get_current_user 依赖项,保护 todo_service 的路由变得异常简单。

在你的路由文件中,只需将它添加到 APIRouterdependencies 参数中即可:

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_servicetodo_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=["*"],
)

恭喜你!通过这个项目,你已经成功地构建并理解了一个简单的微服务系统。让我们回顾一下核心收获:

  1. 微服务解耦: 我们将用户管理和业务逻辑(Todos)拆分到了两个独立的服务中,实现了单一职责。
  2. uv Workspace : 学会了使用 uv 的 workspace 功能来高效管理多项目开发环境。
  3. 跨服务认证: 掌握了在一个服务中,通过向另一个认证服务发起 API 请求来验证用户身份的核心模式。
  4. 性能优化: 利用 Redis 对用户信息进行缓存,显著减少了服务间的通信次数,提升了系统性能和响应速度。
  5. 依赖注入: 深度运用了 FastAPI 的依赖注入系统,使代码结构清晰、易于测试和维护。

这个模式是你构建更复杂微服务系统的绝佳起点。现在,你可以尝试将你自己的任何项目改造成这种架构,享受微服务带来的灵活性和可扩展性吧!

详细代码参考:github.com/acelee0621/...

mp.weixin.qq.com/s/QbSzaP5Gx...

相关推荐
曾经的三心草2 小时前
OpenCV1
python
不搞学术柒柒3 小时前
设计模式-行为型设计模式(针对对象之间的交互)
python·设计模式·交互
MThinker3 小时前
02-Media-11-video_player.py 对H.264或H.265格式视频播放器的示例程序
python·音视频·h.265·h.264·micropython·canmv·k230
2301_764441333 小时前
Python常见的排序算法及其特点和实现代码
python·算法·排序算法
MediaTea3 小时前
Python 编辑器:IDLE
开发语言·python·编辑器
ones~3 小时前
Python 简单算法题精选与题解汇总
数据结构·python·算法
胡耀超3 小时前
开源生态与技术民主化 - 从LLaMA到DeepSeek的开源革命(LLaMA、DeepSeek-V3、Mistral 7B)
人工智能·python·神经网络·开源·大模型·llama·deepseek
站大爷IP4 小时前
Python字典:高效数据管理的瑞士军刀
python
星川皆无恙4 小时前
电商机器学习线性回归:基于 Python 电商数据爬虫可视化分析预测系统
大数据·人工智能·爬虫·python·机器学习·数据分析·线性回归