FastAPI + GraphQL + SQLAlchemy 实现博客系统

本文将详细介绍如何使用 FastAPI、GraphQL(Strawberry)和 SQLAlchemy 实现一个带有认证功能的博客系统。
在这里插入图片描述

技术栈

  • FastAPI:高性能的 Python Web 框架
  • Strawberry:Python GraphQL 库
  • SQLAlchemy:Python ORM 框架
  • JWT:用于用户认证

系统架构

1. 数据模型(Models)

使用 SQLAlchemy 定义数据模型,以用户模型为例:

class UserModel(Base):
    """SQLAlchemy model for the users table"""
    __tablename__ = "users"
    
    id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
    username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
    email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
    hashed_password: Mapped[str] = mapped_column(String(200))
    nickname: Mapped[str] = mapped_column(String(50), nullable=True)
    is_active: Mapped[bool] = mapped_column(Boolean, default=True)
    is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
    
    # 关联关系
    posts = relationship("PostModel", back_populates="author")
    
    def verify_password(self, password: str) -> bool:
        """验证密码"""
        return pwd_context.verify(password, self.hashed_password)

2. GraphQL Schema

使用 Strawberry 定义 GraphQL schema,包括查询和变更:

from models.types import (
    UserRead,      # 用户信息读取类型
    UserCreate,    # 用户创建输入类型
    LoginInput,    # 登录输入类型
    LoginResponse, # 登录响应类型
    RegisterResponse, # 注册响应类型
    Token,        # Token类型
    PostRead,     # 文章读取类型
    PostCreate,   # 文章创建输入类型
    PageInput,    # 分页输入类型
    Page,         # 分页响应类型
    PageInfo      # 分页信息类型
)

@strawberry.type
class Query:
    @strawberry.field
    def hello(self) -> str:
        """测试接口"""
        return "Hello World"

    @strawberry.field
    def me(self, info) -> Optional[UserRead]:
        """
        获取当前用户信息
        - 需要认证
        - 返回 None 表示未登录
        - 返回 UserRead 类型表示当前登录用户信息
        """
        if not info.context.get("user"):
            return None
        return info.context["user"].to_read()

    @strawberry.field
    def my_posts(self, info, page_input: Optional[PageInput] = None) -> Page[PostRead]:
        """
        获取当前用户的文章列表
        - 需要认证
        - 支持分页查询
        - 返回带分页信息的文章列表
        
        参数:
        - page_input: 可选的分页参数
          - page: 页码(默认1)
          - size: 每页大小(默认10)
        
        返回:
        - items: 文章列表
        - page_info: 分页信息
          - total: 总记录数
          - page: 当前页码
          - size: 每页大小
          - has_next: 是否有下一页
          - has_prev: 是否有上一页
        """
        # 认证检查
        if not info.context.get("user"):
            raise ValueError("Not authenticated")
        
        # 数据库操作
        db = SessionLocal()
        try:
            # 设置分页参数
            page = page_input.page if page_input else 1
            size = page_input.size if page_input else 10
            
            # 查询总数
            total = db.query(func.count(PostModel.id)).filter(
                PostModel.author_id == info.context["user"].id
            ).scalar()
            
            # 查询分页数据
            posts = (
                db.query(PostModel)
                .options(joinedload(PostModel.author))  # 预加载作者信息
                .filter(PostModel.author_id == info.context["user"].id)
                .order_by(PostModel.created_at.desc())  # 按创建时间倒序
                .offset((page - 1) * size)
                .limit(size)
                .all()
            )
            
            # 构建分页信息
            page_info = PageInfo(
                total=total,
                page=page,
                size=size,
                has_next=total > page * size,
                has_prev=page > 1
            )
            
            return Page(
                items=[post.to_read() for post in posts],
                page_info=page_info
            )
        finally:
            db.close()

    @strawberry.field
    def user_posts(self, username: str, page_input: Optional[PageInput] = None) -> Page[PostRead]:
        """
        获取指定用户的文章列表
        - 公开接口,无需认证
        - 支持分页查询
        - 返回带分页信息的文章列表
        
        参数:
        - username: 用户名
        - page_input: 可选的分页参数
        """
        # ... 实现类似 my_posts

