You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -32,17 +32,38 @@ const sessionTTL = 7 * 24 * time.Hour
|
||||
// service 是 Service 接口的默认实现。字段保持小写以禁止外部直接构造,
|
||||
// 调用方必须通过 NewService 注入 Repository。
|
||||
type service struct {
|
||||
repo Repository
|
||||
audit audit.Recorder
|
||||
now func() time.Time
|
||||
repo Repository
|
||||
audit audit.Recorder
|
||||
now func() time.Time
|
||||
throttle *LoginThrottle // 登录暴力破解节流;默认 disabled,装配期可回填
|
||||
}
|
||||
|
||||
// NewService 用给定的 Repository 和 audit.Recorder 构造 Service 实现。
|
||||
// recorder 可为 nil(审计是 best-effort,nil 时直接跳过写审计)。
|
||||
// 返回 Service 接口而非 *service,以便日后扩展实现(如增加缓存层装饰器)
|
||||
// 时不破坏调用方代码。
|
||||
// 时不破坏调用方代码。默认装一个 disabled 的 LoginThrottle(无节流),
|
||||
// 装配期通过 SetLoginThrottle 注入启用的实例。
|
||||
func NewService(repo Repository, recorder audit.Recorder) Service {
|
||||
return &service{repo: repo, audit: recorder, now: time.Now}
|
||||
return &service{
|
||||
repo: repo,
|
||||
audit: recorder,
|
||||
now: time.Now,
|
||||
throttle: NewLoginThrottle(LoginThrottleConfig{}, nil),
|
||||
}
|
||||
}
|
||||
|
||||
// SetLoginThrottle 注入登录节流器(装配期回填)。传 nil 时退回 disabled。
|
||||
// 因 Service 是接口,调用方需类型断言到该 setter(与项目其它装配期回填一致)。
|
||||
func (s *service) SetLoginThrottle(t *LoginThrottle) {
|
||||
if t == nil {
|
||||
t = NewLoginThrottle(LoginThrottleConfig{}, nil)
|
||||
}
|
||||
s.throttle = t
|
||||
}
|
||||
|
||||
// LoginThrottleSetter 是装配期回填登录节流器的窄接口(app.go 用类型断言取得)。
|
||||
type LoginThrottleSetter interface {
|
||||
SetLoginThrottle(t *LoginThrottle)
|
||||
}
|
||||
|
||||
// Login 校验邮箱与密码,成功后签发新的会话 token。
|
||||
@@ -50,9 +71,17 @@ func NewService(repo Repository, recorder audit.Recorder) Service {
|
||||
// 不向调用方泄露 "邮箱是否存在" 这一侧信道;只有底层故障(哈希解析、
|
||||
// token 生成、写库失败等)会返回带详细 cause 的非授权类错误。
|
||||
func (s *service) Login(ctx context.Context, email, password, ip string) (string, *User, error) {
|
||||
// 暴力破解节流:在任何 DB 查询 / 密码哈希之前判定,超限直接 429,避免攻击者
|
||||
// 持续消耗 bcrypt CPU。key = ip + lowercase(email),未知邮箱与已知邮箱走同一
|
||||
// 计数路径,不引入邮箱枚举侧信道。
|
||||
if allowed, _ := s.throttle.Allow(ip, email); !allowed {
|
||||
return "", nil, errs.New(errs.CodeRateLimited, "登录尝试过于频繁,请稍后再试")
|
||||
}
|
||||
|
||||
u, err := s.repo.GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
// 不区分 "邮箱不存在" 与 "密码错误",统一报 unauthorized。
|
||||
// 不区分 "邮箱不存在" 与 "密码错误",统一报 unauthorized。失败计数。
|
||||
s.throttle.RecordFailure(ip, email)
|
||||
return "", nil, errs.New(errs.CodeUnauthorized, "邮箱或密码错误")
|
||||
}
|
||||
ok, err := VerifyPassword(password, u.PasswordHash)
|
||||
@@ -60,8 +89,11 @@ func (s *service) Login(ctx context.Context, email, password, ip string) (string
|
||||
return "", nil, errs.Wrap(err, errs.CodeInternal, "verify password")
|
||||
}
|
||||
if !ok {
|
||||
s.throttle.RecordFailure(ip, email)
|
||||
return "", nil, errs.New(errs.CodeUnauthorized, "邮箱或密码错误")
|
||||
}
|
||||
// 凭据正确:清零该 key 的失败计数。
|
||||
s.throttle.Reset(ip, email)
|
||||
|
||||
tok, hash, err := NewSessionToken()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user