固化七月迭代审计治理进展以隔离线上热修
Constraint: 切换 main 前必须保存当前七月分支全部项目进展,套餐生效提案仅属于 Iteration/7-11。 Rejected: 将七月套餐修复直接移植到 main | 两个分支的可靠投递架构不同。 Confidence: medium Scope-risk: broad Directive: 不得将本提交整体 cherry-pick 到 main;main 套餐热修必须基于其纯 Asynq 代码独立实施。 Tested: git diff --check;openspec validate fix-package-activation-starvation --strict。 Not-tested: 按用户要求未运行自动化测试;go build ./... 因当前审计改造中的 Enterprise 模型字面量和 role.recordFailure 参数类型错误未通过。
This commit is contained in:
@@ -3,19 +3,25 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
accountauditapp "github.com/break/junhong_cmp_fiber/internal/application/accountaudit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditfailure"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auth"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
securityAudit accountauditapp.Writer
|
||||
accountStore *postgres.AccountStore
|
||||
accountRoleStore *postgres.AccountRoleStore
|
||||
rolePermStore *postgres.RolePermissionStore
|
||||
@@ -25,6 +31,12 @@ type Service struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// SetSecurityAudit 注入后台认证安全状态的统一审计接缝。
|
||||
func (s *Service) SetSecurityAudit(db *gorm.DB, writer accountauditapp.Writer) {
|
||||
s.db = db
|
||||
s.securityAudit = writer
|
||||
}
|
||||
|
||||
func New(
|
||||
accountStore *postgres.AccountStore,
|
||||
accountRoleStore *postgres.AccountRoleStore,
|
||||
@@ -54,15 +66,23 @@ func (s *Service) Login(ctx context.Context, req *dto.LoginRequest, clientIP str
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询账号失败")
|
||||
}
|
||||
device := req.Device
|
||||
if device == "" {
|
||||
device = "web"
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(req.Password)); err != nil {
|
||||
s.logger.Warn("登录失败:密码错误", zap.String("username", req.Username), zap.String("ip", clientIP))
|
||||
return nil, errors.New(errors.CodeInvalidCredentials, "用户名或密码错误")
|
||||
appErr := errors.New(errors.CodeInvalidCredentials, "用户名或密码错误")
|
||||
s.recordSecurityFailure(ctx, account, constants.AuditActionAuthLogin, "拒绝后台账号登录", constants.AuditResultDenied, device, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
if account.Status != 1 {
|
||||
s.logger.Warn("登录失败:账号已禁用", zap.String("username", req.Username), zap.Uint("user_id", account.ID))
|
||||
return nil, errors.New(errors.CodeAccountDisabled, "账号已禁用")
|
||||
appErr := errors.New(errors.CodeAccountDisabled, "账号已禁用")
|
||||
s.recordSecurityFailure(ctx, account, constants.AuditActionAuthLogin, "拒绝后台账号登录", constants.AuditResultDenied, device, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
// 检查店铺状态(代理账号必须关联店铺且店铺必须启用)
|
||||
@@ -71,21 +91,21 @@ func (s *Service) Login(ctx context.Context, req *dto.LoginRequest, clientIP str
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
s.logger.Warn("登录失败:关联店铺不存在", zap.String("username", req.Username), zap.Uint("shop_id", *account.ShopID))
|
||||
return nil, errors.New(errors.CodeShopNotFound, "关联店铺不存在")
|
||||
appErr := errors.New(errors.CodeShopNotFound, "关联店铺不存在")
|
||||
s.recordSecurityFailure(ctx, account, constants.AuditActionAuthLogin, "拒绝后台账号登录", constants.AuditResultDenied, device, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
s.recordSecurityFailure(ctx, account, constants.AuditActionAuthLogin, "后台账号登录失败", constants.AuditResultFailed, device, err)
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询店铺失败")
|
||||
}
|
||||
if shop.Status != constants.StatusEnabled {
|
||||
s.logger.Warn("登录失败:关联店铺已禁用", zap.String("username", req.Username), zap.Uint("shop_id", *account.ShopID))
|
||||
return nil, errors.New(errors.CodeShopDisabled, "店铺已禁用,无法登录")
|
||||
appErr := errors.New(errors.CodeShopDisabled, "店铺已禁用,无法登录")
|
||||
s.recordSecurityFailure(ctx, account, constants.AuditActionAuthLogin, "拒绝后台账号登录", constants.AuditResultDenied, device, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
}
|
||||
|
||||
device := req.Device
|
||||
if device == "" {
|
||||
device = "web"
|
||||
}
|
||||
|
||||
var shopID, enterpriseID uint
|
||||
if account.ShopID != nil {
|
||||
shopID = *account.ShopID
|
||||
@@ -106,8 +126,16 @@ func (s *Service) Login(ctx context.Context, req *dto.LoginRequest, clientIP str
|
||||
|
||||
accessToken, refreshToken, err := s.tokenManager.GenerateTokenPair(ctx, tokenInfo)
|
||||
if err != nil {
|
||||
s.recordSecurityFailure(ctx, account, constants.AuditActionAuthLogin, "后台账号登录失败", constants.AuditResultFailed, device, err)
|
||||
return nil, err
|
||||
}
|
||||
if err := s.writeSecurityAudit(ctx, account, accountauditapp.SecurityAudit{
|
||||
ActionCode: constants.AuditActionAuthLogin, Summary: "后台账号登录", Result: constants.AuditResultSuccess,
|
||||
ActorID: account.ID, ActorName: account.Username, AuthenticationKey: "account:" + strconv.FormatUint(uint64(account.ID), 10) + ":" + device,
|
||||
Authentication: authenticationData(account.ID, device, "password", "authenticated"),
|
||||
}); err != nil {
|
||||
auditfailure.RecordSecondaryWriteFailure(constants.AuditActionAuthLogin, account.Username, contextRequestID(ctx), contextRequestID(ctx), strconv.Itoa(errors.CodeInternalError), err)
|
||||
}
|
||||
|
||||
permissions, menus, buttons, err := s.getUserPermissionsAndMenus(ctx, account.ID, account.UserType, device)
|
||||
if err != nil {
|
||||
@@ -139,6 +167,9 @@ func (s *Service) Login(ctx context.Context, req *dto.LoginRequest, clientIP str
|
||||
|
||||
func (s *Service) Logout(ctx context.Context, accessToken, refreshToken string) error {
|
||||
if err := s.tokenManager.RevokeToken(ctx, accessToken); err != nil {
|
||||
if account := s.loadAuditAccount(ctx); account != nil {
|
||||
s.recordSecurityFailure(ctx, account, constants.AuditActionAuthLogout, "后台账号退出登录失败", constants.AuditResultFailed, "", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -147,6 +178,15 @@ func (s *Service) Logout(ctx context.Context, accessToken, refreshToken string)
|
||||
s.logger.Warn("撤销 refresh token 失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
if account := s.loadAuditAccount(ctx); account != nil {
|
||||
if err := s.writeSecurityAudit(ctx, account, accountauditapp.SecurityAudit{
|
||||
ActionCode: constants.AuditActionAuthLogout, Summary: "后台账号退出登录", Result: constants.AuditResultSuccess,
|
||||
ActorID: account.ID, ActorName: account.Username, AuthenticationKey: "account:" + strconv.FormatUint(uint64(account.ID), 10) + ":session",
|
||||
Authentication: authenticationData(account.ID, "", "token", "revoked"),
|
||||
}); err != nil {
|
||||
auditfailure.RecordSecondaryWriteFailure(constants.AuditActionAuthLogout, account.Username, contextRequestID(ctx), contextRequestID(ctx), strconv.Itoa(errors.CodeInternalError), err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -185,15 +225,34 @@ func (s *Service) ChangePassword(ctx context.Context, userID uint, oldPassword,
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(oldPassword)); err != nil {
|
||||
return errors.New(errors.CodeInvalidOldPassword, "旧密码错误")
|
||||
appErr := errors.New(errors.CodeInvalidOldPassword, "旧密码错误")
|
||||
s.recordSecurityFailure(ctx, account, constants.AuditActionAccountPasswordChanged, "拒绝修改账号密码", constants.AuditResultDenied, "", appErr)
|
||||
return appErr
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
s.recordSecurityFailure(ctx, account, constants.AuditActionAccountPasswordChanged, "修改账号密码失败", constants.AuditResultFailed, "", err)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "密码加密失败")
|
||||
}
|
||||
|
||||
if err := s.accountStore.UpdatePassword(ctx, userID, string(hashedPassword), userID); err != nil {
|
||||
if s.db == nil || s.securityAudit == nil {
|
||||
return errors.New(errors.CodeInvalidStatus, "后台认证审计接缝未配置")
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := postgres.NewAccountStore(tx, nil).UpdatePassword(ctx, userID, string(hashedPassword), userID); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.securityAudit.WriteAccountSecurity(ctx, tx, accountauditapp.SecurityAudit{
|
||||
ActionCode: constants.AuditActionAccountPasswordChanged, Summary: "修改账号密码", Result: constants.AuditResultSuccess,
|
||||
ActorID: account.ID, ActorName: account.Username, Account: account,
|
||||
AuthenticationKey: "account:" + strconv.FormatUint(uint64(account.ID), 10) + ":password",
|
||||
Authentication: authenticationData(account.ID, "", "password", "changed"),
|
||||
BeforeData: map[string]any{"credentials_configured": account.Password != ""},
|
||||
AfterData: map[string]any{"credentials_configured": true},
|
||||
})
|
||||
}); err != nil {
|
||||
s.recordSecurityFailure(ctx, account, constants.AuditActionAccountPasswordChanged, "修改账号密码失败", constants.AuditResultFailed, "", err)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新密码失败")
|
||||
}
|
||||
|
||||
@@ -206,6 +265,63 @@ func (s *Service) ChangePassword(ctx context.Context, userID uint, oldPassword,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) writeSecurityAudit(ctx context.Context, account *model.Account, audit accountauditapp.SecurityAudit) error {
|
||||
if s.db == nil || s.securityAudit == nil || account == nil || account.ID == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "后台认证审计接缝未配置")
|
||||
}
|
||||
audit.Account = account
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.securityAudit.WriteAccountSecurity(ctx, tx, audit)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) recordSecurityFailure(ctx context.Context, account *model.Account, actionCode, summary, result, device string, originalErr error) {
|
||||
if account == nil || account.ID == 0 {
|
||||
return
|
||||
}
|
||||
errorCode := strconv.Itoa(errors.CodeInternalError)
|
||||
if appErr, ok := originalErr.(*errors.AppError); ok {
|
||||
errorCode = strconv.Itoa(appErr.Code)
|
||||
}
|
||||
err := s.writeSecurityAudit(ctx, account, accountauditapp.SecurityAudit{
|
||||
ActionCode: actionCode, Summary: summary, Result: result, ErrorCode: errorCode, ErrorSummary: summary,
|
||||
ActorID: account.ID, ActorName: account.Username,
|
||||
AuthenticationKey: "account:" + strconv.FormatUint(uint64(account.ID), 10) + ":security",
|
||||
Authentication: authenticationData(account.ID, device, "password", result),
|
||||
})
|
||||
if err != nil {
|
||||
auditfailure.RecordSecondaryWriteFailure(actionCode, account.Username, contextRequestID(ctx), contextRequestID(ctx), errorCode, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) loadAuditAccount(ctx context.Context) *model.Account {
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
return s.loadAuditAccountByID(ctx, userID)
|
||||
}
|
||||
|
||||
func (s *Service) loadAuditAccountByID(ctx context.Context, userID uint) *model.Account {
|
||||
if userID == 0 || s.db == nil {
|
||||
return nil
|
||||
}
|
||||
var account model.Account
|
||||
if err := s.db.WithContext(ctx).Unscoped().First(&account, userID).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
return &account
|
||||
}
|
||||
|
||||
func authenticationData(accountID uint, device, method, state string) map[string]any {
|
||||
return map[string]any{"account_id": accountID, "device": device, "auth_method": method, "state": state}
|
||||
}
|
||||
|
||||
func contextRequestID(ctx context.Context) string {
|
||||
value := middleware.GetRequestIDFromContext(ctx)
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func (s *Service) getUserPermissions(ctx context.Context, userID uint) ([]string, error) {
|
||||
accountRoles, err := s.accountRoleStore.GetByAccountID(ctx, userID)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user