From 107251891f5bd862f227c04e16c3323e2b083fa7 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 29 Aug 2026 11:46:39 +0800 Subject: [PATCH] fix(user): restore plaintext default password checking and warning mechanism --- .../migrations/postgres/00001_initial.sql | 2 +- .../user/migrations/sqlite/00001_initial.sql | 2 +- backend/plugins/domain/user/models.go | 57 +++++++++++++------ backend/plugins/domain/user/plugin_test.go | 20 +++++++ backend/plugins/domain/user/service.go | 31 +++++----- 5 files changed, 77 insertions(+), 35 deletions(-) diff --git a/backend/plugins/domain/user/migrations/postgres/00001_initial.sql b/backend/plugins/domain/user/migrations/postgres/00001_initial.sql index 8b275286..e183f348 100644 --- a/backend/plugins/domain/user/migrations/postgres/00001_initial.sql +++ b/backend/plugins/domain/user/migrations/postgres/00001_initial.sql @@ -30,7 +30,7 @@ ON CONFLICT (username) DO NOTHING; -- Seed default administrator user (username: admin, password: 12345678) INSERT INTO w_users (id, username, password, nickname, email, is_active, is_admin, last_login_at, created_at, updated_at) -VALUES (1, 'admin', '$2a$12$oJ3wdlwSnBxjJHUdxDlWAOF/9JyWa1Ordk8XRcfcghmLI6v4VMA12', '管理员', 'admin@wavelet.local', TRUE, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) +VALUES (1, 'admin', '12345678', '管理员', 'admin@wavelet.local', TRUE, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) ON CONFLICT (username) DO NOTHING; -- +goose StatementEnd diff --git a/backend/plugins/domain/user/migrations/sqlite/00001_initial.sql b/backend/plugins/domain/user/migrations/sqlite/00001_initial.sql index c7c2a7b7..20b7f7a0 100644 --- a/backend/plugins/domain/user/migrations/sqlite/00001_initial.sql +++ b/backend/plugins/domain/user/migrations/sqlite/00001_initial.sql @@ -30,7 +30,7 @@ ON CONFLICT (username) DO NOTHING; -- Seed default administrator user (username: admin, password: 12345678) INSERT INTO w_users (id, username, password, nickname, email, is_active, is_admin, last_login_at, created_at, updated_at) -VALUES (1, 'admin', '$2a$12$oJ3wdlwSnBxjJHUdxDlWAOF/9JyWa1Ordk8XRcfcghmLI6v4VMA12', '管理员', 'admin@wavelet.local', 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) +VALUES (1, 'admin', '12345678', '管理员', 'admin@wavelet.local', 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) ON CONFLICT (username) DO NOTHING; -- +goose StatementEnd diff --git a/backend/plugins/domain/user/models.go b/backend/plugins/domain/user/models.go index c4006cae..b742f9a9 100644 --- a/backend/plugins/domain/user/models.go +++ b/backend/plugins/domain/user/models.go @@ -29,22 +29,23 @@ func (AccessToken) TableName() string { // User 用户表实体 type User struct { - ID uint64 `json:"id,string" gorm:"primaryKey;not null"` - Username string `json:"username" gorm:"size:64;uniqueIndex"` - Password string `json:"password,omitempty" gorm:"size:255"` - Nickname string `json:"nickname" gorm:"size:255"` - Email string `json:"email" gorm:"size:255;index"` - AvatarURL string `json:"avatar_url" gorm:"size:255"` - IsActive bool `json:"is_active" gorm:"default:true;index"` - IsAdmin bool `json:"is_admin" gorm:"default:false"` - Bio string `json:"bio" gorm:"size:500"` - Phone string `json:"phone" gorm:"size:32"` - Gender string `json:"gender" gorm:"size:16"` - Website string `json:"website" gorm:"size:255"` - Location string `json:"location" gorm:"size:255"` - LastLoginAt time.Time `json:"last_login_at" gorm:"index"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"` - UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"` + ID uint64 `json:"id,string" gorm:"primaryKey;not null"` + Username string `json:"username" gorm:"size:64;uniqueIndex"` + Password string `json:"password,omitempty" gorm:"size:255"` + Nickname string `json:"nickname" gorm:"size:255"` + Email string `json:"email" gorm:"size:255;index"` + AvatarURL string `json:"avatar_url" gorm:"size:255"` + IsActive bool `json:"is_active" gorm:"default:true;index"` + IsAdmin bool `json:"is_admin" gorm:"default:false"` + NeedChangePassword bool `json:"need_change_password,omitempty" gorm:"-"` + Bio string `json:"bio" gorm:"size:500"` + Phone string `json:"phone" gorm:"size:32"` + Gender string `json:"gender" gorm:"size:16"` + Website string `json:"website" gorm:"size:255"` + Location string `json:"location" gorm:"size:255"` + LastLoginAt time.Time `json:"last_login_at" gorm:"index"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"` } // TableName 表名 @@ -52,6 +53,17 @@ func (User) TableName() string { return "w_users" } +// IsPlaintextPassword 检查当前密码是否为未加密的明文密码(如初始默认密码) +func (u *User) IsPlaintextPassword() bool { + if u.Password == "" { + return false + } + return !strings.HasPrefix(u.Password, "$2a$") && + !strings.HasPrefix(u.Password, "$2b$") && + !strings.HasPrefix(u.Password, "$2y$") && + !strings.HasPrefix(u.Password, "$2x$") +} + // SetEncryptedPassword 设置加密密码 func (u *User) SetEncryptedPassword(password string) error { trimmed := strings.TrimSpace(password) @@ -66,13 +78,22 @@ func (u *User) SetEncryptedPassword(password string) error { return nil } -// CheckPassword 校验密码 +// CheckPassword 校验密码(支持 bcrypt 哈希校验与初始明文密码校验) func (u *User) CheckPassword(password string) bool { if u.Password == "" { util.DummyCheckPassword(password) return false } - return util.CheckPasswordHash(u.Password, password) + if !u.IsPlaintextPassword() { + return util.CheckPasswordHash(u.Password, password) + } + + // 明文密码兼容比对(识别初始默认密码向用户警告修改密码) + if u.Password == password { + u.NeedChangePassword = true + return true + } + return false } // loginRequest 登录请求参数 diff --git a/backend/plugins/domain/user/plugin_test.go b/backend/plugins/domain/user/plugin_test.go index 1d471c13..eac4ec15 100644 --- a/backend/plugins/domain/user/plugin_test.go +++ b/backend/plugins/domain/user/plugin_test.go @@ -157,4 +157,24 @@ func TestUserLoginHTTPHandler(t *testing.T) { setCookie := w.Header().Get("Set-Cookie") assert.NotEmpty(t, setCookie) assert.Contains(t, setCookie, "wavelet_session=") + + // Plaintext default password seeded user + plainUser := &user.User{ + Username: "plain_admin", + Password: "12345678", // Plaintext seed + Email: "plain@example.com", + IsActive: true, + } + require.NoError(t, user.CreateUser(context.Background(), plainUser)) + + reqBodyPlain := `{"username":"plain_admin","password":"12345678"}` + reqPlain, _ := http.NewRequest(http.MethodPost, "/api/v1/user/login", bytes.NewBufferString(reqBodyPlain)) + reqPlain.Header.Set("Content-Type", "application/json") + wPlain := httptest.NewRecorder() + + r.ServeHTTP(wPlain, reqPlain) + + assert.Equal(t, http.StatusOK, wPlain.Code) + assert.Contains(t, wPlain.Body.String(), `"username":"plain_admin"`) + assert.Contains(t, wPlain.Body.String(), `"need_change_password":true`) } diff --git a/backend/plugins/domain/user/service.go b/backend/plugins/domain/user/service.go index d3b2fd15..24753636 100644 --- a/backend/plugins/domain/user/service.go +++ b/backend/plugins/domain/user/service.go @@ -24,21 +24,22 @@ func toUserDTO(u *User) *contracts.UserDTO { return nil } return &contracts.UserDTO{ - ID: u.ID, - Username: u.Username, - Nickname: u.Nickname, - Email: u.Email, - AvatarURL: u.AvatarURL, - IsActive: u.IsActive, - IsAdmin: u.IsAdmin, - Bio: u.Bio, - Phone: u.Phone, - Gender: u.Gender, - Website: u.Website, - Location: u.Location, - LastLoginAt: u.LastLoginAt, - CreatedAt: u.CreatedAt, - UpdatedAt: u.UpdatedAt, + ID: u.ID, + Username: u.Username, + Nickname: u.Nickname, + Email: u.Email, + AvatarURL: u.AvatarURL, + IsActive: u.IsActive, + IsAdmin: u.IsAdmin, + NeedChangePassword: u.NeedChangePassword || u.IsPlaintextPassword(), + Bio: u.Bio, + Phone: u.Phone, + Gender: u.Gender, + Website: u.Website, + Location: u.Location, + LastLoginAt: u.LastLoginAt, + CreatedAt: u.CreatedAt, + UpdatedAt: u.UpdatedAt, } }