- 更新 changelog.md: v0.2.0 记录 M2-M8 全部完成,路线图更新 - 新增 5 篇指南: RBAC、组织管理、审计日志、通知模块、质量横切 - 新增 5 篇 API 文档: RBAC(11)、Org(5)、Audit(1)、Settings(7)、Notification(7) - 更新 api/overview.md: 完整 37 端点列表 - 更新 config.ts: 侧边栏分组 + 11 页 SEO 元数据
146 lines
3.6 KiB
Markdown
146 lines
3.6 KiB
Markdown
---
|
||
title: 质量横切 | 安全基线、限流、日志与优雅停机 - MengStack官方文档
|
||
---
|
||
|
||
# 质量横切
|
||
|
||
M8 为框架添加了生产级质量基线,覆盖安全、限流、日志、容错和测试基础设施。
|
||
|
||
## 中间件链
|
||
|
||
所有请求经过以下中间件链(按顺序):
|
||
|
||
```
|
||
RequestID → RequestLogger → SecurityHeaders → CORS → RateLimit → BodyLimit → Recovery
|
||
```
|
||
|
||
## IP 限流
|
||
|
||
基于内存的 IP 限流中间件,自动清理过期记录:
|
||
|
||
```go
|
||
// 每个 IP 每分钟最多 100 次请求
|
||
r.Use(middleware.RateLimit(100, time.Minute))
|
||
```
|
||
|
||
| 特性 | 说明 |
|
||
|------|------|
|
||
| 限流维度 | 客户端 IP |
|
||
| 存储方式 | 内存 map + sync.Mutex |
|
||
| 自动清理 | 后台 goroutine 每分钟清理过期记录 |
|
||
| 超限响应 | HTTP 429 + `RATE_LIMITED` 错误码 |
|
||
|
||
::: warning
|
||
内存限流适合单机部署。多实例部署建议替换为 Redis 限流。
|
||
:::
|
||
|
||
## 安全响应头
|
||
|
||
自动为所有响应添加安全头:
|
||
|
||
| Header | 值 | 作用 |
|
||
|--------|------|------|
|
||
| `X-Content-Type-Options` | `nosniff` | 防止 MIME 类型嗅探 |
|
||
| `X-Frame-Options` | `DENY` | 防止点击劫持 |
|
||
| `X-XSS-Protection` | `1; mode=block` | 浏览器 XSS 过滤 |
|
||
| `Referrer-Policy` | `strict-origin-when-cross-origin` | 控制 Referer 泄露 |
|
||
| `Content-Security-Policy` | `default-src 'self'` | 限制资源加载来源 |
|
||
|
||
## 请求体限制
|
||
|
||
限制请求体最大为 10MB,防止大文件攻击:
|
||
|
||
```go
|
||
r.Use(middleware.BodyLimit(10 << 20)) // 10 MB
|
||
```
|
||
|
||
- 先检查 `Content-Length` header,超限直接返回 413
|
||
- 再通过 `http.MaxBytesReader` 限制实际读取大小
|
||
|
||
## 结构化请求日志
|
||
|
||
使用 Zap 替代 fmt,输出结构化 JSON 日志:
|
||
|
||
```json
|
||
{
|
||
"level": "info",
|
||
"msg": "request",
|
||
"status": 200,
|
||
"method": "GET",
|
||
"path": "/api/v1/profile",
|
||
"query": "",
|
||
"ip": "192.168.1.100",
|
||
"user-agent": "Mozilla/5.0",
|
||
"latency_ms": 12,
|
||
"trace_id": "req-abc-123"
|
||
}
|
||
```
|
||
|
||
日志级别按状态码自动选择:
|
||
|
||
| 状态码范围 | 日志级别 |
|
||
|-----------|---------|
|
||
| < 400 | INFO |
|
||
| 400-499 | WARN |
|
||
| >= 500 | ERROR |
|
||
|
||
## 自定义 Recovery
|
||
|
||
替代 `gin.Recovery()`,panic 信息通过 Zap 记录而非 stdout:
|
||
|
||
```go
|
||
r.Use(middleware.Recovery(log))
|
||
```
|
||
|
||
记录的字段:panic 内容、stack trace、请求方法、路径、trace_id。
|
||
|
||
## 优雅停机
|
||
|
||
应用收到终止信号后,等待最多 10 秒处理完进行中的请求:
|
||
|
||
```go
|
||
// app.go
|
||
OnStop: func(ctx context.Context) error {
|
||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
return srv.Shutdown(shutdownCtx)
|
||
}
|
||
```
|
||
|
||
## 测试基础设施
|
||
|
||
### testutil 包
|
||
|
||
```go
|
||
import "mengstack/internal/kernel/testutil"
|
||
|
||
func TestSomething(t *testing.T) {
|
||
log := testutil.TestLogger(t) // 测试专用 Zap logger
|
||
cfg := testutil.TestConfig() // 最小化测试配置
|
||
}
|
||
```
|
||
|
||
### 中间件测试
|
||
|
||
内置 11 个单元测试覆盖所有中间件:
|
||
|
||
```bash
|
||
go test ./internal/app/middleware/ -v
|
||
```
|
||
|
||
| 测试 | 覆盖场景 |
|
||
|------|----------|
|
||
| TestRequestID_GeneratesNew | 无 trace_id 时自动生成 |
|
||
| TestRequestID_PreservesExisting | 已有 trace_id 时保留 |
|
||
| TestSecurityHeaders | 5 个安全头全部设置 |
|
||
| TestRateLimit_AllowsWithinLimit | 限额内请求正常通过 |
|
||
| TestRateLimit_BlocksOverLimit | 超限额返回 429 |
|
||
| TestRequestLogger_DoesNotPanic | 日志中间件不 panic |
|
||
| TestRecovery_HandlesPanic | 捕获 panic 并记录 |
|
||
| TestBodyLimit_RejectsLargeBody | 超大 body 返回 413 |
|
||
| TestBodyLimit_AllowsSmallBody | 正常 body 通过 |
|
||
|
||
## 数据库时区
|
||
|
||
PostgreSQL DSN 默认携带 `TimeZone=Asia/Shanghai`,确保时间字段按中国时区存储和返回。
|