修复:调整游客取号限流策略
问题:移动网络共享出口 IP 会造成游客取号被误限流。 实现:移除公开取号 IP 限制,改用项目总量与手机号 HMAC 限流,并支持 Retry-After 倒计时。
This commit is contained in:
@@ -12,22 +12,25 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Environment string
|
||||
HTTPAddr string
|
||||
DatabaseURL string
|
||||
EncryptionKey []byte
|
||||
PhoneHMACKey []byte
|
||||
SessionCookieName string
|
||||
SessionSecure bool
|
||||
SessionTTL time.Duration
|
||||
MigrateOnStart bool
|
||||
ShutdownTimeout time.Duration
|
||||
DBMaxOpenConns int
|
||||
DBMaxIdleConns int
|
||||
DBConnMaxIdleTime time.Duration
|
||||
DBConnMaxLifetime time.Duration
|
||||
MaintenanceInterval time.Duration
|
||||
MaintenanceBatchSize int
|
||||
Environment string
|
||||
HTTPAddr string
|
||||
DatabaseURL string
|
||||
EncryptionKey []byte
|
||||
PhoneHMACKey []byte
|
||||
SessionCookieName string
|
||||
SessionSecure bool
|
||||
SessionTTL time.Duration
|
||||
MigrateOnStart bool
|
||||
ShutdownTimeout time.Duration
|
||||
DBMaxOpenConns int
|
||||
DBMaxIdleConns int
|
||||
DBConnMaxIdleTime time.Duration
|
||||
DBConnMaxLifetime time.Duration
|
||||
MaintenanceInterval time.Duration
|
||||
MaintenanceBatchSize int
|
||||
PublicTicketProjectLimit int
|
||||
PublicTicketPhoneLimit int
|
||||
PublicTicketRateWindow time.Duration
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
@@ -100,6 +103,18 @@ func load(lookup func(string) (string, bool)) (Config, error) {
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
publicTicketProjectLimit, err := parseIntSetting("PUBLIC_TICKET_PROJECT_LIMIT", get("PUBLIC_TICKET_PROJECT_LIMIT", "1000"), 1, 1000000)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
publicTicketPhoneLimit, err := parseIntSetting("PUBLIC_TICKET_PHONE_LIMIT", get("PUBLIC_TICKET_PHONE_LIMIT", "5"), 1, 10000)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
publicTicketRateWindow, err := parseDurationSetting("PUBLIC_TICKET_RATE_WINDOW", get("PUBLIC_TICKET_RATE_WINDOW", "1m"), time.Second, time.Hour)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if environment == "production" && !sessionSecure {
|
||||
return Config{}, errors.New("SESSION_COOKIE_SECURE must be true in production")
|
||||
}
|
||||
@@ -119,22 +134,25 @@ func load(lookup func(string) (string, bool)) (Config, error) {
|
||||
}
|
||||
|
||||
return Config{
|
||||
Environment: environment,
|
||||
HTTPAddr: get("HTTP_ADDR", ":8080"),
|
||||
DatabaseURL: databaseURL,
|
||||
EncryptionKey: encryptionKey,
|
||||
PhoneHMACKey: phoneHMACKey,
|
||||
SessionCookieName: cookieName,
|
||||
SessionSecure: sessionSecure,
|
||||
SessionTTL: sessionTTL,
|
||||
MigrateOnStart: migrate,
|
||||
ShutdownTimeout: shutdownTimeout,
|
||||
DBMaxOpenConns: maxOpen,
|
||||
DBMaxIdleConns: maxIdle,
|
||||
DBConnMaxIdleTime: connMaxIdleTime,
|
||||
DBConnMaxLifetime: connMaxLifetime,
|
||||
MaintenanceInterval: maintenanceInterval,
|
||||
MaintenanceBatchSize: maintenanceBatchSize,
|
||||
Environment: environment,
|
||||
HTTPAddr: get("HTTP_ADDR", ":8080"),
|
||||
DatabaseURL: databaseURL,
|
||||
EncryptionKey: encryptionKey,
|
||||
PhoneHMACKey: phoneHMACKey,
|
||||
SessionCookieName: cookieName,
|
||||
SessionSecure: sessionSecure,
|
||||
SessionTTL: sessionTTL,
|
||||
MigrateOnStart: migrate,
|
||||
ShutdownTimeout: shutdownTimeout,
|
||||
DBMaxOpenConns: maxOpen,
|
||||
DBMaxIdleConns: maxIdle,
|
||||
DBConnMaxIdleTime: connMaxIdleTime,
|
||||
DBConnMaxLifetime: connMaxLifetime,
|
||||
MaintenanceInterval: maintenanceInterval,
|
||||
MaintenanceBatchSize: maintenanceBatchSize,
|
||||
PublicTicketProjectLimit: publicTicketProjectLimit,
|
||||
PublicTicketPhoneLimit: publicTicketPhoneLimit,
|
||||
PublicTicketRateWindow: publicTicketRateWindow,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user