修复:调整游客取号限流策略

问题:移动网络共享出口 IP 会造成游客取号被误限流。

实现:移除公开取号 IP 限制,改用项目总量与手机号 HMAC 限流,并支持 Retry-After 倒计时。
This commit is contained in:
2026-07-31 22:56:33 +08:00
parent 3f4a9bf398
commit c2a5281534
17 changed files with 357 additions and 73 deletions

View File

@@ -4,6 +4,7 @@ import (
"encoding/base64"
"strings"
"testing"
"time"
)
func TestLoadAcceptsExactly32ByteKeys(t *testing.T) {
@@ -74,3 +75,66 @@ func TestLoadRequiresTLSAndReleaseMigrationsInProduction(t *testing.T) {
t.Fatalf("expected production migration mode error, got %v", err)
}
}
func TestLoadConfiguresPublicTicketRateLimits(t *testing.T) {
key := base64.StdEncoding.EncodeToString([]byte(strings.Repeat("x", 32)))
values := map[string]string{
"DATABASE_URL": "postgres://localhost/test",
"DATA_ENCRYPTION_KEY_BASE64": key,
"PHONE_HMAC_KEY_BASE64": key,
"SESSION_COOKIE_SECURE": "false",
}
lookup := func(name string) (string, bool) { value, ok := values[name]; return value, ok }
cfg, err := load(lookup)
if err != nil {
t.Fatal(err)
}
if cfg.PublicTicketProjectLimit != 1000 || cfg.PublicTicketPhoneLimit != 5 || cfg.PublicTicketRateWindow != time.Minute {
t.Fatalf("unexpected public ticket limits: project=%d phone=%d window=%s",
cfg.PublicTicketProjectLimit, cfg.PublicTicketPhoneLimit, cfg.PublicTicketRateWindow)
}
values["PUBLIC_TICKET_PROJECT_LIMIT"] = "1200"
values["PUBLIC_TICKET_PHONE_LIMIT"] = "8"
values["PUBLIC_TICKET_RATE_WINDOW"] = "2m"
cfg, err = load(lookup)
if err != nil {
t.Fatal(err)
}
if cfg.PublicTicketProjectLimit != 1200 || cfg.PublicTicketPhoneLimit != 8 || cfg.PublicTicketRateWindow != 2*time.Minute {
t.Fatalf("configured public ticket limits not applied: project=%d phone=%d window=%s",
cfg.PublicTicketProjectLimit, cfg.PublicTicketPhoneLimit, cfg.PublicTicketRateWindow)
}
}
func TestLoadRejectsInvalidPublicTicketRateLimits(t *testing.T) {
key := base64.StdEncoding.EncodeToString([]byte(strings.Repeat("x", 32)))
base := map[string]string{
"DATABASE_URL": "postgres://localhost/test",
"DATA_ENCRYPTION_KEY_BASE64": key,
"PHONE_HMAC_KEY_BASE64": key,
"SESSION_COOKIE_SECURE": "false",
}
tests := []struct {
name string
key string
value string
}{
{name: "project limit", key: "PUBLIC_TICKET_PROJECT_LIMIT", value: "0"},
{name: "phone limit", key: "PUBLIC_TICKET_PHONE_LIMIT", value: "0"},
{name: "window", key: "PUBLIC_TICKET_RATE_WINDOW", value: "0s"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
values := make(map[string]string, len(base)+1)
for name, value := range base {
values[name] = value
}
values[test.key] = test.value
if _, err := load(func(name string) (string, bool) { value, ok := values[name]; return value, ok }); err == nil {
t.Fatalf("%s=%q should be rejected", test.key, test.value)
}
})
}
}