@strawberry.type
class Mutation:
    @strawberry.mutation
    def login(self, login_data: LoginInput) -> LoginResponse:
        """
        用户登录
        - 公开接口,无需认证
        - 验证用户名密码
        - 生成访问令牌
        
        参数:
        - login_data:
          - username: 用户名
          - password: 密码
        
        返回:
        - token: 访问令牌
        - user: 用户信息
        """
        db = SessionLocal()
        try:
            # 查找用户
            user = db.query(UserModel).filter(UserModel.username == login_data.username).first()
            # 验证密码
            if not user or not user.verify_password(login_data.password):
                raise ValueError("Incorrect username or password")
            
            # 生成访问令牌
            access_token = create_access_token(data={"sub": str(user.id)})
            token = Token(access_token=access_token)
            
            return LoginResponse(token=token, user=user.to_read())
        finally:
            db.close()

    @strawberry.mutation
    def register(self, user_data: UserCreate) -> RegisterResponse:
        """
        用户注册
        - 公开接口,无需认证
        - 检查用户名和邮箱是否已存在
        - 创建新用户
        - 生成访问令牌
        
        参数:
        - user_data:
          - username: 用户名
          - password: 密码
          - email: 邮箱
        
        返回:
        - token: 访问令牌
        - user: 用户信息
        """
        # ... 实现代码

    @strawberry.mutation
    def create_post(self, post_data: PostCreate, info) -> PostRead:
        """
        创建文章
        - 需要认证
        - 创建新文章
        - 设置当前用户为作者
        
        参数:
        - post_data:
          - title: 标题
          - content: 内容
        
        返回:
        - 创建的文章信息
        """
        # ... 实现代码

schema = strawberry.Schema(query=Query, mutation=Mutation)

认证实现

1. JWT Token 生成

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """创建访问令牌"""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

2. 认证中间件

在 FastAPI 应用中实现认证中间件,用于解析和验证 token:

async def get_context(request: Request):
    """GraphQL 上下文处理器,用于认证"""
    auth_header = request.headers.get("Authorization")
    context = {"user": None}
    
    if auth_header and auth_header.startswith("Bearer "):
        token = auth_header.split(" ")[1]
        token_data = verify_token(token)
        if token_data:
            db = SessionLocal()
            try:
                user = db.query(UserModel).filter(UserModel.id == int(token_data["sub"])).first()
                if user:
                    context["user"] = user
            finally:
                db.close()
    
    return context

3. 认证流程

  1. 用户登录:
mutation Login {
  login(loginData: {
    username: "admin",
    password: "111111"
  }) {
    token {
      accessToken
    }
    user {
      id
      username
      email
    }
  }
}
  1. 服务器验证用户名密码,生成 JWT token

  2. 后续请求中使用 token:

    • 在请求头中添加:Authorization: Bearer your_token
    • 中间件解析 token 并验证
    • 将用户信息添加到 GraphQL context
  3. 在需要认证的操作中检查用户:

if not info.context.get("user"):
    raise ValueError("Not authenticated")

API 权限设计

1. 公开接口(无需认证)

  • hello: 测试接口
  • login: 用户登录
  • register: 用户注册
  • userPosts: 获取指定用户的文章列表

2. 私有接口(需要认证)

  • me: 获取当前用户信息
  • myPosts: 获取当前用户的文章列表
  • createPost: 创建新文章

使用示例

1. 登录获取 Token

mutation Login {
  login(loginData: {
    username: "admin",
    password: "111111"
  }) {
    token {
      accessToken
    }
  }
}

2. 使用 Token 访问私有接口

在 GraphQL Playground 中设置 HTTP Headers:

{
  "Authorization": "Bearer your_token"
}

然后可以查询私有数据:

