fix(api): relax account password validation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
24
backend/api/internal/logic/common/password_test.go
Normal file
24
backend/api/internal/logic/common/password_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
## 2. 实施原则
|
||||
|
||||
- 所有表均使用数据库 `id` 自增主键,作为物理主键和内部关联键。
|
||||
- 平台后台管理的平台、气站、配送、员工和业主账号在初始化、创建和修改密码时仅校验密码不少于 6 个字符,不校验字符种类或组合复杂度。
|
||||
- 所有主表额外包含 `identity` 字段,类型为 `varchar(36)`,由应用生成 UUID V7;`identity` 是对外接口、审计、跨服务关联使用的唯一业务主键,并建立唯一索引。明细表是否保留 `identity` 由是否需要对外暴露或被审计引用决定。
|
||||
- 表名、Go 模型名、模型文件名使用同一单数实体词根;接口仅以 `list`、`items` 表达集合语义,不使用复数实体名。
|
||||
- 平台角色权限统一使用 `platform_role` 与 `platform_role_menu_relation`;系统初始化时创建 `root` 系统管理员角色并授予全部菜单权限。
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
## 4. 安全、隐私与合规
|
||||
|
||||
- 登录令牌短期有效,刷新令牌可撤销;后台高权限账号启用 MFA、IP/设备策略和强密码。
|
||||
- 登录令牌短期有效,刷新令牌可撤销;后台高权限账号启用 MFA、IP/设备策略。平台后台管理的平台、气站、配送、员工和业主账号密码按当前实施口径仅要求不少于 6 个字符,不附加复杂度校验。
|
||||
- 权限校验在服务端执行,前端菜单隐藏不构成权限控制。按角色、站点、区域、对象归属联合鉴权。
|
||||
- 手机号、地址、身份证明、收款账户、定位、视频为敏感数据:传输 TLS、存储加密/字段加密、显示脱敏、访问留痕、最小化留存。
|
||||
- 所有支付回调验证签名与金额、订单、商户号一致性;合同文件使用可信第三方原文与哈希存证。
|
||||
|
||||
Reference in New Issue
Block a user