| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- package service
- import (
- "context"
- "strings"
- "time"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- )
- // SecurityPolicyModeBlockSession 命中后终止会话:同一会话后续请求直接 403,
- // 需新建会话才能继续。
- const SecurityPolicyModeBlockSession = "block_session"
- // SecurityPolicyModeBlockRequest 命中后仅拦截当次请求,不记录会话封禁。
- const SecurityPolicyModeBlockRequest = "block_request"
- // NormalizeSecurityPolicyMode 把未知/空模式收敛为 block_session,保证读路径
- // 永不因脏配置静默放行。
- func NormalizeSecurityPolicyMode(mode string) string {
- if strings.EqualFold(strings.TrimSpace(mode), SecurityPolicyModeBlockRequest) {
- return SecurityPolicyModeBlockRequest
- }
- return SecurityPolicyModeBlockSession
- }
- // SecurityPolicyActionBlock / SecurityPolicyActionSessionBlock 是写入
- // content_moderation_logs 的 action。两者恒排除在封号计数之外
- // (安全策略只做会话处置,不做用户封禁)。
- const (
- SecurityPolicyActionBlock = "security_policy_block"
- SecurityPolicyActionSessionBlock = "security_policy_session_block"
- SecurityPolicyActionCategory = "security_policy"
- SecurityPolicyErrorCodeViolation = "SECURITY_POLICY_VIOLATION"
- SecurityPolicyErrorCodeTerminated = "SECURITY_POLICY_SESSION_TERMINATED"
- )
- // SecurityPolicySessionTTL 会话封禁默认有效期:命中后同一会话 24h 内直接拒绝。
- const SecurityPolicySessionTTL = 24 * time.Hour
- // SecurityPolicyKeyword 是 security_policy_keywords 表的行映射。
- type SecurityPolicyKeyword struct {
- ID int64
- GroupID *int64
- Keyword string
- Category string
- Enabled bool
- CreatedAt time.Time
- UpdatedAt time.Time
- }
- // SecurityPolicyKeywordInput 是新建自定义词的入参。GroupID 为 nil 表示全局词。
- type SecurityPolicyKeywordInput struct {
- GroupID *int64
- Keyword string
- Category string
- }
- // SecurityPolicyKeywordUpdate 是更新自定义词的入参,全 nil 表示无变更。
- type SecurityPolicyKeywordUpdate struct {
- Keyword *string
- Category *string
- Enabled *bool
- }
- var (
- // ErrSecurityPolicyKeywordExists 关键词在同一作用域已存在。
- ErrSecurityPolicyKeywordExists = infraerrors.Conflict("SECURITY_POLICY_KEYWORD_EXISTS", "security policy keyword already exists")
- // ErrSecurityPolicyKeywordNotFound 关键词不存在。
- ErrSecurityPolicyKeywordNotFound = infraerrors.NotFound("SECURITY_POLICY_KEYWORD_NOT_FOUND", "security policy keyword not found")
- )
- // SecurityPolicyRepository 是安全策略关键词仓储接口。
- type SecurityPolicyRepository interface {
- // ListEffectiveKeywords 返回对指定分组生效的自定义词(全局 + 本组),仅 enabled。
- ListEffectiveKeywords(ctx context.Context, groupID *int64) ([]SecurityPolicyKeyword, error)
- // ListKeywords 管理端查询:groupID nil = 全部,0 = 仅全局,>0 = 仅指定分组。
- ListKeywords(ctx context.Context, groupID *int64, includeDisabled bool) ([]SecurityPolicyKeyword, error)
- CreateKeyword(ctx context.Context, in SecurityPolicyKeywordInput) (*SecurityPolicyKeyword, error)
- UpdateKeyword(ctx context.Context, id int64, in SecurityPolicyKeywordUpdate) (*SecurityPolicyKeyword, error)
- DeleteKeyword(ctx context.Context, id int64) error
- }
- // SecurityPolicySessionStore 是会话封禁存储(Redis HASH:scope -> field)。
- type SecurityPolicySessionStore interface {
- // MarkSessionBlocked 在 scope 下写入一批封禁 field 并刷新 TTL。
- MarkSessionBlocked(ctx context.Context, scope string, fields []string, ttl time.Duration) error
- // FindBlockedSessionField 按序检查 fields,返回命中的第一个。
- FindBlockedSessionField(ctx context.Context, scope string, fields []string) (string, error)
- // UnblockSessionScope 清除整个 scope(管理端解封)。
- UnblockSessionScope(ctx context.Context, scope string) (bool, error)
- }
- // SecurityPolicySnapshot 是一次合并后的词表快照:全局集合(内置 seed +
- // 全局自定义词)与按组集合(各组自定义词),分别编译 AC 自动机。
- type SecurityPolicySnapshot struct {
- Global *securityPolicyCompiledSet
- ByGroup map[int64]*securityPolicyCompiledSet
- }
- type securityPolicyCompiledSet struct {
- Keywords []string
- Categories map[string]string
- Matcher *contentModerationKeywordMatcher
- }
- // BuildSecurityPolicySnapshot 合并内置 seed 与自定义词并编译。
- // 自定义词按 GroupID 归属分组集合(nil = 全局);去重大小写不敏感,
- // 内置优先保留分类。
- func BuildSecurityPolicySnapshot(custom []SecurityPolicyKeyword) *SecurityPolicySnapshot {
- snapshot := &SecurityPolicySnapshot{ByGroup: make(map[int64]*securityPolicyCompiledSet)}
- globalWords := make([]string, 0, len(SecurityPolicyBuiltinKeywords())+len(custom))
- globalCats := make(map[string]string)
- globalSeen := make(map[string]struct{})
- for _, seed := range SecurityPolicyBuiltinKeywords() {
- key := strings.ToLower(strings.TrimSpace(seed.Keyword))
- if key == "" {
- continue
- }
- if _, dup := globalSeen[key]; dup {
- continue
- }
- globalSeen[key] = struct{}{}
- globalWords = append(globalWords, seed.Keyword)
- globalCats[seed.Keyword] = seed.Category
- }
- grouped := make(map[int64]map[string]SecurityPolicyKeyword)
- for _, word := range custom {
- keyword := strings.TrimSpace(word.Keyword)
- key := strings.ToLower(keyword)
- if key == "" {
- continue
- }
- if word.GroupID == nil {
- if _, dup := globalSeen[key]; dup {
- continue
- }
- globalSeen[key] = struct{}{}
- globalWords = append(globalWords, keyword)
- category := strings.TrimSpace(word.Category)
- if category == "" {
- category = SecurityPolicyCategoryCustom
- }
- globalCats[keyword] = category
- continue
- }
- if grouped[*word.GroupID] == nil {
- grouped[*word.GroupID] = make(map[string]SecurityPolicyKeyword)
- }
- if _, dup := globalSeen[key]; dup {
- continue
- }
- if _, dup := grouped[*word.GroupID][key]; dup {
- continue
- }
- grouped[*word.GroupID][key] = word
- }
- snapshot.Global = compileSecurityPolicySet(globalWords, globalCats)
- for groupID, words := range grouped {
- keywords := make([]string, 0, len(words))
- categories := make(map[string]string, len(words))
- for _, word := range words {
- keywords = append(keywords, word.Keyword)
- category := strings.TrimSpace(word.Category)
- if category == "" {
- category = SecurityPolicyCategoryCustom
- }
- categories[word.Keyword] = category
- }
- snapshot.ByGroup[groupID] = compileSecurityPolicySet(keywords, categories)
- }
- return snapshot
- }
- func compileSecurityPolicySet(keywords []string, categories map[string]string) *securityPolicyCompiledSet {
- return &securityPolicyCompiledSet{
- Keywords: keywords,
- Categories: categories,
- Matcher: newContentModerationKeywordMatcher(keywords),
- }
- }
- // Match 在快照中匹配文本:先全局集合,再本组集合。返回命中的关键词原文与分类。
- func (s *SecurityPolicySnapshot) Match(groupID int64, text string) (string, string, bool) {
- if s == nil || strings.TrimSpace(text) == "" {
- return "", "", false
- }
- if s.Global != nil && s.Global.Matcher != nil {
- if keyword, ok := s.Global.Matcher.Match(text); ok {
- return keyword, s.Global.Categories[keyword], true
- }
- }
- if g := s.ByGroup[groupID]; g != nil && g.Matcher != nil {
- if keyword, ok := g.Matcher.Match(text); ok {
- return keyword, g.Categories[keyword], true
- }
- }
- return "", "", false
- }
|