chore: release server-v1.0.62
Deploy Server / release-deploy-server (push) Successful in 51s

服务端安全加固:多维限流(按 IP/按门店)+ 敏感接口独立速率上限抵御 DDoS/刷接口;
登录暴力破解新增按来源 IP 锁定;反代后正确识别真实客户端 IP;
门店 custom_fields 轻量配置(录入默认值)透传保存。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Wdwo7SmgBJU37cBrkhPK
This commit is contained in:
wangjia
2026-06-19 20:20:44 +08:00
parent 0d967e899a
commit 7bbc944ae2
14 changed files with 628 additions and 57 deletions
+46
View File
@@ -1,12 +1,14 @@
package service
import (
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/wangjia/jiu/backend/config"
"github.com/wangjia/jiu/backend/internal/model"
"github.com/wangjia/jiu/backend/testutil"
)
@@ -146,6 +148,50 @@ func TestAuthService_RefreshTokens_Invalid(t *testing.T) {
assert.Nil(t, newPair)
}
// TestLogin_AccountLockoutAfterMaxFailures 同一账号连续失败达阈值后锁定(回归)。
func TestLogin_AccountLockoutAfterMaxFailures(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LOCK_ACC")
testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin")
config.C.Session.MaxFailures = 3
svc := NewAuthService(db)
defer loginLim.reset("LOCK_ACC|admin")
for i := 0; i < 3; i++ {
_, _, err := svc.Login("LOCK_ACC", "admin", "wrong", DeviceInfo{Platform: "windows", IP: "10.0.0.1"})
require.ErrorIs(t, err, ErrInvalidCredentials)
}
// 第 4 次即便密码正确也被锁定拒绝。
_, _, err := svc.Login("LOCK_ACC", "admin", "password123", DeviceInfo{Platform: "windows", IP: "10.0.0.1"})
assert.ErrorIs(t, err, ErrTooManyAttempts)
}
// TestLogin_IPLockoutAcrossAccounts 单 IP 跨多个(不存在的)账号累计失败达 IP 阈值后锁该 IP。
// 每次用不同用户名,账号维度永不触发锁定,只有 IP 维度会锁——验证 per-IP 防撞库 + 防内存灌爆。
func TestLogin_IPLockoutAcrossAccounts(t *testing.T) {
db := testutil.SetupTestDB()
testutil.CreateTestShop(db, "LOCK_IP")
config.C.Session.MaxFailures = 5
config.C.Session.IPMaxFailures = 4
const attackIP = "203.0.113.9"
svc := NewAuthService(db)
defer loginLim.reset("ip|" + attackIP)
// 4 次不同用户名(invalid_user),账号 key 各不相同永不锁;IP key 累计到 4 → 锁 IP。
for i := 0; i < 4; i++ {
uname := "ghost" + strconv.Itoa(i)
_, _, err := svc.Login("LOCK_IP", uname, "whatever", DeviceInfo{Platform: "windows", IP: attackIP})
require.ErrorIs(t, err, ErrInvalidCredentials)
}
// 同 IP 再来一发(仍是新用户名,账号维度无锁)→ 被 IP 锁拦下。
_, _, err := svc.Login("LOCK_IP", "ghostX", "whatever", DeviceInfo{Platform: "windows", IP: attackIP})
assert.ErrorIs(t, err, ErrTooManyAttempts)
// 另一 IP 不受影响。
_, _, err = svc.Login("LOCK_IP", "ghostY", "whatever", DeviceInfo{Platform: "windows", IP: "198.51.100.7"})
assert.ErrorIs(t, err, ErrInvalidCredentials)
}
func TestHashPassword(t *testing.T) {
hash, err := HashPassword("mypassword")
require.NoError(t, err)