feat(server): 新增修改密码接口 POST /api/auth/password(验原密码、作废会话)
This commit is contained in:
1 parent
ad2322d558
commit
4cbf5a74f0
13 files changed
+528
-20
No files matched your search
@@ -202,6 +202,19 @@ func (s *Service) SetPassword(ctx context.Context, username, password string, mu
|
||||
return s.RevokeUserSessions(ctx, id)
|
||||
}
|
||||
|
||||
// VerifyPassword 校验指定用户的原密码,不创建会话(供「修改密码」等场景使用)。
|
||||
func (s *Service) VerifyPassword(ctx context.Context, userID int64, password string) error {
|
||||
var hash string
|
||||
if err := s.db.QueryRowContext(ctx,
|
||||
`SELECT password_hash FROM users WHERE id = ? AND active = 1`, userID).Scan(&hash); err != nil {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) != nil {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authenticate 校验用户名密码并创建会话,返回用户与不落库的明文 token。
|
||||
func (s *Service) Authenticate(ctx context.Context, username, password, ip, userAgent string) (*User, string, error) {
|
||||
if !s.AllowLogin(ip) {
|
||||
|
||||
@@ -63,6 +63,43 @@ func (s *server) me(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||
return userDTO(u), nil
|
||||
}
|
||||
|
||||
type changePasswordInput struct {
|
||||
CurrentPassword string `json:"currentPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
|
||||
// changePassword 修改当前登录用户的密码:先验原密码,再改密并作废全部会话。
|
||||
func (s *server) changePassword(w http.ResponseWriter, r *http.Request) (any, error) {
|
||||
u := middleware.User(r.Context())
|
||||
if u.Role == auth.RoleGuest {
|
||||
return nil, &apiError{status: http.StatusUnauthorized, code: "AUTH_REQUIRED", message: "请先登录"}
|
||||
}
|
||||
|
||||
var in changePasswordInput
|
||||
if err := decodeJSON(r, &in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(in.NewPassword) < 8 {
|
||||
return nil, badRequest("新密码至少 8 位", nil)
|
||||
}
|
||||
if in.NewPassword == in.CurrentPassword {
|
||||
return nil, badRequest("新密码不能与原密码相同", nil)
|
||||
}
|
||||
// 会话被盗后不能直接改密,必须再验一次原密码
|
||||
if err := s.auth.VerifyPassword(r.Context(), u.ID, in.CurrentPassword); err != nil {
|
||||
return nil, &apiError{status: http.StatusUnauthorized, code: "AUTH_FAILED", message: "原密码不正确"}
|
||||
}
|
||||
if err := s.auth.SetPassword(r.Context(), u.Username, in.NewPassword, false); err != nil {
|
||||
return nil, internal(err)
|
||||
}
|
||||
|
||||
// SetPassword 已作废全部会话,这里清掉 Cookie 让前端引导重新登录
|
||||
s.auth.ClearSessionCookie(w)
|
||||
s.auth.ClearCSRFCookie(w)
|
||||
s.audit(r.Context(), "auth.change_password", "user", u.ID, nil, map[string]any{"username": u.Username})
|
||||
return map[string]any{"relogin": true}, nil
|
||||
}
|
||||
|
||||
func userDTO(u *auth.User) currentUserDTO {
|
||||
dto := currentUserDTO{
|
||||
ID: u.ID,
|
||||
|
||||
@@ -139,6 +139,7 @@ func (s *server) routes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("POST /api/auth/login", s.handle(s.login))
|
||||
mux.HandleFunc("POST /api/auth/logout", s.handle(s.logout))
|
||||
mux.HandleFunc("GET /api/auth/me", s.handle(s.me))
|
||||
mux.HandleFunc("POST /api/auth/password", s.handle(s.changePassword))
|
||||
|
||||
// 系统配置(guest 一律 403)
|
||||
mux.HandleFunc("GET /api/settings", s.read(auth.RoleAdmin, s.getSettingsHandler))
|
||||
|
||||
Reference in new issue
Block a user