加固后台管理安全
This commit is contained in:
@@ -7,14 +7,14 @@ import (
|
||||
)
|
||||
|
||||
type AdminUserDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status string `json:"status"`
|
||||
Roles []adminrole.RoleDTO `json:"roles"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status string `json:"status"`
|
||||
Roles []adminrole.RoleDTO `json:"roles"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type PaginatedResult struct {
|
||||
@@ -43,3 +43,7 @@ type ChangePasswordRequest struct {
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required"`
|
||||
}
|
||||
|
||||
type ResetPasswordRequest struct {
|
||||
NewPassword string `json:"new_password" binding:"required"`
|
||||
}
|
||||
|
||||
@@ -106,8 +106,13 @@ func (h *Handler) AssignRoles(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *Handler) ChangePassword(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
h.ChangeOwnPassword(c)
|
||||
}
|
||||
|
||||
func (h *Handler) ChangeOwnPassword(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
var req ChangePasswordRequest
|
||||
@@ -115,13 +120,39 @@ func (h *Handler) ChangePassword(c *gin.Context) {
|
||||
response.BadRequest(c, "密码不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.ChangePassword(c.Request.Context(), id, req); err != nil {
|
||||
if err := h.service.ChangeOwnPassword(c.Request.Context(), adminID, req); err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func (h *Handler) ResetPassword(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req ResetPasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "密码不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.ResetPassword(c.Request.Context(), id, req); err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func currentAdminID(c *gin.Context) (uint64, bool) {
|
||||
value, ok := c.Get(middleware.ContextAdminID)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
adminID, ok := value.(uint64)
|
||||
return adminID, ok
|
||||
}
|
||||
|
||||
func parseID(c *gin.Context) (uint64, bool) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
@@ -141,6 +172,8 @@ func writeError(c *gin.Context, err error) {
|
||||
response.BadRequest(c, "不能删除最后一个超级管理员")
|
||||
case errors.Is(err, ErrWrongPassword):
|
||||
response.BadRequest(c, "原密码错误")
|
||||
case errors.Is(err, ErrWeakPassword):
|
||||
response.BadRequest(c, "密码至少 8 位且需包含字母和数字")
|
||||
case IsNotFound(err):
|
||||
response.Error(c, http.StatusNotFound, "not_found", "管理员不存在")
|
||||
default:
|
||||
|
||||
@@ -76,10 +76,12 @@ func (r *Repository) Create(ctx context.Context, req CreateAdminRequest) (*Admin
|
||||
return nil, err
|
||||
}
|
||||
admin := model.AdminUser{
|
||||
Username: req.Username,
|
||||
PasswordHash: string(hash),
|
||||
Nickname: req.Nickname,
|
||||
Status: "active",
|
||||
Username: req.Username,
|
||||
PasswordHash: string(hash),
|
||||
Nickname: req.Nickname,
|
||||
Status: "active",
|
||||
TokenVersion: 1,
|
||||
PasswordMustChange: true,
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Create(&admin).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -102,6 +104,12 @@ func (r *Repository) Update(ctx context.Context, id uint64, req UpdateAdminReque
|
||||
if err := db.Save(&admin).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Status != "" {
|
||||
if err := r.bumpTokenVersion(db, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.invalidatePermCache(ctx, id)
|
||||
}
|
||||
return r.FindByID(ctx, id)
|
||||
}
|
||||
|
||||
@@ -150,7 +158,7 @@ func (r *Repository) AssignRoles(ctx context.Context, adminID uint64, roleIDs []
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return r.bumpTokenVersion(tx, adminID)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -159,7 +167,7 @@ func (r *Repository) AssignRoles(ctx context.Context, adminID uint64, roleIDs []
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ChangePassword(ctx context.Context, id uint64, oldPwd, newPwd string) error {
|
||||
func (r *Repository) ChangeOwnPassword(ctx context.Context, id uint64, oldPwd, newPwd string) error {
|
||||
var admin model.AdminUser
|
||||
if err := r.db.WithContext(ctx).First(&admin, id).Error; err != nil {
|
||||
return err
|
||||
@@ -171,7 +179,35 @@ func (r *Repository) ChangePassword(ctx context.Context, id uint64, oldPwd, newP
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.db.WithContext(ctx).Model(&admin).Update("password_hash", string(hash)).Error
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&admin).Updates(map[string]any{
|
||||
"password_hash": string(hash),
|
||||
"password_must_change": false,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return r.bumpTokenVersion(tx, id)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) ResetPassword(ctx context.Context, id uint64, newPwd string) error {
|
||||
var admin model.AdminUser
|
||||
if err := r.db.WithContext(ctx).First(&admin, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(newPwd), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&admin).Updates(map[string]any{
|
||||
"password_hash": string(hash),
|
||||
"password_must_change": true,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return r.bumpTokenVersion(tx, id)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) GetPermissionCodes(ctx context.Context, adminID uint64) ([]string, error) {
|
||||
@@ -223,6 +259,12 @@ func (r *Repository) invalidatePermCache(ctx context.Context, adminID uint64) {
|
||||
r.redis.Del(ctx, permCacheKey(adminID))
|
||||
}
|
||||
|
||||
func (r *Repository) bumpTokenVersion(db *gorm.DB, adminID uint64) error {
|
||||
return db.Model(&model.AdminUser{}).
|
||||
Where("id = ?", adminID).
|
||||
UpdateColumn("token_version", gorm.Expr("token_version + 1")).Error
|
||||
}
|
||||
|
||||
func permCacheKey(adminID uint64) string {
|
||||
return fmt.Sprintf("admin:perms:%d", adminID)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,13 @@ package adminmgr
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||
var (
|
||||
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||
ErrWeakPassword = errors.New("weak password")
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *Repository
|
||||
@@ -39,8 +43,8 @@ func (s *Service) Create(ctx context.Context, req CreateAdminRequest) (*AdminUse
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if len(req.Password) < 6 {
|
||||
return nil, errors.New("password too short")
|
||||
if !passwordStrongEnough(req.Password) {
|
||||
return nil, ErrWeakPassword
|
||||
}
|
||||
return s.repo.Create(ctx, req)
|
||||
}
|
||||
@@ -66,12 +70,39 @@ func (s *Service) AssignRoles(ctx context.Context, adminID uint64, req AssignRol
|
||||
return s.repo.AssignRoles(ctx, adminID, req.RoleIDs)
|
||||
}
|
||||
|
||||
func (s *Service) ChangePassword(ctx context.Context, id uint64, req ChangePasswordRequest) error {
|
||||
func (s *Service) ChangeOwnPassword(ctx context.Context, adminID uint64, req ChangePasswordRequest) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
if len(req.NewPassword) < 6 {
|
||||
return errors.New("new password too short")
|
||||
if !passwordStrongEnough(req.NewPassword) {
|
||||
return ErrWeakPassword
|
||||
}
|
||||
return s.repo.ChangePassword(ctx, id, req.OldPassword, req.NewPassword)
|
||||
return s.repo.ChangeOwnPassword(ctx, adminID, req.OldPassword, req.NewPassword)
|
||||
}
|
||||
|
||||
func (s *Service) ResetPassword(ctx context.Context, id uint64, req ResetPasswordRequest) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
if !passwordStrongEnough(req.NewPassword) {
|
||||
return ErrWeakPassword
|
||||
}
|
||||
return s.repo.ResetPassword(ctx, id, req.NewPassword)
|
||||
}
|
||||
|
||||
func passwordStrongEnough(value string) bool {
|
||||
if len([]rune(value)) < 8 {
|
||||
return false
|
||||
}
|
||||
hasLetter := false
|
||||
hasDigit := false
|
||||
for _, r := range value {
|
||||
if unicode.IsLetter(r) {
|
||||
hasLetter = true
|
||||
}
|
||||
if unicode.IsDigit(r) {
|
||||
hasDigit = true
|
||||
}
|
||||
}
|
||||
return hasLetter && hasDigit
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user