场景:中途部署过旧版二进制时 AutoMigrate 会把 attachments 以 id=1 重新编号;此前按 id 对齐的 NOT EXISTS 会误判已迁移, 导致新上传行静默丢失。现在第二段按 stored_name(内容 SHA-256) 补齐 id 冲突的行(省略 id 由数据库重新分配,内容已登记则跳过)。
Go Blog
一个简洁、快速的博客引擎,使用 Go + Gin + Tailwind CSS 构建。支持 SQLite 和 MySQL,首次运行自动完成配置。
功能特性
- 文章管理 — 文章的创建、编辑、删除,支持标签分类、自定义发布时间和更新时间
- Markdown 渲染 — 文章与评论支持 GFM Markdown(表格、任务列表、删除线),代码块语法高亮 + 一键复制,标题锚点链接,图片懒加载与灯箱预览
- 评论系统 — 文章评论功能,后台可审核(通过/拒绝/删除),支持评论设置
- 用户系统 — 用户注册、登录,角色分为管理员(admin)和作者(author)
- 角色权限 — 管理员可访问后台管理面板;作者可管理自己的文章
- 个人中心 — 上传头像(支持裁剪)、编辑个人信息、修改密码
- 站点设置 — 自定义站点标题、副标题、favicon 图标
- 导航管理 — 自定义首页导航链接,支持图标
- 瀑布流布局 — 首页文章以瀑布流展示,支持无限滚动加载
- 阅读统计 — 文章阅读量统计,带机器人流量检测
- 附件上传 — 文章支持上传附件,基于内容寻址自动去重
- RSS 订阅 — 自动生成 RSS Feed(
/rss、/feed),链接使用站点设置的规范地址 - 搜索功能 — 文章全文搜索
- 多语言 — 支持中文 / English,自动检测浏览器语言或手动切换
- 自适应界面 — Tailwind CSS,桌面端和移动端均可正常使用
- 安全加固 — 会话密钥加密随机、Cookie HttpOnly/SameSite=Lax/HTTPS Secure、全站 CSRF、SQL 注入防护(参数化+路由 ID 数值化)、
/uploads白名单挂载(SQLite 数据库不可下载)、登录限速(IP+用户名,5 次失败锁 15 分钟)、附件越权校验、上传安全(危险扩展黑名单+magic-bytes 检测+头像 JPEG 重编码)、Gravatar 默认关闭、禁用用户会话实时失效,完整清单见 SECURITY_TODO.md - 开箱即用 — 首次运行自动创建配置文件、数据库;管理员账号密码为随机生成并仅一次性打印(不再使用 admin/admin)
技术栈
| 层级 | 库/工具 |
|---|---|
| 路由 | gin |
| ORM | gorm |
| SQLite | glebarez/sqlite(纯 Go,无需 CGO) |
| Session | gin-contrib/sessions |
| 配置 | gopkg.in/yaml.v3 |
| 密码 | golang.org/x/crypto |
| 文件校验 | gabriel-vasile/mimetype |
| CSS | Tailwind CSS(构建期静态生成,见 scripts/build_tailwind.sh) |
快速开始
go run .
打开 http://localhost:8080 登录。首次运行时会自动创建管理员账号 admin,初始密码为随机生成并仅一次性打印在日志中——请立即记录并登录后修改(SECURITY_TODO #12,不再使用默认 admin/admin)。
前端资源全部本地化(
static/css/app.css为 Tailwind 静态构建产物,已提交)。修改 HTML 模板/Go 代码中的 Tailwind 类后,运行./scripts/build_tailwind.sh(需 Node ≥ 18,npx 可用)重新生成并提交新产物;vendor 库更新同理重新下载到static/vendor/并提交。
安全状态一览见下方 安全加固 章节,完整修复清单与验证记录见 SECURITY_TODO.md。
配置
首次运行时,配置文件会自动生成:
| 平台 | 配置文件路径 | 数据存储路径 |
|---|---|---|
| Linux | /etc/blog_go/config.yaml |
/srv/blog_go/ |
| Windows | ./win/etc/blog_go/config.yaml |
./win/srv/blog_go/ |
已存在的配置文件若缺少配置项(例如升级前的旧文件没有 database 段),启动时会自动用默认值补齐并写回。
# config.yaml
database:
type: sqlite # sqlite(默认)或 mysql
db_name: "" # MySQL 数据库名,sqlite 模式下忽略
username: "" # MySQL 用户名,sqlite 模式下忽略
password: "" # MySQL 密码,sqlite 模式下忽略
host: "" # MySQL IP 或主机名,sqlite 模式下忽略
port: "" # MySQL 端口,sqlite 模式下忽略
web:
port: "8080" # Web 服务端口,"" 或 "0" 可只启用 socket
socket: "" # unix socket 路径(Linux 部署推荐,见 install_linux.sh)
trusted_proxies: # 可信反向代理 IP/CIDR;直接影响 X-Forwarded-For
- 127.0.0.1 # 仅列表内的代理可设置客户端 IP(防 XFF 伪造)
- ::1
path: ./win/srv/blog_go # 数据存储路径(数据库、上传文件)
secret: <自动生成> # Session 加密密钥;缺失时拒绝启动
使用 MySQL
编辑 config.yaml:
database:
type: mysql
db_name: blog_go
username: user
password: password
host: 127.0.0.1
port: "3306"
web:
port: "8080"
先创建数据库:
CREATE DATABASE blog_go CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
项目结构
| 目录 | 作用 |
|---|---|
config/ |
配置加载与自动创建(区分 Linux/Windows 路径) |
models/ |
数据模型:用户、文章、标签、评论、附件、阅读统计、站点设置等 + GORM 初始化 |
handlers/ |
HTTP 请求处理:首页、文章、登录/注册、评论、后台管理、个人中心、RSS、上传校验 |
middleware/ |
中间件:登录/角色鉴权、CSRF、HTTPS 检测、安全响应头(CSP/HSTS 等) |
i18n/ |
中英文翻译与浏览器语言检测 |
templates/ |
HTML 模板:layouts 公共布局、pages 前台、admin 后台、user 用户文章 |
static/ |
静态资源:Tailwind 产物、前端 Markdown 渲染器、vendor 本地化第三方库(marked、highlight.js 等) |
scripts/ |
构建辅助脚本(Tailwind 重建、数据库迁移 SQL) |
根目录文件:main.go(入口 + 路由注册)、main_test.go 与 handlers/_test.go(测试)、fresh.conf(fresh 热重载配置)、install_linux.sh(Linux 部署脚本)。逐文件说明:
go_blog/
├── main.go # 入口,路由注册,中间件装配
├── config/
│ └── config.go # 配置加载 / 自动创建(区分操作系统)
├── models/
│ ├── user.go # 用户模型 + bcrypt
│ ├── article.go # 文章模型
│ ├── article_tag.go # 文章-标签关联
│ ├── article_view.go # 文章阅读统计
│ ├── attachment.go # 附件模型
│ ├── bot_detector.go # 机器人流量检测
│ ├── comment.go # 评论模型
│ ├── comment_config.go # 评论配置
│ ├── config_cache.go # 平台配置缓存
│ ├── db.go # GORM 初始化、自动迁移
│ ├── nav_link.go # 导航链接模型
│ ├── seed.go # 初始数据填充
│ ├── site_setting.go # 站点设置模型
│ ├── tag.go # 标签模型
│ └── upload_config.go # 上传配置
├── middleware/
│ ├── auth.go # 登录验证、角色鉴权、语言检测
│ ├── csrf.go # CSRF 同步器令牌防护
│ ├── https.go # 请求是否 HTTPS 检测(cookie Secure)
│ └── security_headers.go # 安全响应头(CSP、HSTS、nosniff 等)
├── handlers/
│ ├── helpers.go # 公共工具函数
│ ├── home.go # 首页
│ ├── article.go # 文章详情、文章列表 API
│ ├── auth.go # 登录
│ ├── login_ratelimit.go # 登录限速(IP+用户名,5 次失败锁定 15 分钟)
│ ├── admin.go # 管理后台首页
│ ├── admin_comment.go # 评论管理
│ ├── admin_user.go # 用户管理
│ ├── admin_analytics.go # 阅读统计
│ ├── attachment.go # 附件上传/删除
│ ├── comment.go # 评论提交
│ ├── my_articles.go # 用户文章管理
│ ├── profile.go # 个人中心
│ ├── rss.go # RSS Feed
│ ├── settings.go # 站点/导航/上传/评论设置
│ └── upload_validator.go # 上传文件校验(扩展名白名单 + magic-bytes)
├── i18n/
│ └── i18n.go # 中英文翻译映射 + Accept-Language 检测
├── static/
│ ├── css/app.css # Tailwind 静态构建产物(go:embed 编入二进制)
│ ├── css/input.css # Tailwind 构建输入(@tailwind 指令)
│ ├── css/markdown.css # Markdown 排版样式(go:embed 编入二进制)
│ ├── js/markdown.js # 前端 Markdown 渲染器(BlogMD)
│ └── vendor/ # 本地化的第三方前端库(marked/DOMPurify/highlight.js/cropperjs/easymde)
├── scripts/
│ └── build_tailwind.sh # 重新生成 Tailwind CSS(需 Node,见下)
├── templates/
│ ├── layouts/base.html # 公共布局(导航栏 + 头像下拉菜单 + 页脚)
│ ├── pages/
│ │ ├── home.html # 首页(瀑布流 + 无限滚动)
│ │ ├── article.html # 文章详情页
│ │ ├── login.html # 登录页
│ │ ├── register.html # 注册页
│ │ ├── profile.html # 个人中心
│ │ ├── search.html # 搜索页
│ │ └── 404.html # 404 页面
│ ├── admin/
│ │ ├── dashboard.html # 管理后台首页
│ │ ├── article_list.html # 文章列表
│ │ ├── article_create.html# 文章编辑
│ │ ├── comment_list.html # 评论审核
│ │ ├── user_list.html # 用户列表
│ │ ├── user_form.html # 用户表单
│ │ ├── analytics_views.html # 阅读统计
│ │ ├── settings_site.html # 站点设置
│ │ ├── settings_navlinks.html # 导航链接设置
│ │ ├── settings_upload.html # 上传设置
│ │ ├── settings_download.html # 下载设置
│ │ └── settings_comment.html # 评论设置
│ └── user/
│ ├── my_articles.html # 我的文章列表
│ └── my_article_form.html # 我的文章编辑
└── win/ # 运行时数据目录(Windows,自动创建)
├── etc/blog_go/
│ └── config.yaml
└── srv/blog_go/
├── blog.db
├── attachments/
├── avatars/
└── logos/
安全加固
基于 2026-08-19 安全审计与网络上线验证(共 25 项发现,P0–P3 均已修复,详见 SECURITY_TODO.md):
- 认证与会话
- 会话密钥由
crypto/rand生成 32 字节随机数;配置文件缺失 secret 时拒绝启动(不再静默回退) - Cookie 加固:
HttpOnly+SameSite=Lax,HTTPS 下自动加Secure - 全站 CSRF 防护(同步器令牌,30+ 表单与 AJAX 全覆盖);登录/注册成功强制会话轮换(防会话固定)
- 登录限速:按 IP+用户名 5 次失败锁定 15 分钟;用户不存在时也执行 bcrypt 比较,抹平计时侧信道
- 禁用/锁定/软删用户的会话实时失效;管理员口令 bcrypt cost 12
- 会话密钥由
- 数据与注入
- GORM 全参数化查询;管理路由的
:id先解析为数值再入查询(防字符串条件注入) /uploads仅白名单挂载子目录,SQLite 数据库文件不可从公网下载;目录列表与路径穿越一律 404
- GORM 全参数化查询;管理路由的
- 上传安全
- 扩展名白名单 + 危险扩展名黑名单(.html/.svg/.js 等,防同源 Active Content)
- 内容 magic-bytes 与声明类型一致性校验(gabriel-vasile/mimetype)
- 头像强制解码→256px 缩放→JPEG 重编码后落盘,原始字节一律不落地
- 输出与传输
- CSP(
default-src 'self',第三方前端资源已本地化)、X-Frame-Options: DENY、X-Content-Type-Options: nosniff、Referrer-Policy、Permissions-Policy;HTTPS 下发 HSTS - 客户端 IP 解析仅信任
web.trusted_proxies名单内代理(防 X-Forwarded-For 伪造);RSS 链接使用站点设置的规范地址(防 Host 头污染)
- CSP(
安全回归用例 50+(handlers/*_test.go、main_test.go、middleware/*_test.go),go test -race ./... 全绿。
路由
公开路由
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | / |
首页 |
| GET | /search |
搜索页 |
| GET | /article/:slug |
文章详情 |
| GET | /rss、/feed |
RSS 订阅 |
| GET | /login |
登录页 |
| GET | /register |
注册页 |
| GET | /uploads/* |
静态文件(头像、附件等) |
公开 JSON API(/api 前缀)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/articles |
文章列表 API(无限滚动) |
| POST | /api/auth/login |
登录(限流锁定 429) |
| POST | /api/auth/register |
注册(用户名冲突 409) |
| POST | /api/auth/logout |
退出登录 |
| POST | /api/article/:slug/comments |
发表评论(校验错误 400) |
管理后台(需管理员权限)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /admin |
管理后台首页 |
| GET | /admin/articles |
文章列表 |
| GET | /admin/articles/new |
新建文章表单 |
| GET | /admin/articles/:id/edit |
编辑文章表单 |
| GET | /admin/comments |
评论管理 |
| GET | /admin/users |
用户列表 |
| GET | /admin/users/new |
新建用户表单 |
| GET | /admin/users/:id/edit |
编辑用户表单 |
| GET | /admin/analytics/views |
阅读统计 |
| GET | /admin/settings/site、/navlinks、/upload、/download、/comments |
各设置页 |
管理 JSON API(仅管理员,均带 JSON 错误码 + HTTP 语义状态码):
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/admin/articles |
创建文章 |
| PUT/DELETE | /api/admin/articles/:id |
更新/删除文章 |
| POST | /api/admin/articles/attachments |
上传附件(multipart) |
| DELETE | /api/admin/articles/attachments/:id |
删除附件 |
| GET | /api/admin/articles/:id/attachments |
附件列表 |
| POST | /api/admin/comments/:id/approve、/:id/reject、/:id/delete |
评论审核操作 |
| POST/PUT/DELETE | /api/admin/users、/:id |
用户增改删(自禁/最后管理员均 403) |
| POST | /api/admin/settings/site |
站点设置(文本/URL/清除) |
| POST | /api/admin/settings/site/favicon、/site/logo |
favicon/logo 上传(multipart) |
| POST | /api/admin/settings/navlinks、/upload、/download、/comments |
各设置保存(action 分发) |
个人中心(需登录)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /profile |
编辑个人信息页 |
| POST | /api/profile |
保存资料(JSON,密码改密校验 400) |
| POST | /api/profile/avatar |
上传头像(multipart、JPEG 重编码) |
| GET | /my/articles |
我的文章列表 |
| GET | /my/articles/new、/:id/edit |
我的文章表单页 |
| POST/PUT/DELETE | /api/my/articles、/:id |
文章增改删(跨作者 404/403) |
| POST | /api/my/articles/attachments |
上传附件(multipart) |
| DELETE | /api/my/articles/attachments/:id |
删除附件 |
| GET | /api/my/articles/:id/attachments |
附件列表 |
API 约定
- 成功:
{"ok": true, "redirect": "...", "data": {...}}(redirect为原 302 目标,前端 fetch 后跳转) - 失败:
{"ok": false, "code": "<i18n 键>", "error": "<按请求语言翻译的文案>"};状态码:400 校验失败 / 401 未登录 / 403 无权限 / 404 不存在 / 409 冲突 / 429 限流锁定 / 500 服务错误 - 除文件上传(multipart)外请求体为
application/json;CSRF 通过X-CSRF-Token请求头传递
用户角色
| 角色 | 个人中心 | 我的文章 | 管理后台 |
|---|---|---|---|
admin |
✓ | ✓ | ✓ |
author |
✓ | ✓ | ✗ |
许可证
MIT