加固后台管理安全

This commit is contained in:
yml2213
2026-06-11 07:23:00 +08:00
parent 5255b21141
commit 88b1df64e7
41 changed files with 1276 additions and 293 deletions
+12 -8
View File
@@ -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"`
}
+35 -2
View File
@@ -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)
}
+38 -7
View File
@@ -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
}