mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-02 06:56:36 +08:00
公开密码登录口按 IP 限制 10 分钟内最多 20 次失败,堵住未授权爆破。metric 持平 8。
Result: {"status":"keep","total_issues":8,"golint_canonicalheader":0,"golint_errname":0,"golint_errorlint":1,"golint_forcetypeassert":0,"golint_gosec":0,"golint_intrange":0,"golint_modernize":3,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":0,"golint_recvcheck":1,"golint_usestdlibvars":0,"golint_wastedassign":0,"golint_total":8,"eslint_problems":0,"eslint_errors":0,"eslint_warnings":0,"tsc_errors":0,"measure_s":85,"golint_test_testifylint":0,"golint_test_thelper":0,"golint_test_usetesting":0,"golint_test_total":0,"golint_exhaustive":0,"golint_vetx_total":0,"vitest_failed":0,"vitest_total":126}
This commit is contained in:
@@ -27,6 +27,7 @@ const (
|
||||
errUnsupportedEmailScene = "不支持的验证场景"
|
||||
errEmailAlreadyRegistered = "该邮箱已被注册"
|
||||
errEmailCodeCooldown = "验证码发送频繁,请稍后再试"
|
||||
errLoginRateLimited = "登录尝试过于频繁,请稍后再试"
|
||||
errEmailFormatInvalid = "邮箱格式不正确"
|
||||
errEmailAlreadyBound = "该邮箱已被其他账号绑定"
|
||||
errRenderEmailTemplateFailed = "渲染验证邮件模板失败:%w"
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||
db "github.com/Rain-kl/Wavelet/internal/infra/persistence"
|
||||
@@ -51,6 +52,47 @@ type updateProfileInput struct {
|
||||
Location string
|
||||
}
|
||||
|
||||
const (
|
||||
loginFailLimitKeyFormat = "login:fail:%s"
|
||||
loginFailLimitMax = 20
|
||||
loginFailLimitWindow = 10 * time.Minute
|
||||
)
|
||||
|
||||
func loginFailLimitKey(ip string) string {
|
||||
return fmt.Sprintf(loginFailLimitKeyFormat, strings.TrimSpace(ip))
|
||||
}
|
||||
|
||||
func loginAttemptsBlocked(ctx context.Context, ip string) bool {
|
||||
if db.Redis == nil {
|
||||
return false
|
||||
}
|
||||
n, err := db.Redis.Get(ctx, db.PrefixedKey(loginFailLimitKey(ip))).Int()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return n >= loginFailLimitMax
|
||||
}
|
||||
|
||||
func recordFailedLogin(ctx context.Context, ip string) {
|
||||
if db.Redis == nil {
|
||||
return
|
||||
}
|
||||
key := db.PrefixedKey(loginFailLimitKey(ip))
|
||||
n, err := db.Redis.Incr(ctx, key).Result()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n == 1 {
|
||||
_ = db.Redis.Expire(ctx, key, loginFailLimitWindow).Err()
|
||||
}
|
||||
}
|
||||
|
||||
func clearFailedLogins(ctx context.Context, ip string) {
|
||||
if db.Redis == nil {
|
||||
return
|
||||
}
|
||||
_ = db.Redis.Del(ctx, db.PrefixedKey(loginFailLimitKey(ip))).Err()
|
||||
}
|
||||
func isPasswordLoginEnabled(ctx context.Context) bool {
|
||||
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyPasswordLoginEnabled)
|
||||
if err != nil {
|
||||
|
||||
@@ -68,6 +68,10 @@ func Login(c *gin.Context) {
|
||||
response.AbortBadRequest(c, errPasswordLoginDisabled)
|
||||
return
|
||||
}
|
||||
if loginAttemptsBlocked(ctx, c.ClientIP()) {
|
||||
response.AbortBadRequest(c, errLoginRateLimited)
|
||||
return
|
||||
}
|
||||
var req loginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortBadRequest(c, err.Error())
|
||||
@@ -82,11 +86,13 @@ func Login(c *gin.Context) {
|
||||
user, err := getUserByUsernameOrEmail(ctx, req.Username)
|
||||
if err != nil {
|
||||
pkgu.DummyCheckPassword(req.Password)
|
||||
recordFailedLogin(ctx, c.ClientIP())
|
||||
logger.WarnF(ctx, "[LoginAudit] failed login attempt (username not found) for input: %s, IP: %s", req.Username, c.ClientIP())
|
||||
response.AbortBadRequest(c, errUsernameOrPasswordWrong)
|
||||
return
|
||||
}
|
||||
if !user.IsActive {
|
||||
recordFailedLogin(ctx, c.ClientIP())
|
||||
logger.WarnF(ctx, "[LoginAudit] banned user login attempt for username: %s, ID: %d, IP: %s", user.Username, user.ID, c.ClientIP())
|
||||
response.AbortBadRequest(c, errUsernameOrPasswordWrong)
|
||||
return
|
||||
@@ -96,6 +102,7 @@ func Login(c *gin.Context) {
|
||||
isPlaintext := !user.IsPasswordEncrypted()
|
||||
|
||||
if !user.CheckPassword(req.Password) {
|
||||
recordFailedLogin(ctx, c.ClientIP())
|
||||
logger.WarnF(ctx, "[LoginAudit] failed login attempt (incorrect password) for username: %s, ID: %d, IP: %s", user.Username, user.ID, c.ClientIP())
|
||||
response.AbortBadRequest(c, errUsernameOrPasswordWrong)
|
||||
return
|
||||
@@ -124,6 +131,7 @@ func Login(c *gin.Context) {
|
||||
if isPlaintext {
|
||||
extras["need_change_password"] = true
|
||||
}
|
||||
clearFailedLogins(ctx, c.ClientIP())
|
||||
if err := oauth.SetLoginSession(ctx, c, user, extras); err != nil {
|
||||
response.AbortBadRequest(c, errSaveSessionFailed)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user