Go + Vue 管理台:Agent 隧道、域名反代、IP 安全、访客验证与监控大盘。 Gitea Actions CI 多架构镜像;纯 Go SQLite、无 CGO Docker 构建。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wormhole/wormhole/internal/config"
|
||||
)
|
||||
|
||||
type LoginBlockedError struct {
|
||||
RetryAfter time.Duration
|
||||
}
|
||||
|
||||
func (e *LoginBlockedError) Error() string {
|
||||
return "too many login attempts"
|
||||
}
|
||||
|
||||
type loginRecord struct {
|
||||
failures int
|
||||
windowStart time.Time
|
||||
lockedUntil time.Time
|
||||
}
|
||||
|
||||
type LoginLimiter struct {
|
||||
mu sync.Mutex
|
||||
maxAttempts int
|
||||
window time.Duration
|
||||
lockout time.Duration
|
||||
records map[string]*loginRecord
|
||||
}
|
||||
|
||||
func NewLoginLimiter(cfg *config.AuthConfig) *LoginLimiter {
|
||||
if cfg.LoginMaxAttempts <= 0 {
|
||||
return nil
|
||||
}
|
||||
max := cfg.LoginMaxAttempts
|
||||
window := cfg.LoginWindow
|
||||
lockout := cfg.LoginLockout
|
||||
if window <= 0 {
|
||||
window = 5 * time.Minute
|
||||
}
|
||||
if lockout <= 0 {
|
||||
lockout = 15 * time.Minute
|
||||
}
|
||||
return &LoginLimiter{
|
||||
maxAttempts: max,
|
||||
window: window,
|
||||
lockout: lockout,
|
||||
records: make(map[string]*loginRecord),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LoginLimiter) keys(ip, username string) []string {
|
||||
keys := []string{"ip:" + ip}
|
||||
if username != "" {
|
||||
keys = append(keys, "user:"+username)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (l *LoginLimiter) IsBlocked(ip, username string) (time.Duration, bool) {
|
||||
if l == nil {
|
||||
return 0, false
|
||||
}
|
||||
now := time.Now()
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
var maxRetry time.Duration
|
||||
for _, key := range l.keys(ip, username) {
|
||||
rec := l.records[key]
|
||||
if rec == nil {
|
||||
continue
|
||||
}
|
||||
if now.Before(rec.lockedUntil) {
|
||||
retry := time.Until(rec.lockedUntil)
|
||||
if retry > maxRetry {
|
||||
maxRetry = retry
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxRetry, maxRetry > 0
|
||||
}
|
||||
|
||||
func (l *LoginLimiter) RecordFailure(ip, username string) {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
for _, key := range l.keys(ip, username) {
|
||||
rec := l.records[key]
|
||||
if rec == nil {
|
||||
rec = &loginRecord{windowStart: now}
|
||||
l.records[key] = rec
|
||||
}
|
||||
if now.Before(rec.lockedUntil) {
|
||||
continue
|
||||
}
|
||||
if now.Sub(rec.windowStart) > l.window {
|
||||
rec.failures = 0
|
||||
rec.windowStart = now
|
||||
}
|
||||
rec.failures++
|
||||
if rec.failures >= l.maxAttempts {
|
||||
rec.lockedUntil = now.Add(l.lockout)
|
||||
rec.failures = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LoginLimiter) Reset(ip, username string) {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
for _, key := range l.keys(ip, username) {
|
||||
delete(l.records, key)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user