security: 会话可撤销与认证面加固

- users 加 token_version(schema v4):改密/登出递增,该用户全部已发 access/refresh 立即作废;refresh 改轮换式换发新 cookie
- 邀请码改精确匹配 + 注册成功后消费(一码一用),修复子串匹配让任意后缀放行、可无限薅体验金
- 生产校验拦下 .env.example 占位密钥;OT_ENV 枚举校验(拼错不再静默跳过断言);生产强制 COOKIE_SECURE;非 development 拒绝默认密码播种管理员
- WebAuthn 会话启用过期校验 + 4096 容量上限(未认证 login/begin 可无限撑爆内存),begin/complete 挂登录级限速
- /api/v1 挂 1MB 请求体上限;login 先限 IP 桶再解析 body;全局安全响应头 + CSP
- 连接池配置(PG 30/SQLite 8);SQLite DSN 自动追加 WAL + busy_timeout(驱动只认 _pragma,旧注释的 _journal_mode 写法不生效)
This commit is contained in:
Sakurasan committed 2026-10-29 02:56:46 +08:00
1 parent d6db0f8162
commit e3ff0d3329
14 files changed
+392 -37

No files matched your search

@@ -39,6 +39,12 @@ func SessionAuth(a *app.App) gin.HandlerFunc {
c.Abort()
return
}
// 会话版本不匹配 = 改密/登出后签发的旧 token,立即失效
if claims.Ver != u.TokenVersion {
resp.Fail(c, http.StatusUnauthorized, "session revoked, please sign in again")
c.Abort()
return
}
c.Set(CtxSessionUser, &u)
c.Next()
}
@@ -100,6 +106,24 @@ func CORS(allow []string) gin.HandlerFunc {
}
}
// SecurityHeaders 基础安全响应头 + SPA 的 CSP。
// script-src 'self':Vite 构建产物无内联脚本;style 保留 'unsafe-inline'
// (Vue 运行时 style 绑定与 Tailwind 注入需要);access token 不再落 localStorage 后,
// XSS 即使发生也拿不到持久凭据,CSP 是纵深防御的一层。
func SecurityHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
h := c.Writer.Header()
h.Set("X-Content-Type-Options", "nosniff")
h.Set("X-Frame-Options", "DENY")
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
h.Set("Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; "+
"img-src 'self' data:; font-src 'self' data:; connect-src 'self'; "+
"frame-ancestors 'none'; base-uri 'self'; form-action 'self'")
c.Next()
}
}
// BodyLimit 限制请求体字节数。超限后 ReadAll 会拿到 *http.MaxBytesError,
// 由代理层按客户端协议返回 413;0 表示不限制。
func BodyLimit(maxBytes int64) gin.HandlerFunc {