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

+10 -2
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/openteam/server/internal/pkg/resp"
@@ -42,7 +43,11 @@ func (h *Handler) PasskeyRegisterComplete(c *gin.Context) {
// PasskeyLoginBegin POST /api/v1/webauthn/login/begin — 生成断言选项。
// 传 username 用指定用户;不传则用可发现凭据(平台 passkey)。
// 无需认证且每次调用都会生成并暂存一个 challenge,必须与登录同级限速。
func (h *Handler) PasskeyLoginBegin(c *gin.Context) {
if h.authLimited(c, "webauthn:ip:"+c.ClientIP(), h.a.Cfg.RateLimit.LoginIPPerMin, time.Minute) {
return
}
var req struct {
Username string `json:"username"`
}
@@ -72,6 +77,9 @@ func (h *Handler) PasskeyLoginBegin(c *gin.Context) {
// PasskeyLoginComplete POST /api/v1/webauthn/login/complete — 校验断言并发放令牌。
func (h *Handler) PasskeyLoginComplete(c *gin.Context) {
if h.authLimited(c, "webauthn:ip:"+c.ClientIP(), h.a.Cfg.RateLimit.LoginIPPerMin, time.Minute) {
return
}
var req struct {
Challenge string `json:"challenge"`
Credential json.RawMessage `json:"credential"`
@@ -105,12 +113,12 @@ func (h *Handler) PasskeyLoginComplete(c *gin.Context) {
resp.Fail(c, http.StatusForbidden, "user account disabled")
return
}
access, _, err := h.a.JWT.Sign(u.ID, u.Username, u.Role, "access")
access, _, err := h.a.JWT.Sign(u.ID, u.Username, u.Role, "access", u.TokenVersion)
if err != nil {
resp.Fail(c, http.StatusInternalServerError, "failed to issue token")
return
}
refresh, _, err := h.a.JWT.Sign(u.ID, u.Username, u.Role, "refresh")
refresh, _, err := h.a.JWT.Sign(u.ID, u.Username, u.Role, "refresh", u.TokenVersion)
if err != nil {
resp.Fail(c, http.StatusInternalServerError, "failed to issue token")
return