diff --git a/internal/config/config.go b/internal/config/config.go index 1e73710..8dd55b2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -31,6 +31,7 @@ type Config struct { HotRank HotRankConfig `mapstructure:"hot_rank"` WebRTC WebRTCConfig `mapstructure:"webrtc"` Report ReportConfig `mapstructure:"report"` + Sensitive SensitiveConfig `mapstructure:"sensitive"` } // Load 加载配置文件 @@ -97,6 +98,10 @@ func Load(configPath string) (*Config, error) { viper.SetDefault("s3.domain", "") viper.SetDefault("sensitive.enabled", true) viper.SetDefault("sensitive.replace_str", "***") + viper.SetDefault("sensitive.min_match_len", 2) + viper.SetDefault("sensitive.load_from_db", true) + viper.SetDefault("sensitive.load_from_redis", false) + viper.SetDefault("sensitive.redis_key_prefix", "sensitive_words") viper.SetDefault("audit.enabled", false) viper.SetDefault("audit.provider", "local") viper.SetDefault("openai.enabled", true) @@ -242,6 +247,23 @@ func Load(configPath string) (*Config, error) { cfg.Encryption.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_ENCRYPTION_ENABLED", fmt.Sprintf("%t", cfg.Encryption.Enabled))) cfg.Encryption.Key = getEnvOrDefault("APP_ENCRYPTION_KEY", cfg.Encryption.Key) cfg.Encryption.KeyVersion, _ = strconv.Atoi(getEnvOrDefault("APP_ENCRYPTION_KEY_VERSION", fmt.Sprintf("%d", cfg.Encryption.KeyVersion))) + // Sensitive 环境变量覆盖 + cfg.Sensitive.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_SENSITIVE_ENABLED", fmt.Sprintf("%t", cfg.Sensitive.Enabled))) + cfg.Sensitive.ReplaceStr = getEnvOrDefault("APP_SENSITIVE_REPLACE_STR", cfg.Sensitive.ReplaceStr) + cfg.Sensitive.MinMatchLen, _ = strconv.Atoi(getEnvOrDefault("APP_SENSITIVE_MIN_MATCH_LEN", fmt.Sprintf("%d", cfg.Sensitive.MinMatchLen))) + cfg.Sensitive.LoadFromDB, _ = strconv.ParseBool(getEnvOrDefault("APP_SENSITIVE_LOAD_FROM_DB", fmt.Sprintf("%t", cfg.Sensitive.LoadFromDB))) + cfg.Sensitive.LoadFromRedis, _ = strconv.ParseBool(getEnvOrDefault("APP_SENSITIVE_LOAD_FROM_REDIS", fmt.Sprintf("%t", cfg.Sensitive.LoadFromRedis))) + cfg.Sensitive.RedisKeyPrefix = getEnvOrDefault("APP_SENSITIVE_REDIS_KEY_PREFIX", cfg.Sensitive.RedisKeyPrefix) + + // 安全检查:生产模式必须设置JWT密钥 + if cfg.Server.Mode != "debug" { + if cfg.JWT.Secret == "" || cfg.JWT.Secret == "your-jwt-secret-key-change-in-production" { + return nil, fmt.Errorf("SECURITY ERROR: JWT secret must be set via APP_JWT_SECRET environment variable in production mode") + } + if len(cfg.JWT.Secret) < 32 { + return nil, fmt.Errorf("SECURITY ERROR: JWT secret must be at least 32 characters long in production mode") + } + } return &cfg, nil } diff --git a/internal/config/sensitive.go b/internal/config/sensitive.go new file mode 100644 index 0000000..e39d951 --- /dev/null +++ b/internal/config/sensitive.go @@ -0,0 +1,10 @@ +package config + +type SensitiveConfig struct { + Enabled bool `mapstructure:"enabled"` + ReplaceStr string `mapstructure:"replace_str"` + MinMatchLen int `mapstructure:"min_match_len"` + LoadFromDB bool `mapstructure:"load_from_db"` + LoadFromRedis bool `mapstructure:"load_from_redis"` + RedisKeyPrefix string `mapstructure:"redis_key_prefix"` +} diff --git a/internal/dto/dto.go b/internal/dto/dto.go index 715097d..80c0735 100644 --- a/internal/dto/dto.go +++ b/internal/dto/dto.go @@ -972,12 +972,12 @@ type AdminModeratePostRequest struct { // AdminBatchDeletePostsRequest 批量删除帖子请求 type AdminBatchDeletePostsRequest struct { - IDs []string `json:"ids" binding:"required,min=1"` + IDs []string `json:"ids" binding:"required,min=1,max=100,dive,required"` } // AdminBatchModeratePostsRequest 批量审核帖子请求 type AdminBatchModeratePostsRequest struct { - IDs []string `json:"ids" binding:"required,min=1"` + IDs []string `json:"ids" binding:"required,min=1,max=100,dive,required"` Status string `json:"status" binding:"required,oneof=published rejected"` } @@ -1045,7 +1045,7 @@ type AdminCommentDetailResponse struct { // AdminBatchDeleteCommentsRequest 批量删除评论请求 type AdminBatchDeleteCommentsRequest struct { - IDs []string `json:"ids" binding:"required,min=1"` + IDs []string `json:"ids" binding:"required,min=1,max=100,dive,required"` } // ==================== Admin Group DTOs ==================== diff --git a/internal/handler/ws_handler.go b/internal/handler/ws_handler.go index 8e1bb95..a0d8594 100644 --- a/internal/handler/ws_handler.go +++ b/internal/handler/ws_handler.go @@ -27,11 +27,43 @@ var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 4096, CheckOrigin: func(r *http.Request) bool { - return true // 允许所有来源,实际生产环境应该限制 + origin := r.Header.Get("Origin") + if origin == "" { + return true + } + return isAllowedWebSocketOrigin(origin) }, HandshakeTimeout: 10 * time.Second, } +var allowedWebSocketOrigins = []string{ + "https://bbs.littlelan.cn", + "https://admin.littlelan.cn", + "https://browser.littlelan.cn", + "http://localhost:3000", + "http://localhost:5173", + "http://127.0.0.1:3000", + "http://127.0.0.1:5173", +} + +func isAllowedWebSocketOrigin(origin string) bool { + for _, o := range allowedWebSocketOrigins { + if o == origin { + return true + } + } + + if strings.HasPrefix(origin, "http://localhost") || strings.HasPrefix(origin, "http://127.0.0.1") { + return true + } + + if strings.HasPrefix(origin, "https://") && strings.HasSuffix(origin, ".littlelan.cn") { + return true + } + + return false +} + // WSHandler WebSocket处理器 type WSHandler struct { wsHub *ws.Hub diff --git a/internal/model/comment.go b/internal/model/comment.go index f8c5946..d7ee53c 100644 --- a/internal/model/comment.go +++ b/internal/model/comment.go @@ -62,8 +62,8 @@ func (Comment) TableName() string { // CommentLike 评论点赞 type CommentLike struct { ID string `json:"id" gorm:"type:varchar(36);primaryKey"` - CommentID string `json:"comment_id" gorm:"type:varchar(36);index;not null"` - UserID string `json:"user_id" gorm:"type:varchar(36);index;not null"` + CommentID string `json:"comment_id" gorm:"type:varchar(36);not null;uniqueIndex:idx_comment_like_user,priority:1"` + UserID string `json:"user_id" gorm:"type:varchar(36);not null;uniqueIndex:idx_comment_like_user,priority:2"` CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` } diff --git a/internal/model/post.go b/internal/model/post.go index 4546506..d576aba 100644 --- a/internal/model/post.go +++ b/internal/model/post.go @@ -20,11 +20,11 @@ const ( // Post 帖子实体 type Post struct { - ID string `json:"id" gorm:"type:varchar(36);primaryKey"` - UserID string `json:"user_id" gorm:"type:varchar(36);index;index:idx_posts_user_status_created,priority:1;not null"` + ID string `json:"id" gorm:"type:varchar(36);primaryKey"` + UserID string `json:"user_id" gorm:"type:varchar(36);index;index:idx_posts_user_status_created,priority:1;not null"` ChannelID *string `json:"channel_id,omitempty" gorm:"type:varchar(36);index"` - Title string `json:"title" gorm:"type:varchar(200);not null"` - Content string `json:"content" gorm:"type:text;not null"` + Title string `json:"title" gorm:"type:varchar(200);not null"` + Content string `json:"content" gorm:"type:text;not null"` // 关联 // User 需要参与缓存序列化;否则列表命中缓存后会丢失作者信息,前端退化为“匿名用户” @@ -38,11 +38,11 @@ type Post struct { RejectReason string `json:"reject_reason" gorm:"type:varchar(500)"` // 统计 - LikesCount int `json:"likes_count" gorm:"column:likes_count;default:0"` - CommentsCount int `json:"comments_count" gorm:"column:comments_count;default:0"` - FavoritesCount int `json:"favorites_count" gorm:"column:favorites_count;default:0"` - SharesCount int `json:"shares_count" gorm:"column:shares_count;default:0"` - ViewsCount int `json:"views_count" gorm:"column:views_count;default:0"` + LikesCount int `json:"likes_count" gorm:"column:likes_count;default:0"` + CommentsCount int `json:"comments_count" gorm:"column:comments_count;default:0"` + FavoritesCount int `json:"favorites_count" gorm:"column:favorites_count;default:0"` + SharesCount int `json:"shares_count" gorm:"column:shares_count;default:0"` + ViewsCount int `json:"views_count" gorm:"column:views_count;default:0"` // 置顶/锁定 IsPinned bool `json:"is_pinned" gorm:"default:false"` @@ -78,7 +78,7 @@ func (Post) TableName() string { // PostImage 帖子图片 type PostImage struct { ID string `json:"id" gorm:"type:varchar(36);primaryKey"` - PostID string `json:"post_id" gorm:"type:varchar(36);index;not null"` + PostID string `json:"post_id" gorm:"type:varchar(36);not null;index:idx_post_images_post_sort,priority:1"` URL string `json:"url" gorm:"type:text;not null"` ThumbnailURL string `json:"thumbnail_url" gorm:"type:text"` PreviewURL string `json:"preview_url" gorm:"type:text"` // 列表/网格预览图(最大300px) @@ -87,7 +87,7 @@ type PostImage struct { Height int `json:"height" gorm:"default:0"` Size int64 `json:"size" gorm:"default:0"` // 文件大小(字节) MimeType string `json:"mime_type" gorm:"type:varchar(50)"` - SortOrder int `json:"sort_order" gorm:"default:0"` + SortOrder int `json:"sort_order" gorm:"default:0;index:idx_post_images_post_sort,priority:2"` CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` } diff --git a/internal/pkg/utils/validator.go b/internal/pkg/utils/validator.go index 81782a3..78c5578 100644 --- a/internal/pkg/utils/validator.go +++ b/internal/pkg/utils/validator.go @@ -24,10 +24,57 @@ func ValidateUsername(username string) bool { // ValidatePassword 验证密码强度 func ValidatePassword(password string) bool { - if len(password) < 6 || len(password) > 50 { + if len(password) < 8 || len(password) > 50 { return false } - return true + + var hasUpper, hasLower, hasDigit bool + for _, c := range password { + switch { + case 'A' <= c && c <= 'Z': + hasUpper = true + case 'a' <= c && c <= 'z': + hasLower = true + case '0' <= c && c <= '9': + hasDigit = true + } + } + + return hasUpper && hasLower && hasDigit +} + +// ValidatePasswordWithDetail 验证密码强度并返回详细信息 +func ValidatePasswordWithDetail(password string) (bool, string) { + if len(password) < 8 { + return false, "密码长度至少8位" + } + if len(password) > 50 { + return false, "密码长度不能超过50位" + } + + var hasUpper, hasLower, hasDigit bool + for _, c := range password { + switch { + case 'A' <= c && c <= 'Z': + hasUpper = true + case 'a' <= c && c <= 'z': + hasLower = true + case '0' <= c && c <= '9': + hasDigit = true + } + } + + if !hasUpper { + return false, "密码必须包含大写字母" + } + if !hasLower { + return false, "密码必须包含小写字母" + } + if !hasDigit { + return false, "密码必须包含数字" + } + + return true, "" } // ValidatePhone 验证手机号 @@ -37,10 +84,13 @@ func ValidatePhone(phone string) bool { return matched } -// SanitizeHTML 清理HTML +// SanitizeHTML 清理HTML,防止XSS攻击 func SanitizeHTML(input string) string { - // 简单实现,实际使用建议用专门库 - input = strings.ReplaceAll(input, "<", "<") - input = strings.ReplaceAll(input, ">", ">") + input = strings.ReplaceAll(input, "&", "&") + input = strings.ReplaceAll(input, "<", "<") + input = strings.ReplaceAll(input, ">", ">") + input = strings.ReplaceAll(input, "\"", """) + input = strings.ReplaceAll(input, "'", "'") + input = strings.ReplaceAll(input, "/", "/") return input } diff --git a/internal/repository/comment_repo.go b/internal/repository/comment_repo.go index 8eab433..7835ac5 100644 --- a/internal/repository/comment_repo.go +++ b/internal/repository/comment_repo.go @@ -508,7 +508,7 @@ func (r *commentRepository) GetCommentsByCursor(ctx context.Context, postID stri if builder.Error() != nil { // 无效游标,返回空列表 - return cursor.NewCursorPageResult[*model.Comment]([]*model.Comment{}, "", "", false), nil + return cursor.NewCursorPageResult([]*model.Comment{}, "", "", false), nil } // 执行查询 @@ -629,7 +629,7 @@ func (r *commentRepository) GetRepliesByCursor(ctx context.Context, rootID strin if builder.Error() != nil { // 无效游标,返回空列表 - return cursor.NewCursorPageResult[*model.Comment]([]*model.Comment{}, "", "", false), nil + return cursor.NewCursorPageResult([]*model.Comment{}, "", "", false), nil } // 执行查询 diff --git a/internal/repository/group_repo.go b/internal/repository/group_repo.go index cbe2b43..7e96585 100644 --- a/internal/repository/group_repo.go +++ b/internal/repository/group_repo.go @@ -492,7 +492,7 @@ func (r *groupRepository) GetGroupsByCursor(ctx context.Context, req *cursor.Pag if builder.Error() != nil { // 无效游标,返回空列表 - return cursor.NewCursorPageResult[*model.Group]([]*model.Group{}, "", "", false), nil + return cursor.NewCursorPageResult([]*model.Group{}, "", "", false), nil } // 执行查询 @@ -551,7 +551,7 @@ func (r *groupRepository) GetUserGroupsByCursor(ctx context.Context, userID stri if builder.Error() != nil { // 无效游标,返回空列表 - return cursor.NewCursorPageResult[*model.Group]([]*model.Group{}, "", "", false), nil + return cursor.NewCursorPageResult([]*model.Group{}, "", "", false), nil } // 执行查询 @@ -605,7 +605,7 @@ func (r *groupRepository) GetMembersByCursor(ctx context.Context, groupID string if builder.Error() != nil { // 无效游标,返回空列表 - return cursor.NewCursorPageResult[*model.GroupMember]([]*model.GroupMember{}, "", "", false), nil + return cursor.NewCursorPageResult([]*model.GroupMember{}, "", "", false), nil } // 执行查询 @@ -660,7 +660,7 @@ func (r *groupRepository) GetAnnouncementsByCursor(ctx context.Context, groupID if builder.Error() != nil { // 无效游标,返回空列表 - return cursor.NewCursorPageResult[*model.GroupAnnouncement]([]*model.GroupAnnouncement{}, "", "", false), nil + return cursor.NewCursorPageResult([]*model.GroupAnnouncement{}, "", "", false), nil } // 执行查询(置顶的排在前面) diff --git a/internal/repository/message_repo.go b/internal/repository/message_repo.go index cdbed0d..b2bee1a 100644 --- a/internal/repository/message_repo.go +++ b/internal/repository/message_repo.go @@ -747,7 +747,7 @@ func (r *messageRepository) GetMessagesByCursor(ctx context.Context, conversatio if builder.Error() != nil { // 无效游标,返回空列表 - return cursor.NewCursorPageResult[*model.Message]([]*model.Message{}, "", "", false), nil + return cursor.NewCursorPageResult([]*model.Message{}, "", "", false), nil } // 执行查询 @@ -810,7 +810,7 @@ func (r *messageRepository) GetConversationsByCursor(ctx context.Context, userID if builder.Error() != nil { // 无效游标,返回空列表 - return cursor.NewCursorPageResult[*model.Conversation]([]*model.Conversation{}, "", "", false), nil + return cursor.NewCursorPageResult([]*model.Conversation{}, "", "", false), nil } // 执行查询 - 需要添加置顶排序 diff --git a/internal/repository/notification_repo.go b/internal/repository/notification_repo.go index 6aa8784..59c4d67 100644 --- a/internal/repository/notification_repo.go +++ b/internal/repository/notification_repo.go @@ -111,7 +111,7 @@ func (r *notificationRepository) GetNotificationsByCursor(ctx context.Context, u if builder.Error() != nil { // 无效游标,返回空列表 - return cursor.NewCursorPageResult[*model.Notification]([]*model.Notification{}, "", "", false), nil + return cursor.NewCursorPageResult([]*model.Notification{}, "", "", false), nil } // 执行查询 diff --git a/internal/repository/operation_log_repo.go b/internal/repository/operation_log_repo.go index b5a45b7..de7c21a 100644 --- a/internal/repository/operation_log_repo.go +++ b/internal/repository/operation_log_repo.go @@ -127,9 +127,10 @@ func (r *operationLogRepository) GetStatistics(ctx context.Context, startTime, e // 汇总结果 for _, r := range results { stats.TotalCount += r.Count - if r.Status == "success" { + switch r.Status { + case "success": stats.SuccessCount = r.Count - } else if r.Status == "fail" { + case "fail": stats.FailCount = r.Count } } diff --git a/internal/repository/post_repo.go b/internal/repository/post_repo.go index c6ade3e..0ef7e4f 100644 --- a/internal/repository/post_repo.go +++ b/internal/repository/post_repo.go @@ -996,7 +996,7 @@ func (r *postRepository) GetPostsByCursor(ctx context.Context, userID string, in if builder.Error() != nil { // 无效游标,返回空列表 - return cursor.NewCursorPageResult[*model.Post]([]*model.Post{}, "", "", false), nil + return cursor.NewCursorPageResult([]*model.Post{}, "", "", false), nil } // 执行查询 @@ -1064,7 +1064,7 @@ func (r *postRepository) SearchPostsByCursor(ctx context.Context, keyword string if builder.Error() != nil { // 无效游标,返回空列表 - return cursor.NewCursorPageResult[*model.Post]([]*model.Post{}, "", "", false), nil + return cursor.NewCursorPageResult([]*model.Post{}, "", "", false), nil } // 执行查询 @@ -1124,7 +1124,7 @@ func (r *postRepository) GetUserPostsByCursor(ctx context.Context, userID string if builder.Error() != nil { // 无效游标,返回空列表 - return cursor.NewCursorPageResult[*model.Post]([]*model.Post{}, "", "", false), nil + return cursor.NewCursorPageResult([]*model.Post{}, "", "", false), nil } // 执行查询 @@ -1180,7 +1180,7 @@ func (r *postRepository) GetFollowingPostsByCursor(ctx context.Context, userID s if builder.Error() != nil { // 无效游标,返回空列表 - return cursor.NewCursorPageResult[*model.Post]([]*model.Post{}, "", "", false), nil + return cursor.NewCursorPageResult([]*model.Post{}, "", "", false), nil } // 执行查询 @@ -1234,7 +1234,7 @@ func (r *postRepository) GetHotPostsByCursor(ctx context.Context, req *cursor.Pa if builder.Error() != nil { // 无效游标,返回空列表 - return cursor.NewCursorPageResult[*model.Post]([]*model.Post{}, "", "", false), nil + return cursor.NewCursorPageResult([]*model.Post{}, "", "", false), nil } // 执行查询 diff --git a/internal/repository/sensitive_word_repo.go b/internal/repository/sensitive_word_repo.go new file mode 100644 index 0000000..b754d67 --- /dev/null +++ b/internal/repository/sensitive_word_repo.go @@ -0,0 +1,66 @@ +package repository + +import ( + "context" + + "carrot_bbs/internal/model" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type SensitiveWordRepository interface { + Create(ctx context.Context, word *model.SensitiveWord) error + GetByWord(ctx context.Context, word string) (*model.SensitiveWord, error) + Update(ctx context.Context, word *model.SensitiveWord) error + Upsert(ctx context.Context, word *model.SensitiveWord) error + DeactivateByWord(ctx context.Context, word string) error + ListActive(ctx context.Context) ([]model.SensitiveWord, error) +} + +type sensitiveWordRepository struct { + db *gorm.DB +} + +func NewSensitiveWordRepository(db *gorm.DB) SensitiveWordRepository { + return &sensitiveWordRepository{db: db} +} + +func (r *sensitiveWordRepository) Create(ctx context.Context, word *model.SensitiveWord) error { + return r.db.WithContext(ctx).Create(word).Error +} + +func (r *sensitiveWordRepository) GetByWord(ctx context.Context, word string) (*model.SensitiveWord, error) { + var sw model.SensitiveWord + err := r.db.WithContext(ctx).Where("word = ?", word).First(&sw).Error + if err != nil { + return nil, err + } + return &sw, nil +} + +func (r *sensitiveWordRepository) Update(ctx context.Context, word *model.SensitiveWord) error { + return r.db.WithContext(ctx).Save(word).Error +} + +func (r *sensitiveWordRepository) Upsert(ctx context.Context, word *model.SensitiveWord) error { + return r.db.WithContext(ctx).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "word"}}, + DoUpdates: clause.AssignmentColumns([]string{ + "category", "level", "is_active", "updated_by", "remark", "updated_at", + }), + }).Create(word).Error +} + +func (r *sensitiveWordRepository) DeactivateByWord(ctx context.Context, word string) error { + return r.db.WithContext(ctx). + Model(&model.SensitiveWord{}). + Where("word = ?", word). + Update("is_active", false).Error +} + +func (r *sensitiveWordRepository) ListActive(ctx context.Context) ([]model.SensitiveWord, error) { + var words []model.SensitiveWord + err := r.db.WithContext(ctx).Where("is_active = ?", true).Find(&words).Error + return words, err +} diff --git a/internal/repository/user_repo.go b/internal/repository/user_repo.go index c0e5f7f..c1448a7 100644 --- a/internal/repository/user_repo.go +++ b/internal/repository/user_repo.go @@ -377,21 +377,15 @@ func (r *userRepository) BlockUserAndCleanupRelations(blockerID, blockedID strin } for _, uid := range []string{blockerID, blockedID} { - var followersCount int64 - if err := tx.Model(&model.Follow{}).Where("following_id = ?", uid).Count(&followersCount).Error; err != nil { - return err - } if err := tx.Model(&model.User{}).Where("id = ?", uid). - UpdateColumn("followers_count", followersCount).Error; err != nil { + UpdateColumn("followers_count", tx.Model(&model.Follow{}). + Select("COUNT(*)").Where("following_id = ?", uid)).Error; err != nil { return err } - var followingCount int64 - if err := tx.Model(&model.Follow{}).Where("follower_id = ?", uid).Count(&followingCount).Error; err != nil { - return err - } if err := tx.Model(&model.User{}).Where("id = ?", uid). - UpdateColumn("following_count", followingCount).Error; err != nil { + UpdateColumn("following_count", tx.Model(&model.Follow{}). + Select("COUNT(*)").Where("follower_id = ?", uid)).Error; err != nil { return err } } diff --git a/internal/service/group_service.go b/internal/service/group_service.go index 82e9c6f..982d668 100644 --- a/internal/service/group_service.go +++ b/internal/service/group_service.go @@ -207,7 +207,7 @@ func (s *groupService) CreateGroup(ownerID string, name string, description stri Name: name, Description: description, OwnerID: ownerID, - MemberCount: 1, // 群主 + MemberCount: 0, MaxMembers: 500, JoinType: model.JoinTypeAnyone, MuteAll: false, diff --git a/internal/service/sensitive_service.go b/internal/service/sensitive_service.go index a95998d..60beb02 100644 --- a/internal/service/sensitive_service.go +++ b/internal/service/sensitive_service.go @@ -12,9 +12,9 @@ import ( "carrot_bbs/internal/model" redisclient "carrot_bbs/internal/pkg/redis" + "carrot_bbs/internal/repository" "go.uber.org/zap" - "gorm.io/gorm" ) // ==================== DFA 敏感词过滤实现 ==================== @@ -250,7 +250,7 @@ type SensitiveService interface { // sensitiveServiceImpl 敏感词服务实现 type sensitiveServiceImpl struct { tree *SensitiveWordTree - db *gorm.DB + repo repository.SensitiveWordRepository redis *redisclient.Client config *SensitiveConfig mu sync.RWMutex @@ -272,10 +272,10 @@ type SensitiveConfig struct { } // NewSensitiveService 创建敏感词服务 -func NewSensitiveService(db *gorm.DB, redisClient *redisclient.Client, config *SensitiveConfig) SensitiveService { +func NewSensitiveService(repo repository.SensitiveWordRepository, redisClient *redisclient.Client, config *SensitiveConfig) SensitiveService { s := &sensitiveServiceImpl{ tree: NewSensitiveWordTree(), - db: db, + repo: repo, redis: redisClient, config: config, replaceStr: config.ReplaceStr, @@ -356,33 +356,16 @@ func (s *sensitiveServiceImpl) AddWord(ctx context.Context, word string, categor s.tree.AddWord(word, wordLevel, wordCategory) // 持久化到数据库 - if s.db != nil { - sensitiveWord := model.SensitiveWord{ + if s.repo != nil { + sensitiveWord := &model.SensitiveWord{ Word: word, Category: wordCategory, Level: wordLevel, IsActive: true, } - // 使用 upsert 逻辑 - var existing model.SensitiveWord - result := s.db.Where("word = ?", word).First(&existing) - if result.Error == gorm.ErrRecordNotFound { - if err := s.db.Create(&sensitiveWord).Error; err != nil { - zap.L().Warn("Failed to save sensitive word to database", - zap.Error(err), - ) - } - } else if result.Error == nil { - // 更新已存在的记录 - existing.Category = wordCategory - existing.Level = wordLevel - existing.IsActive = true - if err := s.db.Save(&existing).Error; err != nil { - zap.L().Warn("Failed to update sensitive word in database", - zap.Error(err), - ) - } + if err := s.repo.Upsert(ctx, sensitiveWord); err != nil { + return fmt.Errorf("failed to upsert sensitive word to database: %w", err) } } @@ -411,12 +394,9 @@ func (s *sensitiveServiceImpl) RemoveWord(ctx context.Context, word string) erro s.tree.RemoveWord(word) // 从数据库中标记为不活跃 - if s.db != nil { - result := s.db.Model(&model.SensitiveWord{}).Where("word = ?", word).Update("is_active", false) - if result.Error != nil { - zap.L().Warn("Failed to deactivate sensitive word in database", - zap.Error(result.Error), - ) + if s.repo != nil { + if err := s.repo.DeactivateByWord(ctx, word); err != nil { + return fmt.Errorf("failed to deactivate sensitive word in database: %w", err) } } @@ -458,12 +438,12 @@ func (s *sensitiveServiceImpl) GetWordCount(ctx context.Context) int { // loadFromDB 从数据库加载敏感词 func (s *sensitiveServiceImpl) loadFromDB(ctx context.Context) error { - if s.db == nil { + if s.repo == nil { return nil } - var words []model.SensitiveWord - if err := s.db.Where("is_active = ?", true).Find(&words).Error; err != nil { + words, err := s.repo.ListActive(ctx) + if err != nil { return err } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index d0093af..2f6d8c9 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -278,6 +278,9 @@ func (s *userServiceImpl) Login(ctx context.Context, account, password string) ( var failReason string if err != nil || user == nil { + // 时序攻击防护:执行假的密码哈希验证,保持响应时间一致 + _ = utils.CheckPasswordHash(password, "$2a$10$fakehashfortimingattackprevention1234567890") + loginResult = string(model.LoginResultFail) failReason = string(model.FailReasonUserNotFound) diff --git a/internal/wire/repository.go b/internal/wire/repository.go index 18b4cb5..2017973 100644 --- a/internal/wire/repository.go +++ b/internal/wire/repository.go @@ -37,6 +37,9 @@ var RepositorySet = wire.NewSet( // 身份认证相关仓储 repository.NewVerificationRepository, + + // 敏感词相关仓储 + repository.NewSensitiveWordRepository, ) // ProvideUserActivityRepository 提供用户活跃数据仓储 diff --git a/internal/wire/service.go b/internal/wire/service.go index 438ec72..6e676d3 100644 --- a/internal/wire/service.go +++ b/internal/wire/service.go @@ -60,6 +60,7 @@ var ServiceSet = wire.NewSet( ProvideAdminReportService, ProvideVerificationService, ProvideAdminVerificationService, + ProvideSensitiveService, // 日志服务 ProvideAsyncLogManager, @@ -445,3 +446,20 @@ func ProvideAdminVerificationService( ) service.AdminVerificationService { return service.NewAdminVerificationService(verificationRepo, userRepo) } + +// ProvideSensitiveService 提供敏感词服务 +func ProvideSensitiveService( + sensitiveWordRepo repository.SensitiveWordRepository, + redisClient *redis.Client, + cfg *config.Config, +) service.SensitiveService { + sensitiveCfg := &service.SensitiveConfig{ + Enabled: cfg.Sensitive.Enabled, + ReplaceStr: cfg.Sensitive.ReplaceStr, + MinMatchLen: cfg.Sensitive.MinMatchLen, + LoadFromDB: cfg.Sensitive.LoadFromDB, + LoadFromRedis: cfg.Sensitive.LoadFromRedis, + RedisKeyPrefix: cfg.Sensitive.RedisKeyPrefix, + } + return service.NewSensitiveService(sensitiveWordRepo, redisClient, sensitiveCfg) +}