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

254 阅读11分钟

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 的增删改查。

项目整体结构

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

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. 创建并进入项目根目录:

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

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

    mkdir user_service
    cd user_service
    uv init
    

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

  4. 同样地,创建 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
代码释义:

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

  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 的路由变得异常简单。

在你的路由文件中,只需将它添加到 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=["*"],
)

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

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

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

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

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