发布 0.1.0:通知渠道、告警规则、令牌版本与安全加固
This commit is contained in:
@@ -1,10 +1,18 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// maxGuardEntries 是 failures / lockedAt 各自的条目上限(S-03):
|
||||
// 攻击者持续提交唯一用户名时先清过期、仍满驱逐最旧,内存有界。
|
||||
const maxGuardEntries = 4096
|
||||
|
||||
// guardUserMax 是计入守卫 key 的用户名字节上限,超长截断防高基数撑爆。
|
||||
const guardUserMax = 32
|
||||
|
||||
// loginGuard 按「IP|用户名」双维度做滑动窗口失败计数与锁定;
|
||||
// 窗口与锁定时长同值、阈值由调用方按安全设置传入(探索文档主题三);
|
||||
// 内存实现(单体面板),重启清零可接受。
|
||||
@@ -21,6 +29,16 @@ func newLoginGuard() *loginGuard {
|
||||
}
|
||||
}
|
||||
|
||||
// guardKey 规范化守卫键:用户名去首尾空白并按字节截断,
|
||||
// 高基数/超长输入不会产生无界的独立条目。
|
||||
func guardKey(clientIP, username string) string {
|
||||
name := strings.TrimSpace(username)
|
||||
if len(name) > guardUserMax {
|
||||
name = name[:guardUserMax]
|
||||
}
|
||||
return clientIP + "|" + name
|
||||
}
|
||||
|
||||
// locked 判定 key 是否处于锁定期;过期锁惰性清除。
|
||||
func (g *loginGuard) locked(key string, now time.Time, lockFor time.Duration) bool {
|
||||
g.mu.Lock()
|
||||
@@ -37,9 +55,13 @@ func (g *loginGuard) locked(key string, now time.Time, lockFor time.Duration) bo
|
||||
}
|
||||
|
||||
// fail 记一次失败并裁剪窗口;达到阈值转入锁定并返回 true(仅触发那一次)。
|
||||
// 新建条目前先保证容量(过期清理 + 最旧驱逐),防高基数 key 单向增长。
|
||||
func (g *loginGuard) fail(key string, now time.Time, limit int, window time.Duration) bool {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
if _, exists := g.failures[key]; !exists {
|
||||
g.ensureCapacity(now, window)
|
||||
}
|
||||
kept := g.failures[key][:0]
|
||||
for _, t := range g.failures[key] {
|
||||
if now.Sub(t) < window {
|
||||
@@ -49,6 +71,9 @@ func (g *loginGuard) fail(key string, now time.Time, limit int, window time.Dura
|
||||
kept = append(kept, now)
|
||||
if len(kept) >= limit {
|
||||
delete(g.failures, key)
|
||||
// 转入锁定同样要保证容量:该写入发生在已有 failures 条目的
|
||||
// 第 N 次失败,不经过新建条目路径的 ensureCapacity
|
||||
g.ensureLockCapacity(now, window)
|
||||
g.lockedAt[key] = now
|
||||
return true
|
||||
}
|
||||
@@ -56,6 +81,77 @@ func (g *loginGuard) fail(key string, now time.Time, limit int, window time.Dura
|
||||
return false
|
||||
}
|
||||
|
||||
// ensureLockCapacity 在写入 lockedAt 前保证容量:先清已过锁定期的锁,
|
||||
// 仍满驱逐锁定最早的条目(其剩余锁定时间最短,提前解锁的影响最小)。
|
||||
func (g *loginGuard) ensureLockCapacity(now time.Time, lockFor time.Duration) {
|
||||
if len(g.lockedAt) < maxGuardEntries {
|
||||
return
|
||||
}
|
||||
for k, at := range g.lockedAt {
|
||||
if now.Sub(at) >= lockFor {
|
||||
delete(g.lockedAt, k)
|
||||
}
|
||||
}
|
||||
if len(g.lockedAt) >= maxGuardEntries {
|
||||
g.evictOldestLock()
|
||||
}
|
||||
}
|
||||
|
||||
// evictOldestLock 驱逐锁定时间最早的 lockedAt 条目(调用方须持锁)。
|
||||
func (g *loginGuard) evictOldestLock() {
|
||||
var oldestKey string
|
||||
var oldestAt time.Time
|
||||
for k, at := range g.lockedAt {
|
||||
if oldestKey == "" || at.Before(oldestAt) {
|
||||
oldestKey, oldestAt = k, at
|
||||
}
|
||||
}
|
||||
if oldestKey != "" {
|
||||
delete(g.lockedAt, oldestKey)
|
||||
}
|
||||
}
|
||||
|
||||
// ensureCapacity 在新建 failures 条目前保证容量:先清出窗口外的失败记录
|
||||
// 与已过锁定期的锁,仍满则驱逐最后失败时间最早的条目(调用方须持锁)。
|
||||
func (g *loginGuard) ensureCapacity(now time.Time, window time.Duration) {
|
||||
if len(g.failures) < maxGuardEntries && len(g.lockedAt) < maxGuardEntries {
|
||||
return
|
||||
}
|
||||
for k, times := range g.failures {
|
||||
if len(times) == 0 || now.Sub(times[len(times)-1]) >= window {
|
||||
delete(g.failures, k)
|
||||
}
|
||||
}
|
||||
for k, at := range g.lockedAt {
|
||||
if now.Sub(at) >= window {
|
||||
delete(g.lockedAt, k)
|
||||
}
|
||||
}
|
||||
if len(g.failures) >= maxGuardEntries {
|
||||
g.evictOldest()
|
||||
}
|
||||
}
|
||||
|
||||
// evictOldest 驱逐最后失败时间最早的 failures 条目(调用方须持锁);
|
||||
// 仅在容量兜底时触发,O(n) 扫描可接受。
|
||||
func (g *loginGuard) evictOldest() {
|
||||
var oldestKey string
|
||||
var oldestAt time.Time
|
||||
for k, times := range g.failures {
|
||||
if len(times) == 0 {
|
||||
delete(g.failures, k)
|
||||
continue
|
||||
}
|
||||
last := times[len(times)-1]
|
||||
if oldestKey == "" || last.Before(oldestAt) {
|
||||
oldestKey, oldestAt = k, last
|
||||
}
|
||||
}
|
||||
if oldestKey != "" {
|
||||
delete(g.failures, oldestKey)
|
||||
}
|
||||
}
|
||||
|
||||
// success 清空 key 的失败计数(成功登录重置窗口)。
|
||||
func (g *loginGuard) success(key string) {
|
||||
g.mu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user