26.9(安全审计修复版)

Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务:

- 安全审计全部修复(docs/security-audit-2026-09-05.md):
  bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、
  全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、
  管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、
  会话默认 7 天、限流缓存故障降级、robots.txt 端点等
- 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固
- Redis 支持库号(FCB_REDIS_DB / redis://…/db URL)
- 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、
  提码 5 位起、chunk 32MiB 上限、admin 审计动作等)

验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
This commit is contained in:
2026-09-05 04:22:41 +08:00
commit 7f060dd0e4
173 changed files with 32455 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
# 文件快传 Go 后端(server/
Go 1.27.1 + Gin + GORM 重写的文件快传后端。数据库**默认 SQLite**modernc.org/sqlite
纯 Go 驱动,CGO_ENABLED=0 交叉编译友好,零外部依赖),配置 `FCB_DB_DRIVER=postgres`
切换 Postgres(需求 ⑧);Redis 为**可选**增强(未配置 `FCB_REDIS_ADDR` 时自动降级为进程内存缓存)。
## 目录结构
```
server/
├── cmd/server/main.go # 入口:装配 配置→DB→缓存→设置→审计→中间件→路由
└── internal/
├── config/ # 配置:FCB_* 环境变量基线 + DB settings KV 运行时覆盖
│ └── schema.go # v2 新配置键 schema(键名/类型/默认值/边界)
├── model/ # GORM 模型 + 双方言 AutoMigrate
├── cache/ # 缓存统一接口:redis.go / memory.go 双实现
├── database/ # 双方言连接与迁移(sqlite 默认 / postgres 可选)
├── settings/ # settings KV 读写、密码哈希(sha256$salt$hash)、键 schema re-export
├── audit/ # 审计服务(Sink 抽象、失败重试队列)+ UA 设备解析
├── middleware/ # JWT 认证、IP 限流、审计中间件、CORS、IP 解析
├── storage/ # 存储引擎契约(interface.go 为 go-storage 的实现契约)
└── response/ # 统一响应 {"code":200,"msg":"","data":...}
```
## 环境变量
| 变量 | 必需 | 默认 | 说明 |
|---|---|---|---|
| `FCB_DB_DRIVER` | ❌ | `sqlite` | 数据库驱动:`sqlite` \| `postgres`(需求 ⑧) |
| `FCB_DB_DSN` | 视驱动 | `./data/filecodebox.db` | postgres:连接串(**必需**),如 `postgres://user:pass@host:5432/filecodebox?sslmode=disable`;sqlite:数据库文件路径(可空,自动创建 `data/` 目录) |
| `FCB_REDIS_ADDR` | ❌ | 空 | 为空时缓存降级为内存实现 |
| `FCB_LISTEN` | ❌ | `:8466` | 监听地址 |
| `FCB_STORAGE_ENGINE` | ❌ | `local` | `local` \| `s3` \| `webdav` |
| `FCB_TRUSTED_PROXIES` | ❌ | 空 | 可信代理 CIDR(逗号分隔),用于解析真实客户端 IP |
### 数据库模式(需求 ⑧)
- **SQLite(默认)**`FCB_DB_DRIVER=sqlite`(或缺省)。零 DSN 零依赖启动,数据库文件
默认 `./data/filecodebox.db``FCB_DB_DSN` 可覆盖路径;父目录自动创建)。
连接参数:`busy_timeout=10s` + `WAL` 日志模式 + `foreign_keys=1`(通过 DSN pragma 注入)。
- **Postgres(可选)**`FCB_DB_DRIVER=postgres` 且必须提供 `FCB_DB_DSN`,否则启动报错。
连接池沿用 v1 参数(32/8、1h 轮换)。
- 双方言共用 GORM 抽象:AutoMigrate、settings KV、全部业务查询方言无关;
唯一原生 DDLmigrates 台账表)在 `database.createMigratesTable` 内部分支处理。
## v2 新增配置键(internal/config/schema.go 为单一事实来源)
| 键 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `background_url` | string | `""` | 需求 ①:背景图 URL/上传地址(空=主题默认;legacy `background` 键兜底) |
| `footer_text` | string | `""` | 需求 ②:页脚自定义内容(≤2000 字符) |
| `footer_beian` | string | `""` | 需求 ②:备案号(≤128 字符) |
| `notify_enabled` | int | `1` | 需求 ③:通知开关(1 开 / 0 关) |
| `notify_title` / `notify_content` | string | 见 defaults | 需求 ③:通知标题/内容(沿用参考实现语义) |
| `max_save_seconds` | int64 | `0` | 需求 ④:最长保存秒数上限(0=仅默认 7 天兜底,≤365 天) |
| `max_save_count` | int | `0` | 需求 ④:单次分享最大可取次数上限(0=不限制,≤100000) |
| `max_file_size` | int64 | `0` | 需求 ⑩:存储策略-单文件上限字节(0=回落 `uploadSize`,≤10GiB |
| `uploadSize` / `allowed_file_types` / `storageLimit` / `openUpload` | 既有 | - | 存储策略既有键(语义不变) |
| `uploadCount` / `uploadMinute` | 既有 | `10` / `1` | 上传频率限制(对齐参考 `ip_limit["upload"]` |
管理端与文档(t2/t4)以 `config.KVSchema()``settings.KVSchema()` re-export)为元数据源;
schema 同步测试保证 `KVSchema()``defaults()` 逐键一致。
## 数据模型
- `file_codes`:文件/文本分享(对齐参考 `apps/base/models.py::FileCodes`
- `upload_chunks`:分片上传记录
- `key_values`:运行时配置 KV`settings` / `sys_start` 键)
- `presign_upload_sessions`:预签名直传会话
- `storage_reservations`:上传容量预留
- `audit_logs`:上传/下载审计(时间/IP/UA/设备/动作/结果/字节数/耗时),需求 ③
- `migrates`:迁移台账表(双方言 DDL 分支,见 `database.createMigratesTable`
## 存储引擎契约(internal/storage/interface.go
`go-storage` 按此契约实现 local/s3/webdav 三引擎(**接口签名已冻结**):
```go
type Storage interface {
SaveFile(ctx, r io.Reader, savePath) (int64, error) // 流式保存
DeleteFile(ctx, savePath) error
Open(ctx, savePath, rng *Range) (*Download, error) // Range 下载
Stat(ctx, savePath) (*FileMeta, error)
SaveChunk(ctx, uploadID, chunkIndex, r io.Reader, savePath) (int64, error)
MergeChunks(ctx, uploadID, total, verifyHash, savePath) (int64, string, error)
CleanChunks(ctx, uploadID, savePath) error
FileExists(ctx, savePath) (bool, error)
PresignGetURL(ctx, savePath, expires) (string, error) // 不支持→ErrNotSupported
PresignPutURL(ctx, savePath, expires) (string, error)
HealthCheck(ctx) error
}
```
错误映射约定:`ErrNotFound`→404、`ErrInvalidPath`→400、`ErrUnavailable`→503、`ErrNotSupported`→501、`ErrRangeNotSatisfiable`→416。
引擎通过 `storage.RegisterEngine("local"|"s3"|"webdav", factory)` 注册,
`storage.NewEngine(ctx, name)` 构造。分片路径约定:`<父目录>/chunks/<uploadID>/<index>.part`
### 引擎构造选项(t3 API 层接入)
构造引擎前必须先注入选项(否则 local 退到系统临时目录、s3/webdav 因缺配置失败):
```go
storage.SetEngineOptions(storage.EngineOptions{
Local: storage.LocalOptions{Root: cfg.GetString("local_storage_path")},
S3: storage.S3Options{
AccessKeyID: cfg.GetString("s3_access_key_id"), SecretAccessKey: cfg.GetString("s3_secret_access_key"),
Bucket: cfg.GetString("s3_bucket_name"), Endpoint: cfg.GetString("s3_endpoint_url"),
Region: cfg.GetString("s3_region_name"), AddressingStyle: cfg.GetString("s3_addressing_style"),
},
WebDAV: storage.WebDAVOptions{
BaseURL: cfg.GetString("webdav_url"), Username: cfg.GetString("webdav_username"),
Password: cfg.GetString("webdav_password"), RootPath: cfg.GetString("webdav_root_path"),
},
})
st, err := storage.NewEngine(ctx, cfg.Engine()) // 构造后调 st.HealthCheck(ctx) 完成启动自检
```
引擎要点:local 原子写(临时文件+fsync+rename)、防穿越+符号链接逃逸校验;
s3 原生 multipart 流式合并、预签名直链、SDK 内置 5xx 重试;webdav 连接池复用、
Basic/Digest 自动协商、Range 透传、按需逐级 MKCOL(带缓存)、5xx/429 指数退避重试、
下载 io.Pipe 流式不落盘。三引擎均支持分片上传/合并(SHA256 校验)与 Range 下载。
## 审计埋点用法(API 层)
```go
r.Use(middleware.Audit(auditSvc, nil)) // nil=默认按路由前缀分类 upload/download
// handler 内填充业务字段并显式落库(推荐):
middleware.AuditSet(c, func(e *audit.Entry) { e.FileCode = code; e.FileName = name })
middleware.AuditRecordRequest(c, auditSvc, model.AuditResultSuccess, "")
// 或不显式落库:中间件按 HTTP 状态兜底(4xx/5xx→failed401/403/423/429/428→denied
```
下载响应字节数由中间件自动统计;上传字节数由 handler 填 `TransferredBytes`
## 限流语义(对齐参考实现)
- `error`(取件错误)/`login`(登录失败):**仅在失败时计数**,handler 调用 `limiter.Add(c, kind)`
- `upload`**成功上传才计数**(先 `Check` 放行,成功后 `Add`
- `metadata`:每次访问即计数,可用 `RequireRateLimit` 中间件
- 超限返回 HTTP 423;规则来自 settings KVerrorCount/errorMinute 等),可运行时调整
## 本地开发
```bash
# 默认 SQLite 模式:零依赖,数据库落 ./data/filecodebox.db
go run ./cmd/server # 启动于 :8466
curl localhost:8466/api/v1/health
# Postgres 模式(可选)
export FCB_DB_DRIVER=postgres
export FCB_DB_DSN='postgres://postgres:postgres@localhost:5432/filecodebox?sslmode=disable'
go run ./cmd/server
# 双方言单测:sqlite 始终执行;postgres 需真实实例(FCB_TEST_PG_DSN 指向测试库)
FCB_TEST_PG_DSN='postgres://postgres:postgres@localhost:5432/fcb_test?sslmode=disable' go test ./internal/database/ ./internal/settings/
go test ./... # 全量单测(未设置 FCB_TEST_PG_DSN 时自动跳过 PG 用例)
```
系统未初始化时除 `/setup``/api/v1/health` 外一律返回 428,初始化路由由 API 层任务接入。
## 开发注意
- **GOCACHE/GOMODCACHE 必须用 `export` 设置**(沙箱环境):本地与 CI 沙箱通常禁止写默认 Go 缓存
目录(`~/Library/Caches/go-build`)。注意一个隐蔽的坑——用**内联前缀变量**的方式
`GOCACHE=... GOMODCACHE=... go build ./... && go vet ./... && go test ./...`)时,
只有第一条命令继承这些变量,`&&` 后续命令(vet/test)会丢失前缀、落回默认缓存路径,
报错 `operation not permitted`(指向 `~/Library/Caches/...`)却极易误判为代码问题。
正确写法:
```bash
cd server
export GOCACHE=$(pwd)/../.gocache GOMODCACHE=$(pwd)/../.gomodcache GOSUMDB=off
go build ./... && go vet ./... && go test ./... # 后续命令也能继承 export 的变量
```