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

+5 -2
View File
@@ -9,10 +9,12 @@ import (
)
// Claims 用户声明;Subject 字段区分 "access" / "refresh"。
// Ver 是用户级会话版本号:改密/登出时递增,旧 token 立即失效(可撤销性)。
type Claims struct {
UserID uint64 `json:"uid"`
Username string `json:"uname"`
Role string `json:"role"`
Ver int `json:"ver"`
jwt.RegisteredClaims
}
@@ -30,8 +32,8 @@ func NewManager(secret, issuer string, accessTTL, refreshTTL time.Duration) *Man
func (m *Manager) AccessTTL() time.Duration { return m.accessTTL }
func (m *Manager) RefreshTTL() time.Duration { return m.refreshTTL }
// Sign 签发 token;typ 取 "access" / "refresh"。
func (m *Manager) Sign(userID uint64, username, role, typ string) (string, time.Time, error) {
// Sign 签发 token;typ 取 "access" / "refresh",ver 为签发时的会话版本。
func (m *Manager) Sign(userID uint64, username, role, typ string, ver int) (string, time.Time, error) {
ttl := m.accessTTL
if typ == "refresh" {
ttl = m.refreshTTL
@@ -42,6 +44,7 @@ func (m *Manager) Sign(userID uint64, username, role, typ string) (string, time.
UserID: userID,
Username: username,
Role: role,
Ver: ver,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: m.issuer,
Subject: typ,
+21 -2
View File
@@ -7,7 +7,7 @@ import (
func TestSignParseAccess(t *testing.T) {
m := NewManager("test-secret", "openteam", time.Hour, 24*time.Hour)
tok, exp, err := m.Sign(42, "alice", "admin", "access")
tok, exp, err := m.Sign(42, "alice", "admin", "access", 7)
if err != nil {
t.Fatalf("Sign: %v", err)
}
@@ -24,6 +24,25 @@ func TestSignParseAccess(t *testing.T) {
if claims.Subject != "access" {
t.Fatalf("subject mismatch: %s", claims.Subject)
}
if claims.Ver != 7 {
t.Fatalf("ver mismatch: %d", claims.Ver)
}
}
// 会话版本承载撤销语义:Ver 编码进 token,校验方与用户当前版本比对
func TestClaimsCarryVersion(t *testing.T) {
m := NewManager("test-secret", "openteam", time.Hour, 24*time.Hour)
tok, _, err := m.Sign(1, "bob", "user", "refresh", 3)
if err != nil {
t.Fatalf("Sign: %v", err)
}
claims, err := m.Parse(tok)
if err != nil {
t.Fatalf("Parse: %v", err)
}
if claims.Ver != 3 || claims.Subject != "refresh" {
t.Fatalf("claims mismatch: %+v", claims)
}
}
func TestParseRejectsBadToken(t *testing.T) {
@@ -35,7 +54,7 @@ func TestParseRejectsBadToken(t *testing.T) {
func TestExpiredToken(t *testing.T) {
m := NewManager("test-secret", "openteam", -time.Hour, -time.Hour)
tok, _, err := m.Sign(1, "bob", "user", "access")
tok, _, err := m.Sign(1, "bob", "user", "access", 0)
if err != nil {
t.Fatalf("Sign: %v", err)
}