feat(config): add sensitive word filtering system with database support
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:
@@ -31,6 +31,7 @@ type Config struct {
|
|||||||
HotRank HotRankConfig `mapstructure:"hot_rank"`
|
HotRank HotRankConfig `mapstructure:"hot_rank"`
|
||||||
WebRTC WebRTCConfig `mapstructure:"webrtc"`
|
WebRTC WebRTCConfig `mapstructure:"webrtc"`
|
||||||
Report ReportConfig `mapstructure:"report"`
|
Report ReportConfig `mapstructure:"report"`
|
||||||
|
Sensitive SensitiveConfig `mapstructure:"sensitive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load 加载配置文件
|
// Load 加载配置文件
|
||||||
@@ -97,6 +98,10 @@ func Load(configPath string) (*Config, error) {
|
|||||||
viper.SetDefault("s3.domain", "")
|
viper.SetDefault("s3.domain", "")
|
||||||
viper.SetDefault("sensitive.enabled", true)
|
viper.SetDefault("sensitive.enabled", true)
|
||||||
viper.SetDefault("sensitive.replace_str", "***")
|
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.enabled", false)
|
||||||
viper.SetDefault("audit.provider", "local")
|
viper.SetDefault("audit.provider", "local")
|
||||||
viper.SetDefault("openai.enabled", true)
|
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.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.Key = getEnvOrDefault("APP_ENCRYPTION_KEY", cfg.Encryption.Key)
|
||||||
cfg.Encryption.KeyVersion, _ = strconv.Atoi(getEnvOrDefault("APP_ENCRYPTION_KEY_VERSION", fmt.Sprintf("%d", cfg.Encryption.KeyVersion)))
|
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
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|||||||
10
internal/config/sensitive.go
Normal file
10
internal/config/sensitive.go
Normal 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"`
|
||||||
|
}
|
||||||
@@ -972,12 +972,12 @@ type AdminModeratePostRequest struct {
|
|||||||
|
|
||||||
// AdminBatchDeletePostsRequest 批量删除帖子请求
|
// AdminBatchDeletePostsRequest 批量删除帖子请求
|
||||||
type AdminBatchDeletePostsRequest struct {
|
type AdminBatchDeletePostsRequest struct {
|
||||||
IDs []string `json:"ids" binding:"required,min=1"`
|
IDs []string `json:"ids" binding:"required,min=1,max=100,dive,required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminBatchModeratePostsRequest 批量审核帖子请求
|
// AdminBatchModeratePostsRequest 批量审核帖子请求
|
||||||
type AdminBatchModeratePostsRequest struct {
|
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"`
|
Status string `json:"status" binding:"required,oneof=published rejected"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,7 +1045,7 @@ type AdminCommentDetailResponse struct {
|
|||||||
|
|
||||||
// AdminBatchDeleteCommentsRequest 批量删除评论请求
|
// AdminBatchDeleteCommentsRequest 批量删除评论请求
|
||||||
type AdminBatchDeleteCommentsRequest struct {
|
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 ====================
|
// ==================== Admin Group DTOs ====================
|
||||||
|
|||||||
@@ -27,11 +27,43 @@ var upgrader = websocket.Upgrader{
|
|||||||
ReadBufferSize: 1024,
|
ReadBufferSize: 1024,
|
||||||
WriteBufferSize: 4096,
|
WriteBufferSize: 4096,
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
return true // 允许所有来源,实际生产环境应该限制
|
origin := r.Header.Get("Origin")
|
||||||
|
if origin == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return isAllowedWebSocketOrigin(origin)
|
||||||
},
|
},
|
||||||
HandshakeTimeout: 10 * time.Second,
|
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处理器
|
// WSHandler WebSocket处理器
|
||||||
type WSHandler struct {
|
type WSHandler struct {
|
||||||
wsHub *ws.Hub
|
wsHub *ws.Hub
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ func (Comment) TableName() string {
|
|||||||
// CommentLike 评论点赞
|
// CommentLike 评论点赞
|
||||||
type CommentLike struct {
|
type CommentLike struct {
|
||||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||||
CommentID string `json:"comment_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);index;not null"`
|
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"`
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func (Post) TableName() string {
|
|||||||
// PostImage 帖子图片
|
// PostImage 帖子图片
|
||||||
type PostImage struct {
|
type PostImage struct {
|
||||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
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"`
|
URL string `json:"url" gorm:"type:text;not null"`
|
||||||
ThumbnailURL string `json:"thumbnail_url" gorm:"type:text"`
|
ThumbnailURL string `json:"thumbnail_url" gorm:"type:text"`
|
||||||
PreviewURL string `json:"preview_url" gorm:"type:text"` // 列表/网格预览图(最大300px)
|
PreviewURL string `json:"preview_url" gorm:"type:text"` // 列表/网格预览图(最大300px)
|
||||||
@@ -87,7 +87,7 @@ type PostImage struct {
|
|||||||
Height int `json:"height" gorm:"default:0"`
|
Height int `json:"height" gorm:"default:0"`
|
||||||
Size int64 `json:"size" gorm:"default:0"` // 文件大小(字节)
|
Size int64 `json:"size" gorm:"default:0"` // 文件大小(字节)
|
||||||
MimeType string `json:"mime_type" gorm:"type:varchar(50)"`
|
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"`
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,57 @@ func ValidateUsername(username string) bool {
|
|||||||
|
|
||||||
// ValidatePassword 验证密码强度
|
// ValidatePassword 验证密码强度
|
||||||
func ValidatePassword(password string) bool {
|
func ValidatePassword(password string) bool {
|
||||||
if len(password) < 6 || len(password) > 50 {
|
if len(password) < 8 || len(password) > 50 {
|
||||||
return false
|
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 验证手机号
|
// ValidatePhone 验证手机号
|
||||||
@@ -37,10 +84,13 @@ func ValidatePhone(phone string) bool {
|
|||||||
return matched
|
return matched
|
||||||
}
|
}
|
||||||
|
|
||||||
// SanitizeHTML 清理HTML
|
// SanitizeHTML 清理HTML,防止XSS攻击
|
||||||
func SanitizeHTML(input string) string {
|
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
|
return input
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -508,7 +508,7 @@ func (r *commentRepository) GetCommentsByCursor(ctx context.Context, postID stri
|
|||||||
|
|
||||||
if builder.Error() != nil {
|
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 {
|
if builder.Error() != nil {
|
||||||
// 无效游标,返回空列表
|
// 无效游标,返回空列表
|
||||||
return cursor.NewCursorPageResult[*model.Comment]([]*model.Comment{}, "", "", false), nil
|
return cursor.NewCursorPageResult([]*model.Comment{}, "", "", false), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行查询
|
// 执行查询
|
||||||
|
|||||||
@@ -492,7 +492,7 @@ func (r *groupRepository) GetGroupsByCursor(ctx context.Context, req *cursor.Pag
|
|||||||
|
|
||||||
if builder.Error() != nil {
|
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 {
|
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 {
|
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 {
|
if builder.Error() != nil {
|
||||||
// 无效游标,返回空列表
|
// 无效游标,返回空列表
|
||||||
return cursor.NewCursorPageResult[*model.GroupAnnouncement]([]*model.GroupAnnouncement{}, "", "", false), nil
|
return cursor.NewCursorPageResult([]*model.GroupAnnouncement{}, "", "", false), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行查询(置顶的排在前面)
|
// 执行查询(置顶的排在前面)
|
||||||
|
|||||||
@@ -747,7 +747,7 @@ func (r *messageRepository) GetMessagesByCursor(ctx context.Context, conversatio
|
|||||||
|
|
||||||
if builder.Error() != nil {
|
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 {
|
if builder.Error() != nil {
|
||||||
// 无效游标,返回空列表
|
// 无效游标,返回空列表
|
||||||
return cursor.NewCursorPageResult[*model.Conversation]([]*model.Conversation{}, "", "", false), nil
|
return cursor.NewCursorPageResult([]*model.Conversation{}, "", "", false), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行查询 - 需要添加置顶排序
|
// 执行查询 - 需要添加置顶排序
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ func (r *notificationRepository) GetNotificationsByCursor(ctx context.Context, u
|
|||||||
|
|
||||||
if builder.Error() != nil {
|
if builder.Error() != nil {
|
||||||
// 无效游标,返回空列表
|
// 无效游标,返回空列表
|
||||||
return cursor.NewCursorPageResult[*model.Notification]([]*model.Notification{}, "", "", false), nil
|
return cursor.NewCursorPageResult([]*model.Notification{}, "", "", false), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行查询
|
// 执行查询
|
||||||
|
|||||||
@@ -127,9 +127,10 @@ func (r *operationLogRepository) GetStatistics(ctx context.Context, startTime, e
|
|||||||
// 汇总结果
|
// 汇总结果
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
stats.TotalCount += r.Count
|
stats.TotalCount += r.Count
|
||||||
if r.Status == "success" {
|
switch r.Status {
|
||||||
|
case "success":
|
||||||
stats.SuccessCount = r.Count
|
stats.SuccessCount = r.Count
|
||||||
} else if r.Status == "fail" {
|
case "fail":
|
||||||
stats.FailCount = r.Count
|
stats.FailCount = r.Count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -996,7 +996,7 @@ func (r *postRepository) GetPostsByCursor(ctx context.Context, userID string, in
|
|||||||
|
|
||||||
if builder.Error() != nil {
|
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 {
|
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 {
|
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 {
|
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 {
|
if builder.Error() != nil {
|
||||||
// 无效游标,返回空列表
|
// 无效游标,返回空列表
|
||||||
return cursor.NewCursorPageResult[*model.Post]([]*model.Post{}, "", "", false), nil
|
return cursor.NewCursorPageResult([]*model.Post{}, "", "", false), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行查询
|
// 执行查询
|
||||||
|
|||||||
66
internal/repository/sensitive_word_repo.go
Normal file
66
internal/repository/sensitive_word_repo.go
Normal 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
|
||||||
|
}
|
||||||
@@ -377,21 +377,15 @@ func (r *userRepository) BlockUserAndCleanupRelations(blockerID, blockedID strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, uid := range []string{blockerID, blockedID} {
|
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).
|
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
|
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).
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ func (s *groupService) CreateGroup(ownerID string, name string, description stri
|
|||||||
Name: name,
|
Name: name,
|
||||||
Description: description,
|
Description: description,
|
||||||
OwnerID: ownerID,
|
OwnerID: ownerID,
|
||||||
MemberCount: 1, // 群主
|
MemberCount: 0,
|
||||||
MaxMembers: 500,
|
MaxMembers: 500,
|
||||||
JoinType: model.JoinTypeAnyone,
|
JoinType: model.JoinTypeAnyone,
|
||||||
MuteAll: false,
|
MuteAll: false,
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
|
|
||||||
"carrot_bbs/internal/model"
|
"carrot_bbs/internal/model"
|
||||||
redisclient "carrot_bbs/internal/pkg/redis"
|
redisclient "carrot_bbs/internal/pkg/redis"
|
||||||
|
"carrot_bbs/internal/repository"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ==================== DFA 敏感词过滤实现 ====================
|
// ==================== DFA 敏感词过滤实现 ====================
|
||||||
@@ -250,7 +250,7 @@ type SensitiveService interface {
|
|||||||
// sensitiveServiceImpl 敏感词服务实现
|
// sensitiveServiceImpl 敏感词服务实现
|
||||||
type sensitiveServiceImpl struct {
|
type sensitiveServiceImpl struct {
|
||||||
tree *SensitiveWordTree
|
tree *SensitiveWordTree
|
||||||
db *gorm.DB
|
repo repository.SensitiveWordRepository
|
||||||
redis *redisclient.Client
|
redis *redisclient.Client
|
||||||
config *SensitiveConfig
|
config *SensitiveConfig
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@@ -272,10 +272,10 @@ type SensitiveConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewSensitiveService 创建敏感词服务
|
// 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{
|
s := &sensitiveServiceImpl{
|
||||||
tree: NewSensitiveWordTree(),
|
tree: NewSensitiveWordTree(),
|
||||||
db: db,
|
repo: repo,
|
||||||
redis: redisClient,
|
redis: redisClient,
|
||||||
config: config,
|
config: config,
|
||||||
replaceStr: config.ReplaceStr,
|
replaceStr: config.ReplaceStr,
|
||||||
@@ -356,33 +356,16 @@ func (s *sensitiveServiceImpl) AddWord(ctx context.Context, word string, categor
|
|||||||
s.tree.AddWord(word, wordLevel, wordCategory)
|
s.tree.AddWord(word, wordLevel, wordCategory)
|
||||||
|
|
||||||
// 持久化到数据库
|
// 持久化到数据库
|
||||||
if s.db != nil {
|
if s.repo != nil {
|
||||||
sensitiveWord := model.SensitiveWord{
|
sensitiveWord := &model.SensitiveWord{
|
||||||
Word: word,
|
Word: word,
|
||||||
Category: wordCategory,
|
Category: wordCategory,
|
||||||
Level: wordLevel,
|
Level: wordLevel,
|
||||||
IsActive: true,
|
IsActive: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 upsert 逻辑
|
if err := s.repo.Upsert(ctx, sensitiveWord); err != nil {
|
||||||
var existing model.SensitiveWord
|
return fmt.Errorf("failed to upsert sensitive word to database: %w", err)
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,12 +394,9 @@ func (s *sensitiveServiceImpl) RemoveWord(ctx context.Context, word string) erro
|
|||||||
s.tree.RemoveWord(word)
|
s.tree.RemoveWord(word)
|
||||||
|
|
||||||
// 从数据库中标记为不活跃
|
// 从数据库中标记为不活跃
|
||||||
if s.db != nil {
|
if s.repo != nil {
|
||||||
result := s.db.Model(&model.SensitiveWord{}).Where("word = ?", word).Update("is_active", false)
|
if err := s.repo.DeactivateByWord(ctx, word); err != nil {
|
||||||
if result.Error != nil {
|
return fmt.Errorf("failed to deactivate sensitive word in database: %w", err)
|
||||||
zap.L().Warn("Failed to deactivate sensitive word in database",
|
|
||||||
zap.Error(result.Error),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,12 +438,12 @@ func (s *sensitiveServiceImpl) GetWordCount(ctx context.Context) int {
|
|||||||
|
|
||||||
// loadFromDB 从数据库加载敏感词
|
// loadFromDB 从数据库加载敏感词
|
||||||
func (s *sensitiveServiceImpl) loadFromDB(ctx context.Context) error {
|
func (s *sensitiveServiceImpl) loadFromDB(ctx context.Context) error {
|
||||||
if s.db == nil {
|
if s.repo == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var words []model.SensitiveWord
|
words, err := s.repo.ListActive(ctx)
|
||||||
if err := s.db.Where("is_active = ?", true).Find(&words).Error; err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -278,6 +278,9 @@ func (s *userServiceImpl) Login(ctx context.Context, account, password string) (
|
|||||||
var failReason string
|
var failReason string
|
||||||
|
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
|
// 时序攻击防护:执行假的密码哈希验证,保持响应时间一致
|
||||||
|
_ = utils.CheckPasswordHash(password, "$2a$10$fakehashfortimingattackprevention1234567890")
|
||||||
|
|
||||||
loginResult = string(model.LoginResultFail)
|
loginResult = string(model.LoginResultFail)
|
||||||
failReason = string(model.FailReasonUserNotFound)
|
failReason = string(model.FailReasonUserNotFound)
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ var RepositorySet = wire.NewSet(
|
|||||||
|
|
||||||
// 身份认证相关仓储
|
// 身份认证相关仓储
|
||||||
repository.NewVerificationRepository,
|
repository.NewVerificationRepository,
|
||||||
|
|
||||||
|
// 敏感词相关仓储
|
||||||
|
repository.NewSensitiveWordRepository,
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProvideUserActivityRepository 提供用户活跃数据仓储
|
// ProvideUserActivityRepository 提供用户活跃数据仓储
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ var ServiceSet = wire.NewSet(
|
|||||||
ProvideAdminReportService,
|
ProvideAdminReportService,
|
||||||
ProvideVerificationService,
|
ProvideVerificationService,
|
||||||
ProvideAdminVerificationService,
|
ProvideAdminVerificationService,
|
||||||
|
ProvideSensitiveService,
|
||||||
|
|
||||||
// 日志服务
|
// 日志服务
|
||||||
ProvideAsyncLogManager,
|
ProvideAsyncLogManager,
|
||||||
@@ -445,3 +446,20 @@ func ProvideAdminVerificationService(
|
|||||||
) service.AdminVerificationService {
|
) service.AdminVerificationService {
|
||||||
return service.NewAdminVerificationService(verificationRepo, userRepo)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user