Rust 从零手搓个人博客:构建属于自己的一方小世界

本文并非博客搭建教程,仅记录我的个人博客开发历程。如果你需要的是建站方法,那么这篇文章可能并不适合你。

起因

2019 年,我接触了 Rust 这门语言,并立即被其设计深深吸引。

在随后的学习和实践中,我深入体验了 Rust 生态下的多款 Web 框架,包括 actix-webrockettidewarp 以及目前社区主流的 axum 等。

我汲取了各框架的设计精髓,也反思了它们的短板与局限性,在这个过程中,构建一款全新 Web 框架的想法也逐渐成型。最终我将这份构想落地实践,从零打造了全新的 Rust Web 框架 boluo。它未必比现有框架更优秀,却完全契合我的设计理念与技术美学追求。

框架开发完成后,我选择开发个人博客系统作为验证载体:该项目体量适中,既能全面覆盖 Web 开发的核心场景(路由匹配、错误处理、数据库交互、权限控制等),充分验证框架的稳定性与易用性。同时,市面上现有的博客系统大多功能繁杂、过于臃肿,我更希望拥有一套简洁、只保留核心功能的博客系统。因此自研博客也能更好地满足我对简洁体验、数据安全和高度自定义的需求,打造完全由自己掌控的轻量化个人博客。

需求

除文章创建、编辑、发布、删除等博客系统核心功能外,我希望个人博客还能满足以下需求:

  • 支持使用 Markdown 格式编辑文章内容
  • 支持文章隐藏功能(仅管理员可见)
  • 支持文章访问保护,可设置文章访问密码
  • 支持文章附件,附件可见性与所属文章保持一致
  • 支持文章全文搜索(游客仅可搜索公开文章)
  • 支持双因素身份验证(2FA),强化账号安全
  • 支持 RSS 2.0 和 Atom 1.0 订阅

在部署和数据迁移方面,我的要求是:

  • 单文件运行:整个系统只需一个可执行文件即可启动,无需安装数据库、Redis 等外部依赖
  • 数据可移植:所有数据集中存放在一个目录中,迁移时只需打包该目录,复制到新环境即可完成数据迁移

技术选型

组件 选型 说明
编程语言 rust
异步运行时 tokio
Web 框架 boluo 自己开发的 Web 框架
数据库 sqlite (sqlx) 嵌入式,无需独立部署,迁移简单
模板引擎 tera
Markdown 渲染 comrak
双因素身份验证(2FA) totp

目录规划

├── config                  # 应用配置
│   ├── default.toml        # 默认配置(不建议修改)
│   └── user                # 用户自定义配置目录(优先级高于 default.toml)
│       ├── auth.toml       # 认证配置(密码、密钥、TOTP)
│       └── [mode].toml     # 环境专属配置(根据启动参数加载不同环境配置)
├── data                    # 核心业务数据(迁移/备份仅需打包此目录)
│   ├── myblog.sqlite       # SQLite 核心数据库文件
│   ├── public              # 站点公共文件(robots.txt、favicon.ico 等)
│   ├── theme               # 用户自定义主题(优先级高于 themes 目录)
│   │   ├── templates       # 自定义页面模板
│   │   └── code            # 自定义代码高亮
│   │       ├── syntax      # 自定义语法定义
│   │       └── themes      # 自定义高亮配色
│   ├── trash               # 孤立资源回收站
│   └── upload              # 用户上传资源(配图、附件、头像等)
├── sqlite                  # SQLite 数据库相关
│   ├── extensions          # SQLite 扩展插件
│   └── migrations          # 数据库迁移脚本
├── log                     # 应用日志目录
└── themes                  # 主题目录
    └── simple              # 默认极简主题
        ├── assets          # 静态资源(CSS/JS/图片等)
        ├── code            # 代码高亮
        │   ├── syntax      # 语法定义
        │   └── themes      # 高亮配色
        └── templates       # 页面模板

用户体系设计

这是 “个人” 博客系统,仅面向一人使用,其余访问者均为游客身份。

基于「单人使用」的核心设定,系统无需设计用户表,也无需创建传统意义上的 “账号” 体系:管理员密码直接存储在配置文件中,登录环节仅需验证该密码即可完成身份确认,大幅简化权限管理逻辑。

尽管简化了账号体系,但仅靠密码验证的安全风险较高,既容易被暴力破解,也可能因密码复杂度不足被猜测。为此,在密码验证之外补充了 TOTP 二次认证,通过双因素验证补齐安全短板,并对登录次数进行限制。

数据库设计

系统核心数据存储基于 SQLite 实现,共设计 8 张数据表,具体清单如下:

表名 功能
article_attachment 存储文章附件元数据
article_stats 记录文章访问信息
article_fts 实现文章全文搜索
article_fts_public 实现文章全文搜索(公开文章)
article 存储文章数据
cache 存储缓存数据
resource 存储上传文件元数据
failed_attempts 通用的失败次数统计

cache

为满足「单文件运行、无外部依赖」的核心诉求,放弃 Redis 等外置缓存组件,我设计了 cache 表,将缓存功能交给 SQLite 负责。为兼顾扩展性,我还编写了一个缓存抽象层。未来若需要迁移至 Redis 等缓存后端,仅需新增对应适配器实现,无需修改业务层代码,完全解耦缓存逻辑与底层存储。

