Files
Prism/internal/auth/loginlimiter.go
T
renjue 9dfa7aa7c5
CI / docker (push) Successful in 4m28s
feat: Prism HTTP/SOCKS5 代理网关及管理台
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 15:37:02 +08:00

138 lines
3.3 KiB
Go

package auth
import (
"context"
"fmt"
"strconv"
"time"
"github.com/prism/proxy/internal/store"
)
const (
defaultMaxAttempts = 5
defaultLockMinutes = 15
)
type LoginConfig struct {
MaxAttempts int
Window time.Duration
LockDuration time.Duration
}
type LoginResult struct {
Allowed bool
Locked bool
Remaining int
RetryAfter time.Duration
}
type LoginLimiter struct {
store *store.Store
}
func NewLoginLimiter(st *store.Store) *LoginLimiter {
return &LoginLimiter{store: st}
}
func ParseLoginConfig(settings map[string]string) LoginConfig {
maxAttempts := parsePositiveInt(settings["login_max_attempts"], defaultMaxAttempts)
lockMin := parsePositiveInt(settings["login_lock_minutes"], defaultLockMinutes)
lock := time.Duration(lockMin) * time.Minute
return LoginConfig{
MaxAttempts: maxAttempts,
Window: lock,
LockDuration: lock,
}
}
func (l *LoginLimiter) Check(ctx context.Context, ip string, cfg LoginConfig) (LoginResult, error) {
attempt, err := l.store.GetLoginAttempt(ctx, ip)
if err != nil {
return LoginResult{}, err
}
if attempt == nil {
return LoginResult{Allowed: true, Remaining: cfg.MaxAttempts}, nil
}
now := time.Now()
if attempt.LockedUntil != nil && now.Before(*attempt.LockedUntil) {
return LoginResult{
Allowed: false,
Locked: true,
RetryAfter: attempt.LockedUntil.Sub(now),
}, nil
}
remaining := cfg.MaxAttempts - attempt.FailCount
if remaining < 0 {
remaining = 0
}
if attempt.FailCount > 0 && now.Sub(attempt.WindowStart) > cfg.Window {
remaining = cfg.MaxAttempts
}
return LoginResult{Allowed: true, Remaining: remaining}, nil
}
func (l *LoginLimiter) RecordFailure(ctx context.Context, ip string, cfg LoginConfig) (LoginResult, error) {
now := time.Now()
attempt, err := l.store.GetLoginAttempt(ctx, ip)
if err != nil {
return LoginResult{}, err
}
if attempt != nil && attempt.LockedUntil != nil && now.Before(*attempt.LockedUntil) {
return LoginResult{
Allowed: false,
Locked: true,
RetryAfter: attempt.LockedUntil.Sub(now),
}, nil
}
failCount := 1
windowStart := now
if attempt != nil {
if attempt.LockedUntil != nil || now.Sub(attempt.WindowStart) <= cfg.Window {
if now.Sub(attempt.WindowStart) <= cfg.Window {
failCount = attempt.FailCount + 1
windowStart = attempt.WindowStart
}
}
}
var lockedUntil *time.Time
locked := false
if failCount >= cfg.MaxAttempts {
t := now.Add(cfg.LockDuration)
lockedUntil = &t
locked = true
_ = l.store.AddAuditLog(ctx, "login_lock", "auth", fmt.Sprintf("ip=%s locked %v", ip, cfg.LockDuration))
}
if err := l.store.SetLoginAttempt(ctx, ip, store.LoginAttempt{
FailCount: failCount,
WindowStart: windowStart,
LockedUntil: lockedUntil,
}); err != nil {
return LoginResult{}, err
}
remaining := cfg.MaxAttempts - failCount
if remaining < 0 {
remaining = 0
}
res := LoginResult{Allowed: !locked, Remaining: remaining, Locked: locked}
if locked && lockedUntil != nil {
res.RetryAfter = lockedUntil.Sub(now)
}
return res, nil
}
func (l *LoginLimiter) RecordSuccess(ctx context.Context, ip string) error {
return l.store.ClearLoginAttempt(ctx, ip)
}
func parsePositiveInt(raw string, fallback int) int {
n, err := strconv.Atoi(raw)
if err != nil || n <= 0 {
return fallback
}
return n
}