fix(user): restore plaintext default password checking and warning mechanism

This commit is contained in:
ryan
2026-08-29 11:46:39 +08:00
parent 86c750077f
commit 107251891f
5 changed files with 77 additions and 35 deletions
@@ -30,7 +30,7 @@ ON CONFLICT (username) DO NOTHING;
-- Seed default administrator user (username: admin, password: 12345678) -- 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) 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; ON CONFLICT (username) DO NOTHING;
-- +goose StatementEnd -- +goose StatementEnd
@@ -30,7 +30,7 @@ ON CONFLICT (username) DO NOTHING;
-- Seed default administrator user (username: admin, password: 12345678) -- 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) 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; ON CONFLICT (username) DO NOTHING;
-- +goose StatementEnd -- +goose StatementEnd
+22 -1
View File
@@ -37,6 +37,7 @@ type User struct {
AvatarURL string `json:"avatar_url" gorm:"size:255"` AvatarURL string `json:"avatar_url" gorm:"size:255"`
IsActive bool `json:"is_active" gorm:"default:true;index"` IsActive bool `json:"is_active" gorm:"default:true;index"`
IsAdmin bool `json:"is_admin" gorm:"default:false"` IsAdmin bool `json:"is_admin" gorm:"default:false"`
NeedChangePassword bool `json:"need_change_password,omitempty" gorm:"-"`
Bio string `json:"bio" gorm:"size:500"` Bio string `json:"bio" gorm:"size:500"`
Phone string `json:"phone" gorm:"size:32"` Phone string `json:"phone" gorm:"size:32"`
Gender string `json:"gender" gorm:"size:16"` Gender string `json:"gender" gorm:"size:16"`
@@ -52,6 +53,17 @@ func (User) TableName() string {
return "w_users" 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 设置加密密码 // SetEncryptedPassword 设置加密密码
func (u *User) SetEncryptedPassword(password string) error { func (u *User) SetEncryptedPassword(password string) error {
trimmed := strings.TrimSpace(password) trimmed := strings.TrimSpace(password)
@@ -66,15 +78,24 @@ func (u *User) SetEncryptedPassword(password string) error {
return nil return nil
} }
// CheckPassword 校验密码 // CheckPassword 校验密码(支持 bcrypt 哈希校验与初始明文密码校验)
func (u *User) CheckPassword(password string) bool { func (u *User) CheckPassword(password string) bool {
if u.Password == "" { if u.Password == "" {
util.DummyCheckPassword(password) util.DummyCheckPassword(password)
return false return false
} }
if !u.IsPlaintextPassword() {
return util.CheckPasswordHash(u.Password, password) return util.CheckPasswordHash(u.Password, password)
} }
// 明文密码兼容比对(识别初始默认密码向用户警告修改密码)
if u.Password == password {
u.NeedChangePassword = true
return true
}
return false
}
// loginRequest 登录请求参数 // loginRequest 登录请求参数
type loginRequest struct { type loginRequest struct {
Username string `json:"username" binding:"required"` Username string `json:"username" binding:"required"`
@@ -157,4 +157,24 @@ func TestUserLoginHTTPHandler(t *testing.T) {
setCookie := w.Header().Get("Set-Cookie") setCookie := w.Header().Get("Set-Cookie")
assert.NotEmpty(t, setCookie) assert.NotEmpty(t, setCookie)
assert.Contains(t, setCookie, "wavelet_session=") 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`)
} }
+1
View File
@@ -31,6 +31,7 @@ func toUserDTO(u *User) *contracts.UserDTO {
AvatarURL: u.AvatarURL, AvatarURL: u.AvatarURL,
IsActive: u.IsActive, IsActive: u.IsActive,
IsAdmin: u.IsAdmin, IsAdmin: u.IsAdmin,
NeedChangePassword: u.NeedChangePassword || u.IsPlaintextPassword(),
Bio: u.Bio, Bio: u.Bio,
Phone: u.Phone, Phone: u.Phone,
Gender: u.Gender, Gender: u.Gender,