此外,针对 cache 表中产生的过期数据,我在系统中编写了定时任务,自动清理 cache 表中的过期数据。

article_attachment

将文章和资源(resource)进行关联,实现附件可见性与所属文章一致。

failed_attempts

用于记录各类敏感操作的失败次数,核心作用是防范暴力破解与恶意攻击。当同一来源在指定周期内的失败次数达到设定阈值时,系统将自动触发临时锁定机制,禁止该来源继续尝试,从而有效抵御暴力猜解密码等恶意行为。

核心功能实现

全文搜索

得益于 SQLite 的 FTS5,我们可以轻松实现全文检索功能。但是 SQLite 自带的分词器对中文的支持不是很好,所以我集成了 Simple Tokenizer 分词器扩展,让博客同时支持中文和拼音检索。

article_fts 表和 article 表的同步则使用 TRIGGER(触发器)自动完成,无需在应用内手动维护同步逻辑,既简化了开发流程,也避免了数据不一致的问题。

页面渲染

页面我选择使用服务端渲染,既能有效提升首屏加载速度,也有利于搜索引擎的抓取与收录,非常适合个人博客系统。

为此,我需要挑选一款合适的模板引擎,好在 Rust 生态中拥有丰富的模板引擎可供选择,如:askamahandlebarstera 等。

askama 属于编译时模板引擎,模板会在 Rust 编译阶段被解析并生成代码嵌入二进制文件,修改或切换主题必须重新编译项目,无法支持用户自定义的需求,因此不适合本项目。

handlebarstera 均为运行时模板引擎,修改模板无需重新编译,能够满足用户自定义的需求。但 handlebars 在模板内的逻辑表达能力较弱,灵活性不及 tera

由于应用后端输出给模板引擎的数据结构固定,为了让主题拥有更强的表现力与设计自由度,我最终选择了逻辑表达能力更丰富的 tera

Markdown 的解析与渲染

博客文章内容采用 Markdown 格式编写,为了实现标准、高效且可扩展的渲染效果,我选用了 Rust 生态中成熟稳定的 comrak 库作为解析引擎。

comrak 集成 syntect 实现代码块的语法高亮,让代码的展示更加清晰美观。同时支持使用 tmTheme 和 sublime-syntax 文件自定义代码高亮主题与语法解析规则,让代码块的渲染具备高度可定制性。

此外,comrak 还支持自定义输出的 HTML 格式,让我可以灵活调整渲染后的 HTML 内容,使最终展示效果更贴合预期。

敏感操作的失败次数限制

最初,我的想法是通过 cache 表来实现这一功能:在缓存中写入来源 IP 和操作对象,利用缓存数据的过期时间来控制检测窗口。这种实现方式逻辑清晰,看起来十分优雅。

然而,在编写缓存抽象层时,为了使其能够适配更多缓存后端,我决定让抽象层不支持原子操作。但失败次数的累加操作必须是原子性的,否则在并发场景下可能出现数据竞态,导致统计结果失真。

因此,我最终放弃了 cache 表的方案,转而创建独立的 failed_attempts 表进行专门的处理,确保失败次数的累加是原子的,避免并发场景下数据竞态导致统计结果失真。

请求大小限制

通过中间件捕获请求对象,核心实现包含两个步骤:校验 Content-Length 请求头,超出限制时直接返回 413 状态码;若请求头不存在或未超出阈值,则为请求体设置数据读取上限。

为提升灵活性,我还支持按接口单独配置请求体大小限制:

toml
### ----------------------------------------------------------------------------
### 请求体大小限制配置
### ----------------------------------------------------------------------------
[body_limit]
### 默认请求体大小限制
default_limit = "2MiB"
### 自定义请求体大小限制规则列表
rules = [
    { path = "/api/resource/upload", method = "POST", limit = "100MiB" },
    { path = "/api/attachment/upload", method = "POST", limit = "500MiB" },
]

为保证配置中的路由语法、匹配规则与 boluo 框架原生路由完全一致,我复用了框架的路由能力:在中间件内部,基于配置文件动态构建 boluo 的路由对象,将请求大小限制逻辑封装为独立端点,并为每个端点注入对应路由的限制阈值。

当请求进入时,中间件会先拦截 Request 对象,将其转发至该路由中,依托框架原生的路由匹配机制完成大小限制校验。该方案无需额外开发路由匹配逻辑,完美兼容框架原生行为,实现简洁且稳定性更高。

注:「核心功能实现」仍有诸多细节未能展开。后续若有时间,我将继续补充完善,也欢迎感兴趣的朋友通过项目源码先行探索。

结语

不同于直接使用成熟框架搭建博客,自研 boluo 框架 + 定制博客的方式,虽然付出了更多的时间和精力,但也让我对 Web 开发的底层逻辑有了更深入的理解。

这款博客没有追求过多花哨的功能,而是聚焦于“好用、简洁、可控”,完全贴合我个人的使用需求。开发过程中,遇到的每一个问题,都是一次成长的机会。

项目地址