package admin import ( "net" "net/http" "sync" "time" ) // loginLimiter 限制每个来源 IP 的登录失败次数(滑动窗口), // 防止默认/弱口令被在线暴力破解。成功登录后计数清零。 type loginLimiter struct { mu sync.Mutex hits map[string][]time.Time } const ( maxLoginFails = 10 loginWindow = 10 * time.Minute ) func newLoginLimiter() *loginLimiter { return &loginLimiter{hits: make(map[string][]time.Time)} } func (l *loginLimiter) blocked(key string) bool { l.mu.Lock() defer l.mu.Unlock() return len(l.recent(key, time.Now())) >= maxLoginFails } func (l *loginLimiter) fail(key string) { l.mu.Lock() defer l.mu.Unlock() now := time.Now() l.hits[key] = append(l.recent(key, now), now) } func (l *loginLimiter) reset(key string) { l.mu.Lock() defer l.mu.Unlock() delete(l.hits, key) } // recent 返回窗口内的失败时间;调用方必须持有 l.mu。 func (l *loginLimiter) recent(key string, now time.Time) []time.Time { hs := l.hits[key] cut := now.Add(-loginWindow) i := 0 for ; i < len(hs); i++ { if hs[i].After(cut) { break } } if i > 0 { hs = hs[i:] l.hits[key] = hs } if len(hs) == 0 { delete(l.hits, key) } return hs } func sourceKey(r *http.Request) string { // 只信连接层地址;X-Forwarded-For 可被伪造,不作为限速键。 if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { return host } return r.RemoteAddr } // isTLS 判断最终用户看到的是不是 HTTPS(含反代 X-Forwarded-Proto)。 func isTLS(r *http.Request) bool { if r.TLS != nil { return true } return r.Header.Get("X-Forwarded-Proto") == "https" }