query MyPosts {
  myPosts(pageInput: {
    page: 1,
    size: 10
  }) {
    items {
      id
      title
      content
    }
  }
}

安全考虑

  1. 密码安全

    • 使用 bcrypt 进行密码哈希
    • 从不存储明文密码
  2. Token 安全

    • 使用 JWT 标准
    • 设置合理的过期时间
    • 使用安全的签名算法
  3. 数据访问控制

    • 严格的权限检查
    • 用户只能访问自己的数据

总结

本项目展示了如何使用现代化的技术栈构建一个安全的 GraphQL API:

  1. 使用 FastAPI 提供高性能的 Web 服务
  2. 使用 Strawberry 实现 GraphQL API
  3. 使用 SQLAlchemy 进行数据库操作
  4. 实现了完整的认证机制
  5. 遵循了最佳安全实践

当然图片上传一类的,还要跟以前一样写,但现在我们只写了一个/api接口就完成了项目所有接口。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/961777.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C语言连接Mysql

目录 C语言连接Mysql下载 mysql 开发库 方法介绍mysql_init()mysql_real_connect()mysql_query()mysql_store_result()mysql_num_fields()mysql_fetch_fields()mysql_fetch_row()mysql_free_result()mysql_close() 完整代码 C语言连接Mysql 下载 mysql 开发库 方法一&#xf…

嵌入式知识点总结 Linux驱动 (二)-uboot bootloader

针对于嵌入式软件杂乱的知识点总结起来,提供给读者学习复习对下述内容的强化。 目录 1.什么是bootloader? 2.Bootloader的两个阶段 3.uboot启动过程中做了哪些事? 4.uboot和内核kernel如何完成参数传递? 5.为什么要给内核传递…

JVM对象分配内存如何保证线程安全?

大家好,我是锋哥。今天分享关于【JVM对象分配内存如何保证线程安全?】面试题。希望对大家有帮助; JVM对象分配内存如何保证线程安全? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在JVM中,对象的内存分配是通过堆内存进行的。…

利用飞书机器人进行 - ArXiv自动化检索推荐

相关作者的Github仓库 ArXivToday-Lark 使用教程 Step1 新建机器人 根据飞书官方机器人使用手册,新建自定义机器人,并记录好webhook地址,后续将在配置文件中更新该地址。 可以先完成到后续步骤之前,后续的步骤与安全相关&…

OpenCV:在图像中添加噪声(瑞利、伽马、脉冲、泊松)

目录 简述 1. 瑞利噪声 2. 伽马噪声 3. 脉冲噪声 4. 泊松噪声 总结 相关阅读 OpenCV:在图像中添加高斯噪声、胡椒噪声-CSDN博客 OpenCV:高通滤波之索贝尔、沙尔和拉普拉斯-CSDN博客 OpenCV:图像处理中的低通滤波-CSDN博客 OpenCV&…

github制作静态网页

打开gihub并新建仓库 命名仓库:xxx.github.io 点击create repository进行创建 点击蓝色字体“creating a new file”创建文件 文件命名为index.html, 并编写html 右上角提交 找到setttings/pages,修改路径,点击保存,等…

从 SAP 功能顾问到解决方案架构师:破茧成蝶之路

目录 行业瞭望:架构师崭露头角 现状剖析:功能顾问的局限与机遇 能力跃迁:转型的核心要素 (一)专业深度的掘进 (二)集成能力的拓展 (三)知识广度的延伸 &#xff0…

unity学习23:场景scene相关,场景信息,场景跳转

目录 1 默认场景和Assets里的场景 1.1 scene的作用 1.2 scene作为project的入口 1.3 默认场景 2 场景scene相关 2.1 创建scene 2.2 切换场景 2.3 build中的场景,在构建中包含的场景 (否则会认为是失效的Scene) 2.4 Scenes in Bui…

36、【OS】【Nuttx】OSTest分析(2):环境变量测试

