fix(auth): 登录限流按解析后的用户身份共享计数

合并 #680:登录限流按解析后的用户身份共享计数
This commit is contained in:
Catlaxy AI
2026-10-06 09:22:29 +08:00
committed by GitHub
parent 82b4b0c260
commit a71e03b131
5 changed files with 104 additions and 1 deletions

View File

@@ -124,7 +124,12 @@ func RegisterAuthRoutes(r *gin.RouterGroup, svc *service.Service) {
if !available || !enforceRateLimit(c, "login-ip:"+c.ClientIP(), policy.Request.LoginIPPerTenMinutes, 10*time.Minute) {
return
}
if !enforceRateLimit(c, "login:"+c.ClientIP()+":"+strings.ToLower(strings.TrimSpace(req.Username)), policy.Request.LoginAccountPerTenMinutes, 10*time.Minute) {
subject, err := svc.LoginRateLimitSubject(req.Username)
if err != nil {
failService(c, err)
return
}
if !enforceRateLimit(c, "login:"+c.ClientIP()+":"+subject, policy.Request.LoginAccountPerTenMinutes, 10*time.Minute) {
return
}
result, err := svc.Login(req)

View File

@@ -0,0 +1,77 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/platform"
"infinite-canvas/backend/internal/repository"
"infinite-canvas/backend/internal/service"
)
func TestLoginIdentityAliasSharesThrottle(t *testing.T) {
testLoginIdentityAttempts(t, []string{"member", "member", "member@example.com"})
}
func TestLoginIdentityUnknownAndKnownResponses(t *testing.T) {
known := testLoginIdentityAttempts(t, []string{"member@example.com", " MEMBER ", "MEMBER@EXAMPLE.COM"})
unknown := testLoginIdentityAttempts(t, []string{"missing", " MISSING ", "missing"})
for i := range known {
if known[i] != unknown[i] {
t.Fatalf("attempt %d reveals account existence: known=%s unknown=%s", i+1, known[i], unknown[i])
}
}
}
func testLoginIdentityAttempts(t *testing.T, accounts []string) []string {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.User{}, &model.SystemSetting{}); err != nil {
t.Fatal(err)
}
sqlDB, _ := db.DB()
t.Cleanup(func() { _ = sqlDB.Close() })
if err := db.Create(&model.User{ID: "member", Username: "member", Email: "member@example.com", Status: model.UserStatusActive, PasswordHash: "invalid-hash"}).Error; err != nil {
t.Fatal(err)
}
policy := platform.DefaultRuntimePolicy()
policy.Request.LoginAccountPerTenMinutes = 2
data, _ := json.Marshal(policy)
if err := db.Create(&model.SystemSetting{Key: "runtime_policy", ValueJSON: string(data)}).Error; err != nil {
t.Fatal(err)
}
svc := service.New(repository.New(db), t.TempDir())
previous := runtimeService
ConfigureRuntime(svc)
t.Cleanup(func() { runtimeService = previous })
gin.SetMode(gin.TestMode)
router := gin.New()
RegisterAuthRoutes(router.Group("/api"), svc)
var bodies []string
for i, account := range accounts {
req := httptest.NewRequest(http.MethodPost, "/api/auth/login", strings.NewReader(fmt.Sprintf(`{"username":%q,"password":"wrong"}`, account)))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
want := http.StatusUnauthorized
if i == 2 {
want = http.StatusTooManyRequests
}
if w.Code != want {
t.Fatalf("attempt %d account=%s status=%d want=%d body=%s", i+1, account, w.Code, want, w.Body.String())
}
bodies = append(bodies, w.Body.String())
}
return bodies
}