feat(config): add sensitive word filtering system with database support
All checks were successful
Build Backend / build (push) Successful in 12m54s
Build Backend / build-docker (push) Successful in 1m41s

Implement sensitive word filtering feature with configurable database and Redis
support. Add security enhancements including WebSocket origin validation, improved
password strength requirements, timing attack prevention, and production JWT secret
validation. Add batch operation validation limits and optimize user blocking
queries with subqueries.

BREAKING CHANGE: Password validation now requires minimum 8 characters with uppercase,
lowercase, and digit (was 6-50 characters without complexity requirements)
This commit is contained in:
lafay
2026-04-07 00:07:40 +08:00
parent 6429322217
commit 98b8abd26a
20 changed files with 263 additions and 84 deletions

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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 ====================

View File

@@ -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

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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, "&", "&amp;")
input = strings.ReplaceAll(input, "<", "&lt;")
input = strings.ReplaceAll(input, ">", "&gt;")
input = strings.ReplaceAll(input, "\"", "&quot;")
input = strings.ReplaceAll(input, "'", "&#x27;")
input = strings.ReplaceAll(input, "/", "&#x2F;")
return input
}

View File

@@ -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
}
// 执行查询

View File

@@ -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
}
// 执行查询(置顶的排在前面)

View File

@@ -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
}
// 执行查询 - 需要添加置顶排序

View File

@@ -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
}
// 执行查询

View File

@@ -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
}
}

View File

@@ -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
}
// 执行查询

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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)

View File

@@ -37,6 +37,9 @@ var RepositorySet = wire.NewSet(
// 身份认证相关仓储
repository.NewVerificationRepository,
// 敏感词相关仓储
repository.NewSensitiveWordRepository,
)
// ProvideUserActivityRepository 提供用户活跃数据仓储

View File

@@ -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)
}