From 1d5ac6470cdde9c8f6b16b4ea345aed0c74f2ef7 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 30 Jul 2026 10:01:19 +0800 Subject: [PATCH] fix(api): relax account password validation --- backend/api/internal/initdb/platform.go | 2 +- backend/api/internal/initdb/platform_test.go | 16 +++++++++++++ backend/api/internal/logic/common/base.go | 9 +++++++ .../internal/logic/common/password_test.go | 24 +++++++++++++++++++ backend/api/internal/logic/platform/auth.go | 10 +++++--- .../logic/platform/delivery/account.go | 6 ++++- .../internal/logic/platform/gas/account.go | 6 ++++- .../logic/platform/platform/account.go | 6 ++++- .../internal/logic/platform/staff/staff.go | 6 +++-- .../api/internal/logic/platform/user/user.go | 6 ++++- docs/05-平台总后台需求-分析.md | 1 + docs/11-数据接口与安全.md | 2 +- 12 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 backend/api/internal/logic/common/password_test.go diff --git a/backend/api/internal/initdb/platform.go b/backend/api/internal/initdb/platform.go index 5c983c5..8602c13 100644 --- a/backend/api/internal/initdb/platform.go +++ b/backend/api/internal/initdb/platform.go @@ -60,7 +60,7 @@ func InitPlatformRoot(database *gorm.DB) error { // platformRootPassword 优先读取部署环境传入的 root 初始密码。 func platformRootPassword() string { - if password := os.Getenv("HEQI_PLATFORM_ROOT_PASSWORD"); len(password) >= 12 { + if password := os.Getenv("HEQI_PLATFORM_ROOT_PASSWORD"); common.IsValidAccountPassword(password) { return password } return PlatformRootPassword diff --git a/backend/api/internal/initdb/platform_test.go b/backend/api/internal/initdb/platform_test.go index 1ceb703..3d4a205 100644 --- a/backend/api/internal/initdb/platform_test.go +++ b/backend/api/internal/initdb/platform_test.go @@ -32,3 +32,19 @@ func TestInitPlatformAccessSeedsRootRole(t *testing.T) { t.Fatal(err) } } + +func TestPlatformRootPassword(t *testing.T) { + t.Run("accepts six character environment password", func(t *testing.T) { + t.Setenv("HEQI_PLATFORM_ROOT_PASSWORD", "123456") + if got := platformRootPassword(); got != "123456" { + t.Fatalf("platformRootPassword() = %q, want environment password", got) + } + }) + + t.Run("falls back when environment password is too short", func(t *testing.T) { + t.Setenv("HEQI_PLATFORM_ROOT_PASSWORD", "12345") + if got := platformRootPassword(); got != PlatformRootPassword { + t.Fatalf("platformRootPassword() = %q, want default password", got) + } + }) +} diff --git a/backend/api/internal/logic/common/base.go b/backend/api/internal/logic/common/base.go index a3af534..e56b52e 100644 --- a/backend/api/internal/logic/common/base.go +++ b/backend/api/internal/logic/common/base.go @@ -5,6 +5,7 @@ import ( "errors" "reflect" "strings" + "unicode/utf8" "git.apinb.com/bsm-sdk/core/errcode" "git.apinb.com/bsm-sdk/core/infra" @@ -16,6 +17,14 @@ import ( "gorm.io/gorm" ) +// AccountPasswordMinLength 是账号密码允许的最低字符数。 +const AccountPasswordMinLength = 6 + +// IsValidAccountPassword 仅校验账号密码的最低字符长度。 +func IsValidAccountPassword(password string) bool { + return utf8.RuneCountInString(password) >= AccountPasswordMinLength +} + // PasswordHash creates the shared password representation used by account modules. func PasswordHash(password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) diff --git a/backend/api/internal/logic/common/password_test.go b/backend/api/internal/logic/common/password_test.go new file mode 100644 index 0000000..d6d14cc --- /dev/null +++ b/backend/api/internal/logic/common/password_test.go @@ -0,0 +1,24 @@ +package common + +import "testing" + +func TestIsValidAccountPassword(t *testing.T) { + tests := []struct { + name string + password string + want bool + }{ + {name: "five characters", password: "12345", want: false}, + {name: "six digits", password: "123456", want: true}, + {name: "six letters without complexity", password: "abcdef", want: true}, + {name: "six unicode characters", password: "密码密码密码", want: true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := IsValidAccountPassword(test.password); got != test.want { + t.Fatalf("IsValidAccountPassword(%q) = %v, want %v", test.password, got, test.want) + } + }) + } +} diff --git a/backend/api/internal/logic/platform/auth.go b/backend/api/internal/logic/platform/auth.go index b5646ff..c8cabe4 100644 --- a/backend/api/internal/logic/platform/auth.go +++ b/backend/api/internal/logic/platform/auth.go @@ -19,7 +19,7 @@ import ( // LoginRequest 是平台总后台的账号密码登录请求。 type LoginRequest struct { Username string `json:"username" binding:"required,max=64"` - Password string `json:"password" binding:"required,min=8,max=128"` + Password string `json:"password" binding:"required"` } // LoginReply 是后台登录成功后的访问凭证与账号状态。 @@ -122,8 +122,8 @@ func CurrentProfile(ctx *gin.Context) { // ChangePasswordRequest 是已登录账号的改密请求。 type ChangePasswordRequest struct { - CurrentPassword string `json:"current_password" binding:"required,min=8,max=128"` - NewPassword string `json:"new_password" binding:"required,min=12,max=128"` + CurrentPassword string `json:"current_password" binding:"required"` + NewPassword string `json:"new_password" binding:"required"` } // ChangePassword 修改当前账号密码并解除首次登录改密限制。 @@ -138,6 +138,10 @@ func ChangePassword(ctx *gin.Context) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } + if !common.IsValidAccountPassword(request.NewPassword) { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } var account models.PlatformAccount if err := impl.DBService.Where("identity = ?", claims.Identity).First(&account).Error; err != nil { infra.Response.Error(ctx, errcode.ErrRecordNotFound) diff --git a/backend/api/internal/logic/platform/delivery/account.go b/backend/api/internal/logic/platform/delivery/account.go index ab39575..541e7d3 100644 --- a/backend/api/internal/logic/platform/delivery/account.go +++ b/backend/api/internal/logic/platform/delivery/account.go @@ -14,7 +14,7 @@ import ( type accountRequest struct { Username string `json:"username" binding:"required,max=64"` - Password string `json:"password" binding:"required,min=8,max=128"` + Password string `json:"password" binding:"required"` DisplayName string `json:"display_name" binding:"max=64"` RoleCode string `json:"role_code" binding:"max=64"` DeliveryBasicIdentity string `json:"delivery_basic_identity"` @@ -50,6 +50,10 @@ func CreateDeliveryAccount(ctx *gin.Context) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } + if !common.IsValidAccountPassword(request.Password) { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } deliveryBasicID, err := common.ResolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, true) if err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) diff --git a/backend/api/internal/logic/platform/gas/account.go b/backend/api/internal/logic/platform/gas/account.go index 3cabae1..d99b1ae 100644 --- a/backend/api/internal/logic/platform/gas/account.go +++ b/backend/api/internal/logic/platform/gas/account.go @@ -14,7 +14,7 @@ import ( type accountRequest struct { Username string `json:"username" binding:"required,max=64"` - Password string `json:"password" binding:"required,min=8,max=128"` + Password string `json:"password" binding:"required"` DisplayName string `json:"display_name" binding:"max=64"` RoleCode string `json:"role_code" binding:"max=64"` GasBasicIdentity string `json:"gas_basic_identity"` @@ -49,6 +49,10 @@ func CreateGasAccount(ctx *gin.Context) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } + if !common.IsValidAccountPassword(request.Password) { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } gasBasicID, err := common.ResolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, true) if err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) diff --git a/backend/api/internal/logic/platform/platform/account.go b/backend/api/internal/logic/platform/platform/account.go index 174328f..b954ec0 100644 --- a/backend/api/internal/logic/platform/platform/account.go +++ b/backend/api/internal/logic/platform/platform/account.go @@ -36,7 +36,7 @@ func ListPlatformAccount(ctx *gin.Context) { type platformAccountRequest struct { Username string `json:"username" binding:"required,max=64"` - Password string `json:"password" binding:"required,min=8,max=128"` + Password string `json:"password" binding:"required"` DisplayName string `json:"display_name" binding:"max=64"` Avatar string `json:"avatar" binding:"max=512"` PlatformRoleCode string `json:"platform_role_code" binding:"required,max=64"` @@ -70,6 +70,10 @@ func CreatePlatformAccount(ctx *gin.Context) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } + if !common.IsValidAccountPassword(request.Password) { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } if !isAssignablePlatformRole(request.PlatformRoleCode) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return diff --git a/backend/api/internal/logic/platform/staff/staff.go b/backend/api/internal/logic/platform/staff/staff.go index b3b0ad7..237d0a8 100644 --- a/backend/api/internal/logic/platform/staff/staff.go +++ b/backend/api/internal/logic/platform/staff/staff.go @@ -51,7 +51,7 @@ func GetStaff(ctx *gin.Context) { common.GetByIdentity[models.StaffAccount](ctx) func CreateStaff(ctx *gin.Context) { var request struct { Username string `json:"username" binding:"required,max=64"` - Password string `json:"password" binding:"required,min=8,max=128"` + Password string `json:"password" binding:"required"` Name string `json:"name" binding:"required,max=64"` Phone string `json:"phone" binding:"max=32"` Avatar string `json:"avatar" binding:"max=512"` @@ -60,7 +60,9 @@ func CreateStaff(ctx *gin.Context) { DeliveryBasicIdentity string `json:"delivery_basic_identity"` WorkStatus string `json:"work_status" binding:"max=32"` } - if err := ctx.ShouldBindJSON(&request); err != nil || !validStaffRole(request.RoleCode) { + if err := ctx.ShouldBindJSON(&request); err != nil || + !common.IsValidAccountPassword(request.Password) || + !validStaffRole(request.RoleCode) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } diff --git a/backend/api/internal/logic/platform/user/user.go b/backend/api/internal/logic/platform/user/user.go index c1a1727..e60aa57 100644 --- a/backend/api/internal/logic/platform/user/user.go +++ b/backend/api/internal/logic/platform/user/user.go @@ -19,7 +19,7 @@ func GetUser(ctx *gin.Context) { common.GetByIdentity[models.UserAccount](ctx) } func CreateUser(ctx *gin.Context) { var request struct { Username string `json:"username" binding:"required,max=64"` - Password string `json:"password" binding:"required,min=8,max=128"` + Password string `json:"password" binding:"required"` Name string `json:"name" binding:"required,max=64"` Phone string `json:"phone" binding:"max=32"` Avatar string `json:"avatar" binding:"max=512"` @@ -29,6 +29,10 @@ func CreateUser(ctx *gin.Context) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } + if !common.IsValidAccountPassword(request.Password) { + infra.Response.Error(ctx, errcode.ErrInvalidArgument) + return + } hash, err := common.PasswordHash(request.Password) if err != nil { infra.Response.Error(ctx, err) diff --git a/docs/05-平台总后台需求-分析.md b/docs/05-平台总后台需求-分析.md index 1297417..b89d14f 100644 --- a/docs/05-平台总后台需求-分析.md +++ b/docs/05-平台总后台需求-分析.md @@ -26,6 +26,7 @@ ## 2. 实施原则 - 所有表均使用数据库 `id` 自增主键,作为物理主键和内部关联键。 +- 平台后台管理的平台、气站、配送、员工和业主账号在初始化、创建和修改密码时仅校验密码不少于 6 个字符,不校验字符种类或组合复杂度。 - 所有主表额外包含 `identity` 字段,类型为 `varchar(36)`,由应用生成 UUID V7;`identity` 是对外接口、审计、跨服务关联使用的唯一业务主键,并建立唯一索引。明细表是否保留 `identity` 由是否需要对外暴露或被审计引用决定。 - 表名、Go 模型名、模型文件名使用同一单数实体词根;接口仅以 `list`、`items` 表达集合语义,不使用复数实体名。 - 平台角色权限统一使用 `platform_role` 与 `platform_role_menu_relation`;系统初始化时创建 `root` 系统管理员角色并授予全部菜单权限。 diff --git a/docs/11-数据接口与安全.md b/docs/11-数据接口与安全.md index 2b63c27..d25c18c 100644 --- a/docs/11-数据接口与安全.md +++ b/docs/11-数据接口与安全.md @@ -87,7 +87,7 @@ ## 4. 安全、隐私与合规 -- 登录令牌短期有效,刷新令牌可撤销;后台高权限账号启用 MFA、IP/设备策略和强密码。 +- 登录令牌短期有效,刷新令牌可撤销;后台高权限账号启用 MFA、IP/设备策略。平台后台管理的平台、气站、配送、员工和业主账号密码按当前实施口径仅要求不少于 6 个字符,不附加复杂度校验。 - 权限校验在服务端执行,前端菜单隐藏不构成权限控制。按角色、站点、区域、对象归属联合鉴权。 - 手机号、地址、身份证明、收款账户、定位、视频为敏感数据:传输 TLS、存储加密/字段加密、显示脱敏、访问留痕、最小化留存。 - 所有支付回调验证签名与金额、订单、商户号一致性;合同文件使用可信第三方原文与哈希存证。