本文并非博客搭建教程,仅记录我的个人博客开发历程。如果你需要的是建站方法,那么这篇文章可能并不适合你。
起因
2019 年,我接触了 Rust 这门语言,并立即被其设计深深吸引。
在随后的学习和实践中,我深入体验了 Rust 生态下的多款 Web 框架,包括 actix-web、rocket、tide、warp 以及目前社区主流的 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 生态中拥有丰富的模板引擎可供选择,如:askama、handlebars、tera 等。
askama 属于编译时模板引擎,模板会在 Rust 编译阶段被解析并生成代码嵌入二进制文件,修改或切换主题必须重新编译项目,无法支持用户自定义的需求,因此不适合本项目。
handlebars 与 tera 均为运行时模板引擎,修改模板无需重新编译,能够满足用户自定义的需求。但 handlebars 在模板内的逻辑表达能力较弱,灵活性不及 tera。
由于应用后端输出给模板引擎的数据结构固定,为了让主题拥有更强的表现力与设计自由度,我最终选择了逻辑表达能力更丰富的 tera。
Markdown 的解析与渲染
博客文章内容采用 Markdown 格式编写,为了实现标准、高效且可扩展的渲染效果,我选用了 Rust 生态中成熟稳定的 comrak 库作为解析引擎。
comrak 集成 syntect 实现代码块的语法高亮,让代码的展示更加清晰美观。同时支持使用 tmTheme 和 sublime-syntax 文件自定义代码高亮主题与语法解析规则,让代码块的渲染具备高度可定制性。
此外,comrak 还支持自定义输出的 HTML 格式,让我可以灵活调整渲染后的 HTML 内容,使最终展示效果更贴合预期。
敏感操作的失败次数限制
最初,我的想法是通过 cache 表来实现这一功能:在缓存中写入来源 IP 和操作对象,利用缓存数据的过期时间来控制检测窗口。这种实现方式逻辑清晰,看起来十分优雅。
然而,在编写缓存抽象层时,为了使其能够适配更多缓存后端,我决定让抽象层不支持原子操作。但失败次数的累加操作必须是原子性的,否则在并发场景下可能出现数据竞态,导致统计结果失真。
因此,我最终放弃了 cache 表的方案,转而创建独立的 failed_attempts 表进行专门的处理,确保失败次数的累加是原子的,避免并发场景下数据竞态导致统计结果失真。
请求大小限制
通过中间件捕获请求对象,核心实现包含两个步骤:校验 Content-Length 请求头,超出限制时直接返回 413 状态码;若请求头不存在或未超出阈值,则为请求体设置数据读取上限。
为提升灵活性,我还支持按接口单独配置请求体大小限制:
### ----------------------------------------------------------------------------
### 请求体大小限制配置
### ----------------------------------------------------------------------------
[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 开发的底层逻辑有了更深入的理解。
这款博客没有追求过多花哨的功能,而是聚焦于“好用、简洁、可控”,完全贴合我个人的使用需求。开发过程中,遇到的每一个问题,都是一次成长的机会。