背景 2025.1.29 蛇年快乐! 接之前wiki 35、【OS】【Nuttx】OSTest分析(1):stdio测试(五) 已经分析完了第一个测试项,输入输出端口测试,接下来分析下环境变量测试,也比较…

使用Ollama本地部署DeepSeek R1

前言 DeepSeek是一款开源的智能搜索引擎,能够通过深度学习技术提高搜索的智能化水平。如果你正在寻找一种方式来将DeepSeek部署在本地环境中,Ollama是一个非常方便的工具,它允许你在本地快速部署并管理各种基于AI的模型。 在本篇博客中&…

libOnvif通过组播不能发现相机

使用libOnvif库OnvifDiscoveryClient类, auto discovery new OnvifDiscoveryClient(QUrl(“soap.udp://239.255.255.250:3702”), cb.Build()); 会有错误: end of file or no input: message transfer interrupted or timed out(30 sec max recv delay)…

Visual Studio使用GitHub Copilot提高.NET开发工作效率

GitHub Copilot介绍 GitHub Copilot 是一款 AI 编码助手,可帮助你更快、更省力地编写代码,从而将更多精力集中在问题解决和协作上。 GitHub Copilot Free包含哪些功能? 每月 2000 代码补全,帮助开发者快速完成代码编写。 每月 …

HTB:Forest[WriteUP]

连接至HTB服务器并启动靶机 分配IP:10.10.16.21 靶机IP:10.10.10.161 靶机Domain:forest.htb 目录 连接至HTB服务器并启动靶机 信息收集 使用rustscan对靶机TCP端口进行开放扫描 将靶机TCP开放端口号提取并保存 使用nmap对靶机TCP开放端…

项目集成GateWay

文章目录 1.环境搭建1.创建sunrays-common-cloud-gateway-starter模块2.目录结构3.自动配置1.GateWayAutoConfiguration.java2.spring.factories 3.pom.xml4.注意:GateWay不能跟Web一起引入! 1.环境搭建 1.创建sunrays-common-cloud-gateway-starter模块…

STM32 PWM驱动直流电机

接线图: 代码配置: 根据驱动舵机的代码来写,与舵机不同的是,这次的引脚接到了PA2上,所以需要改一下引脚以及改为OC3通道。 另外还需在配置两个GPIO引脚,来控制电机的旋转方向,这里连接到了PA4与…

强大到工业层面的软件

电脑数据删不干净,简直是一种让人抓狂的折磨!明明已经把文件扔进了回收站,清空了,可那些残留的数据就像牛皮癣一样,怎么也除不掉。这种烦恼简直无处不在,让人从头到脚都感到无比烦躁。 首先,心…

一分钟搭建promehteus+grafana+alertmanager监控平台

为什么要自己搭建一个监控平台 平时进行后端开发,特别是微服务的后端可开发,一定少不了对接监控平台,但是平时进行一些小功能的测试又没有必要每次都手动安装那么多软件进行一个小功能的测试,这里我使用docker-compose搭建了一个…

记一次STM32编译生成BIN文件过大的问题(基于STM32CubeIDE)

文章目录 问题描述解决方法更多拓展 问题描述 最近在一个项目中使用了 STM32H743 单片机(基于 STM32CubeIDE GCC 开发),它的内存分为了 DTCMRAM RAM_D1 RAM_D2 …等很多部分。其中 DTCM 的速度是比通常的内存要快的,缺点是不支持…

前端-Rollup

Rollup 是一个用于 JavaScript 的模块打包工具,它将小的代码片段编译成更大、更复杂的代码,例如库或应用程序。它使用 JavaScript 的 ES6 版本中包含的新标准化代码模块格式,而不是以前的 CommonJS 和 AMD 等特殊解决方案。ES 模块允许你自由…

崇州市街子古镇正月初一繁华剪影

今天是蛇年正月初一,下午笔者步出家门,逛到了崇州市街子古镇井水街,想看看景象如何。结果看到的是车水马龙、人流如织,繁花似锦,热闹非凡,原来今天开始预订此地摆下的长街宴。心里高兴,便用手机…