当前位置: 代码网 > it编程>编程语言>Java > SpringBoot实现企业级敏感词拦截检查系统的设计方案

SpringBoot实现企业级敏感词拦截检查系统的设计方案

2026年01月15日 Java 我要评论
一、设计背景与目标1.1 业务背景在视频评论、剧情评论、私信等用户生成内容(ugc)场景中,需要对用户输入进行实时敏感词检测,保证内容合规性,防止违规内容传播,降低平台风险。应用场景:评论系统:视频评

一、设计背景与目标

1.1 业务背景

在视频评论、剧情评论、私信等用户生成内容(ugc)场景中,需要对用户输入进行实时敏感词检测,保证内容合规性,防止违规内容传播,降低平台风险。

应用场景

  • 评论系统:视频评论、剧情评论、回复内容过滤
  • 私信系统:用户间私信内容实时审核
  • 内容发布:帖子、动态、资料等ugc内容检测
  • 用户资料:昵称、个性签名等字段过滤

1.2 总体目标

  • 高可用性:系统稳定性 ≥ 99.9%,避免因敏感词检测导致业务不可用
  • 高性能:支持 15,000+ qps,单次检测耗时 < 5ms,不影响业务响应时间
  • 低侵入性:通过注解方式集成,业务代码改动最小化
  • 灵活配置:支持多种处理策略(拦截、替换、标记审核)、热更新敏感词库
  • 精准匹配:支持精确匹配、模糊匹配(变体、拆分、谐音)、白名单机制
  • 可观测性:完善的日志审计、监控告警、统计分析能力

1.3 技术架构约束

  • 运行时框架:spring boot 3.5.6 + java 17
  • 持久层:mybatis plus 3.5.9 + mysql 8.0
  • 缓存层:redis(分布式缓存) + caffeine(本地缓存)
  • 分布式锁:redisson 3.37.0
  • 消息队列:rabbitmq(异步审核通知)
  • 鉴权认证:jwt + spring security

二、核心设计理念

2.1 设计原则

  1. 性能优先:采用高效的 dfa(deterministic finite automaton)算法 + trie 树数据结构
  2. 降级保护:检测失败时不阻塞业务,允许配置降级策略
  3. 缓存分层:本地缓存(caffeine)+ 分布式缓存(redis)+ 数据库(mysql)三级存储
  4. 异步处理:敏感内容标记审核场景使用 mq 异步通知
  5. 可插拔策略:支持多种处理策略,通过配置动态切换

2.2 系统边界

包含功能

  • 敏感词检测(精确匹配、模糊匹配)
  • 多种处理策略(拦截、替换、标记审核)
  • 敏感词库管理(增删改查、热更新)
  • 白名单机制
  • 日志审计与监控

不包含功能

  • 人工审核流程(可通过 mq 对接)
  • 图片/视频内容识别(需对接 ai 服务)
  • 自然语言理解(语义分析)

三、整体技术架构

3.1 组件划分

com.video.sensitive/
├── annotation/                 # 注解层
│   └── sensitivecheck.java    # 敏感词检测注解
├── aspect/                     # aop切面层
│   └── sensitivecheckaspect.java
├── core/                       # 核心引擎
│   ├── sensitivewordfilter.java      # 敏感词过滤器接口
│   ├── dfasensitivewordfilter.java   # dfa算法实现
│   ├── sensitivewordtrie.java        # trie树结构
│   └── sensitivewordmatcher.java     # 匹配器
├── strategy/                   # 处理策略
│   ├── sensitivestrategy.java        # 策略接口
│   ├── rejectstrategy.java           # 拒绝策略
│   ├── replacestrategy.java          # 替换策略
│   └── reviewstrategy.java           # 人工审核标记策略
├── cache/                      # 缓存管理
│   ├── sensitivewordcachemanager.java
│   └── wordcachewarmer.java          # 缓存预热
├── repository/                 # 敏感词库管理
│   ├── sensitivewordrepository.java
│   └── whitelistrepository.java
├── entity/                     # 实体类
│   ├── sensitiveword.java           # 敏感词实体
│   ├── sensitivelog.java            # 审计日志实体
│   └── whitelist.java               # 白名单实体
├── mapper/                     # mybatis mapper
│   ├── sensitivewordmapper.java
│   ├── sensitivelogmapper.java
│   └── whitelistmapper.java
├── service/                    # 业务服务
│   ├── sensitivewordservice.java    # 敏感词管理服务
│   └── sensitivecheckservice.java   # 检测服务
├── controller/                 # 管理接口
│   └── sensitivewordcontroller.java
├── config/                     # 配置类
│   ├── sensitiveconfig.java
│   └── sensitiveproperties.java
├── enums/                      # 枚举类
│   ├── sensitivestrategy.java       # 处理策略枚举
│   ├── sensitivelevel.java          # 敏感等级枚举
│   └── matchtype.java               # 匹配类型枚举
├── exception/                  # 异常类
│   ├── sensitivewordexception.java
│   └── sensitivecontentexception.java
└── monitor/                    # 监控与日志
    ├── sensitivemonitor.java        # 监控指标
    └── sensitiveauditlogger.java    # 审计日志

3.2 核心流程时序图

用户 -> controller -> @sensitivecheck -> sensitivecheckaspect
                                             ↓
                         [1] 获取待检测文本 + 注解配置
                                             ↓
                         [2] sensitivewordcachemanager.getwordtrie()
                                             ↓ (缓存未命中)
                         [3] redis 获取敏感词列表
                                             ↓ (redis未命中)
                         [4] mysql 查询敏感词表
                                             ↓
                         [5] 构建 trie 树并缓存到 caffeine + redis
                                             ↓
                         [6] dfasensitivewordfilter.filter(text, wordtrie)
                                             ↓
                         [7] 返回检测结果(匹配词列表 + 位置)
                                             ↓
                     根据策略处理:
                     - reject: 抛出异常,拦截请求
                     - replace: 替换为 *** 并继续执行
                     - review: 标记待审核,发送 mq 消息
                                             ↓
                         [8] 记录审计日志(异步)
                                             ↓
                         [9] 更新监控指标
                                             ↓
                         返回结果给业务层

3.3 三级缓存架构

┌─────────────────────────────────────────────────────────┐
│                    检测请求                                       │
└────────────────────────┬────────────────────────────────┘
                         │
                         │  l1: caffeine 本地缓存
                         │  - 过期时间:10分钟
                         │  - 命中率:> 95%
                         │  - rt:< 1ms
                         │
                    [命中] │ [未命中]
                         ↓
                         │  l2: redis 分布式缓存
                         │  - 过期时间:1小时
                         │  - 命中率:> 99%
                         │  - rt:< 3ms
                         │
                    [命中] │ [未命中]
                         ↓
                         │  l3: mysql 数据库
                         │  - 持久化存储
                         │  - 用于缓存重建
                         │  - rt:< 10ms
                         │
                         ↓
                    返回 trie 树

四、核心模块设计

4.1 敏感词注解@sensitivecheck

package com.video.sensitive.annotation;

import com.video.sensitive.enums.matchtype;
import com.video.sensitive.enums.sensitivestrategytype;
import java.lang.annotation.*;

/**
 * 敏感词检测注解
 * 用于标记需要进行敏感词检测的方法参数或返回值
 */
@documented
@target({elementtype.method})
@retention(retentionpolicy.runtime)
public @interface sensitivecheck {
    
    /**
     * 需要检测的参数名称(支持spel表达式)
     * 示例:"#dto.content" 或 "#comment.content"
     */
    string[] fields() default {};
    
    /**
     * 处理策略
     * - reject: 直接拒绝请求(默认)
     * - replace: 替换敏感词为 ***
     * - review: 标记为待人工审核
     */
    sensitivestrategytype strategy() default sensitivestrategytype.reject;
    
    /**
     * 匹配类型
     * - exact: 精确匹配(默认)
     * - fuzzy: 模糊匹配(支持变体、拆分、谐音)
     */
    matchtype matchtype() default matchtype.exact;
    
    /**
     * 是否启用白名单
     */
    boolean usewhitelist() default true;
    
    /**
     * 业务场景描述(用于日志和监控)
     */
    string scene() default "";
    
    /**
     * 是否启用降级保护
     * 当检测服务异常时,是否允许请求通过
     */
    boolean enablefallback() default true;
    
    /**
     * 超时时间(毫秒)
     * 超过此时间未完成检测,触发降级
     */
    long timeout() default 100;
}

4.2 dfa 算法实现

4.2.1 trie 树数据结构

package com.video.sensitive.core;

import java.util.hashmap;
import java.util.map;

/**
 * 敏感词 trie 树节点
 */
public class sensitivewordtrie {
    
    /**
     * 根节点
     */
    private final trienode root = new trienode();
    
    /**
     * trie 树节点
     */
    private static class trienode {
        /**
         * 子节点 map(字符 -> 子节点)
         */
        private map<character, trienode> children = new hashmap<>();
        
        /**
         * 是否为敏感词结尾
         */
        private boolean isend = false;
        
        /**
         * 敏感词等级(1-低,2-中,3-高)
         */
        private int level = 1;
        
        /**
         * 敏感词原始文本(用于日志记录)
         */
        private string word;
    }
    
    /**
     * 添加敏感词到 trie 树
     * 
     * @param word 敏感词
     * @param level 敏感等级
     */
    public void addword(string word, int level) {
        if (word == null || word.isempty()) {
            return;
        }
        
        trienode node = root;
        for (char c : word.tochararray()) {
            // 转为小写统一处理
            c = character.tolowercase(c);
            node = node.children.computeifabsent(c, k -> new trienode());
        }
        node.isend = true;
        node.level = level;
        node.word = word;
    }
    
    /**
     * 检测文本中的敏感词(dfa算法)
     * 
     * @param text 待检测文本
     * @return 匹配结果列表
     */
    public list<matchresult> match(string text) {
        if (text == null || text.isempty()) {
            return collections.emptylist();
        }
        
        list<matchresult> results = new arraylist<>();
        int length = text.length();
        
        for (int i = 0; i < length; i++) {
            int matchlength = 0;
            trienode node = root;
            
            // dfa 状态机匹配
            for (int j = i; j < length && node != null; j++) {
                char c = character.tolowercase(text.charat(j));
                node = node.children.get(c);
                
                if (node != null) {
                    matchlength++;
                    // 找到完整敏感词
                    if (node.isend) {
                        matchresult result = new matchresult();
                        result.setword(node.word);
                        result.setstartindex(i);
                        result.setendindex(j + 1);
                        result.setlevel(node.level);
                        result.setmatchedtext(text.substring(i, j + 1));
                        results.add(result);
                        break;
                    }
                }
            }
        }
        
        return results;
    }
    
    /**
     * 匹配结果
     */
    @data
    public static class matchresult {
        private string word;           // 原始敏感词
        private string matchedtext;    // 匹配到的文本
        private int startindex;        // 开始位置
        private int endindex;          // 结束位置
        private int level;             // 敏感等级
    }
}

4.2.2 敏感词过滤器实现

package com.video.sensitive.core;

import com.video.sensitive.enums.matchtype;
import lombok.extern.slf4j.slf4j;
import org.springframework.stereotype.component;

import java.util.list;
import java.util.set;

/**
 * dfa 算法实现的敏感词过滤器
 */
@slf4j
@component
public class dfasensitivewordfilter implements sensitivewordfilter {
    
    @override
    public filterresult filter(string text, sensitivewordtrie wordtrie, 
                               matchtype matchtype, set<string> whitelist) {
        if (text == null || text.isempty()) {
            return filterresult.clean();
        }
        
        // 1. 执行敏感词匹配
        list<sensitivewordtrie.matchresult> matches = wordtrie.match(text);
        
        // 2. 过滤白名单
        if (whitelist != null && !whitelist.isempty()) {
            matches = matches.stream()
                    .filter(m -> !whitelist.contains(m.getword()))
                    .collect(collectors.tolist());
        }
        
        // 3. 模糊匹配增强(可选)
        if (matchtype == matchtype.fuzzy && !matches.isempty()) {
            matches = enhancefuzzymatch(text, matches);
        }
        
        // 4. 构建结果
        filterresult result = new filterresult();
        result.sethassensitiveword(!matches.isempty());
        result.setmatches(matches);
        result.setoriginaltext(text);
        
        if (!matches.isempty()) {
            result.setfilteredtext(replacematches(text, matches));
            result.sethighestlevel(matches.stream()
                    .maptoint(sensitivewordtrie.matchresult::getlevel)
                    .max()
                    .orelse(1));
        } else {
            result.setfilteredtext(text);
            result.sethighestlevel(0);
        }
        
        return result;
    }
    
    /**
     * 替换敏感词为 ***
     */
    private string replacematches(string text, list<sensitivewordtrie.matchresult> matches) {
        stringbuilder sb = new stringbuilder(text);
        
        // 从后往前替换,避免索引位移
        matches.stream()
                .sorted((a, b) -> integer.compare(b.getstartindex(), a.getstartindex()))
                .foreach(match -> {
                    int length = match.getendindex() - match.getstartindex();
                    string replacement = "*".repeat(length);
                    sb.replace(match.getstartindex(), match.getendindex(), replacement);
                });
        
        return sb.tostring();
    }
    
    /**
     * 模糊匹配增强
     * 处理变体词(如:法轮功 -> 法_轮_功、falungong)
     */
    private list<sensitivewordtrie.matchresult> enhancefuzzymatch(
            string text, list<sensitivewordtrie.matchresult> exactmatches) {
        // todo: 实现模糊匹配逻辑
        // 1. 去除特殊符号后重新匹配
        // 2. 谐音转换后匹配
        // 3. 拼音匹配
        return exactmatches;
    }
    
    /**
     * 过滤结果
     */
    @data
    public static class filterresult {
        private boolean hassensitiveword;               // 是否包含敏感词
        private string originaltext;                    // 原始文本
        private string filteredtext;                    // 过滤后文本
        private list<sensitivewordtrie.matchresult> matches;  // 匹配结果列表
        private int highestlevel;                       // 最高敏感等级
        
        public static filterresult clean() {
            filterresult result = new filterresult();
            result.sethassensitiveword(false);
            result.setmatches(collections.emptylist());
            result.sethighestlevel(0);
            return result;
        }
    }
}

4.3 缓存管理器

package com.video.sensitive.cache;

import com.github.benmanes.caffeine.cache.cache;
import com.github.benmanes.caffeine.cache.caffeine;
import com.video.sensitive.core.sensitivewordtrie;
import com.video.sensitive.entity.sensitiveword;
import com.video.sensitive.mapper.sensitivewordmapper;
import jakarta.annotation.postconstruct;
import jakarta.annotation.resource;
import lombok.extern.slf4j.slf4j;
import org.springframework.data.redis.core.stringredistemplate;
import org.springframework.stereotype.component;

import java.util.list;
import java.util.set;
import java.util.concurrent.timeunit;
import java.util.stream.collectors;

/**
 * 敏感词缓存管理器(三级缓存)
 */
@slf4j
@component
public class sensitivewordcachemanager {
    
    private static final string redis_key_words = "sensitive:words:all";
    private static final string redis_key_whitelist = "sensitive:whitelist:all";
    private static final string redis_key_version = "sensitive:version";
    
    @resource
    private sensitivewordmapper sensitivewordmapper;
    
    @resource
    private stringredistemplate stringredistemplate;
    
    /**
     * l1: caffeine 本地缓存 trie 树
     * 缓存时间:10分钟,最大 1000 个 key
     */
    private final cache<string, sensitivewordtrie> triecache = caffeine.newbuilder()
            .expireafterwrite(10, timeunit.minutes)
            .maximumsize(1000)
            .build();
    
    /**
     * l1: 白名单本地缓存
     */
    private final cache<string, set<string>> whitelistcache = caffeine.newbuilder()
            .expireafterwrite(10, timeunit.minutes)
            .maximumsize(100)
            .build();
    
    /**
     * 应用启动时预热缓存
     */
    @postconstruct
    public void warmup() {
        log.info("开始预热敏感词缓存...");
        try {
            // 预加载敏感词 trie 树
            getwordtrie();
            // 预加载白名单
            getwhitelist();
            log.info("敏感词缓存预热完成");
        } catch (exception e) {
            log.error("敏感词缓存预热失败", e);
        }
    }
    
    /**
     * 获取敏感词 trie 树(三级缓存)
     */
    public sensitivewordtrie getwordtrie() {
        string cachekey = "default";
        
        // l1: 本地缓存
        sensitivewordtrie trie = triecache.getifpresent(cachekey);
        if (trie != null) {
            log.debug("命中 caffeine 缓存");
            return trie;
        }
        
        // l2: redis 缓存
        try {
            set<string> rediswords = stringredistemplate.opsforset().members(redis_key_words);
            if (rediswords != null && !rediswords.isempty()) {
                log.debug("命中 redis 缓存,词库大小: {}", rediswords.size());
                trie = buildtriefromwords(rediswords);
                triecache.put(cachekey, trie);
                return trie;
            }
        } catch (exception e) {
            log.warn("从 redis 加载敏感词失败,降级到数据库", e);
        }
        
        // l3: mysql 数据库
        list<sensitiveword> words = sensitivewordmapper.selectlist(
                new lambdaquerywrapper<sensitiveword>()
                        .eq(sensitiveword::getstatus, 1)
                        .eq(sensitiveword::getisdeleted, 0)
        );
        
        log.info("从数据库加载敏感词,数量: {}", words.size());
        
        // 构建 trie 树
        trie = new sensitivewordtrie();
        for (sensitiveword word : words) {
            trie.addword(word.getword(), word.getlevel());
        }
        
        // 回写缓存
        triecache.put(cachekey, trie);
        
        // 回写 redis
        try {
            set<string> wordset = words.stream()
                    .map(sensitiveword::getword)
                    .collect(collectors.toset());
            stringredistemplate.opsforset().add(redis_key_words, wordset.toarray(new string[0]));
            stringredistemplate.expire(redis_key_words, 1, timeunit.hours);
        } catch (exception e) {
            log.warn("回写 redis 缓存失败", e);
        }
        
        return trie;
    }
    
    /**
     * 获取白名单(三级缓存)
     */
    public set<string> getwhitelist() {
        string cachekey = "default";
        
        // l1: 本地缓存
        set<string> whitelist = whitelistcache.getifpresent(cachekey);
        if (whitelist != null) {
            return whitelist;
        }
        
        // l2: redis 缓存
        try {
            set<string> rediswhitelist = stringredistemplate.opsforset().members(redis_key_whitelist);
            if (rediswhitelist != null && !rediswhitelist.isempty()) {
                whitelistcache.put(cachekey, rediswhitelist);
                return rediswhitelist;
            }
        } catch (exception e) {
            log.warn("从 redis 加载白名单失败", e);
        }
        
        // l3: 数据库
        list<whitelist> list = whitelistmapper.selectlist(
                new lambdaquerywrapper<whitelist>()
                        .eq(whitelist::getstatus, 1)
        );
        
        whitelist = list.stream()
                .map(whitelist::getword)
                .collect(collectors.toset());
        
        // 回写缓存
        whitelistcache.put(cachekey, whitelist);
        try {
            stringredistemplate.opsforset().add(redis_key_whitelist, whitelist.toarray(new string[0]));
            stringredistemplate.expire(redis_key_whitelist, 1, timeunit.hours);
        } catch (exception e) {
            log.warn("回写白名单 redis 缓存失败", e);
        }
        
        return whitelist;
    }
    
    /**
     * 清除所有缓存(用于敏感词更新后刷新)
     */
    public void clearcache() {
        log.info("清除敏感词缓存");
        triecache.invalidateall();
        whitelistcache.invalidateall();
        
        try {
            stringredistemplate.delete(redis_key_words);
            stringredistemplate.delete(redis_key_whitelist);
            // 版本号+1,通知其他节点刷新
            stringredistemplate.opsforvalue().increment(redis_key_version);
        } catch (exception e) {
            log.error("清除 redis 缓存失败", e);
        }
    }
    
    /**
     * 从词列表构建 trie 树
     */
    private sensitivewordtrie buildtriefromwords(set<string> words) {
        sensitivewordtrie trie = new sensitivewordtrie();
        for (string word : words) {
            trie.addword(word, 1); // 从 redis 加载默认等级为 1
        }
        return trie;
    }
}

4.4 aop 切面实现

package com.video.sensitive.aspect;

import com.video.common.result;
import com.video.sensitive.annotation.sensitivecheck;
import com.video.sensitive.cache.sensitivewordcachemanager;
import com.video.sensitive.core.dfasensitivewordfilter;
import com.video.sensitive.core.sensitivewordtrie;
import com.video.sensitive.enums.sensitivestrategytype;
import com.video.sensitive.exception.sensitivecontentexception;
import com.video.sensitive.monitor.sensitiveauditlogger;
import com.video.sensitive.monitor.sensitivemonitor;
import jakarta.annotation.resource;
import lombok.extern.slf4j.slf4j;
import org.aspectj.lang.proceedingjoinpoint;
import org.aspectj.lang.annotation.around;
import org.aspectj.lang.annotation.aspect;
import org.aspectj.lang.reflect.methodsignature;
import org.springframework.expression.expression;
import org.springframework.expression.expressionparser;
import org.springframework.expression.spel.standard.spelexpressionparser;
import org.springframework.expression.spel.support.standardevaluationcontext;
import org.springframework.stereotype.component;

import java.lang.reflect.method;
import java.util.set;
import java.util.concurrent.timeunit;

/**
 * 敏感词检测切面
 */
@slf4j
@aspect
@component
public class sensitivecheckaspect {
    
    @resource
    private dfasensitivewordfilter sensitivewordfilter;
    
    @resource
    private sensitivewordcachemanager cachemanager;
    
    @resource
    private sensitiveauditlogger auditlogger;
    
    @resource
    private sensitivemonitor monitor;
    
    private final expressionparser parser = new spelexpressionparser();
    
    @around("@annotation(com.video.sensitive.annotation.sensitivecheck)")
    public object around(proceedingjoinpoint joinpoint) throws throwable {
        long starttime = system.currenttimemillis();
        
        methodsignature signature = (methodsignature) joinpoint.getsignature();
        method method = signature.getmethod();
        sensitivecheck annotation = method.getannotation(sensitivecheck.class);
        
        try {
            // 1. 提取待检测文本
            string[] fields = annotation.fields();
            if (fields.length == 0) {
                return joinpoint.proceed();
            }
            
            object[] args = joinpoint.getargs();
            string[] paramnames = signature.getparameternames();
            
            // 2. 使用 spel 提取字段值
            standardevaluationcontext context = new standardevaluationcontext();
            for (int i = 0; i < paramnames.length; i++) {
                context.setvariable(paramnames[i], args[i]);
            }
            
            // 3. 检测每个字段
            for (string field : fields) {
                expression expression = parser.parseexpression(field);
                object value = expression.getvalue(context);
                
                if (value instanceof string) {
                    string text = (string) value;
                    checktext(text, annotation, method.getname());
                }
            }
            
            // 4. 执行业务逻辑
            object result = joinpoint.proceed();
            
            // 5. 记录监控指标
            long cost = system.currenttimemillis() - starttime;
            monitor.recordchecksuccess(annotation.scene(), cost);
            
            return result;
            
        } catch (sensitivecontentexception e) {
            // 6. 敏感词检测失败
            long cost = system.currenttimemillis() - starttime;
            monitor.recordcheckreject(annotation.scene(), cost);
            throw e;
            
        } catch (exception e) {
            // 7. 系统异常,触发降级
            long cost = system.currenttimemillis() - starttime;
            monitor.recordcheckerror(annotation.scene(), cost);
            
            if (annotation.enablefallback()) {
                log.warn("敏感词检测异常,触发降级保护,允许请求通过", e);
                return joinpoint.proceed();
            } else {
                throw e;
            }
        }
    }
    
    /**
     * 检测文本
     */
    private void checktext(string text, sensitivecheck annotation, string methodname) {
        if (text == null || text.trim().isempty()) {
            return;
        }
        
        // 1. 获取敏感词 trie 树
        sensitivewordtrie wordtrie = cachemanager.getwordtrie();
        
        // 2. 获取白名单
        set<string> whitelist = annotation.usewhitelist() ? 
                cachemanager.getwhitelist() : null;
        
        // 3. 执行过滤
        dfasensitivewordfilter.filterresult result = sensitivewordfilter.filter(
                text, wordtrie, annotation.matchtype(), whitelist);
        
        // 4. 处理结果
        if (result.ishassensitiveword()) {
            // 记录审计日志
            auditlogger.logsensitive(
                    annotation.scene(),
                    methodname,
                    text,
                    result.getmatches(),
                    annotation.strategy()
            );
            
            // 根据策略处理
            handlebystrategy(annotation.strategy(), text, result);
        }
    }
    
    /**
     * 根据策略处理敏感内容
     */
    private void handlebystrategy(sensitivestrategytype strategy, 
                                   string text, 
                                   dfasensitivewordfilter.filterresult result) {
        switch (strategy) {
            case reject:
                // 直接拒绝
                throw new sensitivecontentexception(
                        "内容包含敏感词,禁止发布",
                        result.getmatches()
                );
                
            case replace:
                // 替换敏感词(修改原对象)
                // 注意:需要通过反射修改原始对象的字段值
                log.info("替换敏感词: {} -> {}", text, result.getfilteredtext());
                break;
                
            case review:
                // 标记待审核,发送 mq 消息
                log.info("标记内容待审核: {}", text);
                // todo: 发送 rabbitmq 消息给审核系统
                break;
                
            default:
                throw new illegalargumentexception("未知策略: " + strategy);
        }
    }
}

五、数据库设计

5.1 敏感词表

create table `a_sensitive_word` (
  `id` bigint not null auto_increment comment '主键id',
  `word` varchar(200) not null comment '敏感词',
  `level` tinyint not null default 1 comment '敏感等级:1-低,2-中,3-高',
  `category` varchar(50) default null comment '分类:政治、色情、暴力、广告等',
  `status` tinyint not null default 1 comment '状态:0-禁用,1-启用',
  `remark` varchar(500) default null comment '备注说明',
  `create_user` bigint default null comment '创建人',
  `create_time` datetime not null default current_timestamp comment '创建时间',
  `update_user` bigint default null comment '修改人',
  `update_time` datetime not null default current_timestamp on update current_timestamp comment '修改时间',
  `is_deleted` tinyint not null default 0 comment '是否删除:0-未删除,1-已删除',
  primary key (`id`),
  unique key `uk_word` (`word`),
  key `idx_level` (`level`),
  key `idx_category` (`category`),
  key `idx_status` (`status`)
) engine=innodb default charset=utf8mb4 comment='敏感词表';

5.2 白名单表

create table `a_sensitive_whitelist` (
  `id` bigint not null auto_increment comment '主键id',
  `word` varchar(200) not null comment '白名单词汇',
  `status` tinyint not null default 1 comment '状态:0-禁用,1-启用',
  `remark` varchar(500) default null comment '备注说明',
  `create_user` bigint default null comment '创建人',
  `create_time` datetime not null default current_timestamp comment '创建时间',
  `update_user` bigint default null comment '修改人',
  `update_time` datetime not null default current_timestamp on update current_timestamp comment '修改时间',
  `is_deleted` tinyint not null default 0 comment '是否删除:0-未删除,1-已删除',
  primary key (`id`),
  unique key `uk_word` (`word`),
  key `idx_status` (`status`)
) engine=innodb default charset=utf8mb4 comment='敏感词白名单';

5.3 审计日志表

create table `a_sensitive_log` (
  `id` bigint not null auto_increment comment '主键id',
  `user_id` bigint default null comment '用户id',
  `scene` varchar(50) not null comment '业务场景',
  `method_name` varchar(200) default null comment '方法名',
  `original_text` text comment '原始文本',
  `matched_words` varchar(500) default null comment '匹配的敏感词(json数组)',
  `strategy` varchar(20) not null comment '处理策略',
  `result` varchar(20) not null comment '处理结果:pass-通过,reject-拒绝,review-审核',
  `ip` varchar(50) default null comment '客户端ip',
  `user_agent` varchar(500) default null comment '客户端ua',
  `create_time` datetime not null default current_timestamp comment '创建时间',
  primary key (`id`),
  key `idx_user_id` (`user_id`),
  key `idx_scene` (`scene`),
  key `idx_create_time` (`create_time`),
  key `idx_result` (`result`)
) engine=innodb default charset=utf8mb4 comment='敏感词审计日志';

-- 按月分区
alter table `a_sensitive_log` partition by range (to_days(`create_time`)) (
    partition p202601 values less than (to_days('2026-02-01')),
    partition p202602 values less than (to_days('2026-03-01')),
    partition p202603 values less than (to_days('2026-04-01')),
    partition p_max values less than maxvalue
);

六、使用示例

6.1 评论发布场景

@postmapping("/add")
@operation(summary = "发表评论")
@sensitivecheck(
    fields = {"#dto.content"},
    strategy = sensitivestrategytype.reject,
    matchtype = matchtype.exact,
    scene = "comment_publish",
    enablefallback = true,
    timeout = 100
)
public result<commentoperationvo> addcomment(
        @requestheader("userid") long userid,
        @requestbody commentdto dto) {
    videocomment comment = videocommentservice.addcomment(userid, dto);
    return result.success(commentoperationvo.of(comment));
}

6.2 私信发送场景

@postmapping("/send")
@sensitivecheck(
    fields = {"#message.content"},
    strategy = sensitivestrategytype.review,  // 私信使用人工审核
    matchtype = matchtype.fuzzy,               // 使用模糊匹配
    scene = "private_message",
    enablefallback = false                      // 不允许降级
)
public result<void> sendmessage(
        @requestheader("userid") long userid,
        @requestbody privatemessage message) {
    privatemessageservice.send(userid, message);
    return result.success();
}

6.3 剧情评论场景

@postmapping("/add")
@operation(summary = "发表剧情评论")
@sensitivecheck(
    fields = {"#dto.content"},
    strategy = sensitivestrategytype.reject,
    scene = "story_comment_publish"
)
public result<storycomment> addcomment(
        @requestparam long userid,
        @requestbody commentdto dto) {
    storycomment comment = storycommentservice.addcomment(userid, dto);
    return result.success(comment);
}

七、敏感词管理接口

7.1 管理接口设计

@restcontroller
@requestmapping("/api/admin/sensitive")
@tag(name = "敏感词管理", description = "敏感词库管理接口(需管理员权限)")
public class sensitivewordcontroller {
    
    @resource
    private sensitivewordservice sensitivewordservice;
    
    /**
     * 添加敏感词
     */
    @postmapping("/word/add")
    @preauthorize("hasauthority('role_admin')")
    public result<void> addword(@requestbody sensitiveworddto dto) {
        sensitivewordservice.addword(dto);
        return result.success();
    }
    
    /**
     * 批量导入敏感词
     */
    @postmapping("/word/batch/import")
    @preauthorize("hasauthority('role_admin')")
    public result<importresult> batchimport(@requestbody list<sensitiveworddto> words) {
        importresult result = sensitivewordservice.batchimport(words);
        return result.success(result);
    }
    
    /**
     * 删除敏感词
     */
    @deletemapping("/word/{id}")
    @preauthorize("hasauthority('role_admin')")
    public result<void> deleteword(@pathvariable long id) {
        sensitivewordservice.deleteword(id);
        return result.success();
    }
    
    /**
     * 更新敏感词
     */
    @putmapping("/word/{id}")
    @preauthorize("hasauthority('role_admin')")
    public result<void> updateword(@pathvariable long id, @requestbody sensitiveworddto dto) {
        sensitivewordservice.updateword(id, dto);
        return result.success();
    }
    
    /**
     * 刷新缓存(热更新)
     */
    @postmapping("/cache/refresh")
    @preauthorize("hasauthority('role_admin')")
    public result<void> refreshcache() {
        sensitivewordservice.refreshcache();
        return result.success();
    }
    
    /**
     * 分页查询敏感词
     */
    @getmapping("/word/page")
    @preauthorize("hasauthority('role_admin')")
    public result<ipage<sensitiveword>> getwordpage(
            @requestparam(defaultvalue = "1") int pagenum,
            @requestparam(defaultvalue = "20") int pagesize,
            @requestparam(required = false) string keyword) {
        ipage<sensitiveword> page = sensitivewordservice.getwordpage(pagenum, pagesize, keyword);
        return result.success(page);
    }
    
    /**
     * 测试文本检测
     */
    @postmapping("/test")
    @preauthorize("hasauthority('role_admin')")
    public result<filterresult> testcheck(@requestbody testrequest request) {
        filterresult result = sensitivewordservice.testcheck(request.gettext());
        return result.success(result);
    }
}

八、性能优化方案

8.1 性能设计考量

优化点具体措施预期效果
算法优化使用 dfa + trie 树,时间复杂度 o(n)单次检测 < 3ms
缓存分层caffeine(l1)+ redis(l2)+ mysql(l3)缓存命中率 > 99%
预热机制应用启动时预加载敏感词库首次请求无延迟
异步日志审计日志异步写入,不阻塞主流程rt 降低 80%
降级保护检测超时/异常时允许请求通过可用性 99.9%
批量操作批量导入/更新使用事务导入速度提升 10 倍

8.2 压测方案

8.2.1 压测场景

场景 a:高并发评论发布

  • 目标 qps:15,000
  • 敏感词比例:10%
  • 期望 rt:< 10ms(p95)

场景 b:敏感词命中测试

  • 目标 qps:10,000
  • 敏感词比例:100%
  • 期望拒绝率:100%

8.2.2 性能指标

性能基线:
  qps: ≥ 15,000
  响应时间:
    p50: < 5ms
    p95: < 10ms
    p99: < 20ms
  缓存命中率: ≥ 99%
  错误率: < 0.1%
  可用性: ≥ 99.9%

九、监控与告警

9.1 监控指标

@component
public class sensitivemonitor {
    
    @resource
    private meterregistry meterregistry;
    
    /**
     * 记录检测成功
     */
    public void recordchecksuccess(string scene, long costms) {
        counter.builder("sensitive.check.success")
                .tag("scene", scene)
                .register(meterregistry)
                .increment();
        
        timer.builder("sensitive.check.latency")
                .tag("scene", scene)
                .register(meterregistry)
                .record(costms, timeunit.milliseconds);
    }
    
    /**
     * 记录检测拒绝
     */
    public void recordcheckreject(string scene, long costms) {
        counter.builder("sensitive.check.reject")
                .tag("scene", scene)
                .register(meterregistry)
                .increment();
    }
    
    /**
     * 记录检测异常
     */
    public void recordcheckerror(string scene, long costms) {
        counter.builder("sensitive.check.error")
                .tag("scene", scene)
                .register(meterregistry)
                .increment();
    }
}

9.2 告警规则

指标阈值级别处理方式
检测错误率> 1%p1短信 + 电话
平均响应时间> 50msp2短信
缓存命中率< 95%p3邮件
敏感词拦截数> 1000/minp3邮件

十、安全设计

10.1 权限控制

  • 敏感词管理接口:仅限 role_admin 角色访问
  • 日志查询接口:仅限 role_auditor 角色访问
  • 配置变更:需二次确认 + 审计日志

10.2 数据安全

  • 审计日志脱敏:原始文本存储时进行加密或脱敏
  • 敏感词加密:特别敏感的词库可加密存储
  • 访问控制:redis/mysql 访问需白名单 + 密码认证

10.3 防刷机制

  • 接口限流:管理接口使用 @ratelimiter 注解
  • 异常检测:短时间大量敏感内容触发时封禁用户

十一、部署方案

11.1 配置文件

# application.yml
sensitive:
  enabled: true                    # 是否启用敏感词检测
  fallback-enabled: true           # 是否启用降级保护
  default-strategy: reject         # 默认处理策略
  cache:
    caffeine:
      expire-minutes: 10           # 本地缓存过期时间
      maximum-size: 1000           # 本地缓存最大数量
    redis:
      expire-hours: 1              # redis 缓存过期时间
  performance:
    timeout-ms: 100                # 检测超时时间
    async-log: true                # 异步写入审计日志
  match:
    fuzzy-enabled: false           # 是否启用模糊匹配
    whitelist-enabled: true        # 是否启用白名单

11.2 初始化脚本

-- 插入测试敏感词
insert into `a_sensitive_word` (`word`, `level`, `category`, `status`) values
('测试敏感词', 1, '测试', 1),
('法轮功', 3, '政治', 1),
('色情', 2, '色情', 1);

-- 插入白名单
insert into `a_sensitive_whitelist` (`word`, `status`) values
('正常词汇', 1);

十二、总结与最佳实践

12.1 核心优势

  1. 高性能:dfa + trie 树算法,o(n) 时间复杂度,支持 15,000+ qps
  2. 高可用:三级缓存 + 降级保护,可用性 ≥ 99.9%
  3. 低侵入:注解驱动,业务代码改动最小
  4. 易维护:热更新敏感词库,无需重启服务
  5. 可扩展:支持多种策略、模糊匹配、白名单等扩展能力

12.2 最佳实践

敏感词库管理

  • 定期更新敏感词库,及时响应热点事件
  • 对敏感词按等级分类,区别对待
  • 建立白名单机制,避免误杀正常内容

性能优化

  • 预热缓存,避免冷启动影响
  • 使用异步日志,避免阻塞主流程
  • 合理设置缓存过期时间,平衡命中率与实时性

监控告警

  • 建立完善的监控指标体系
  • 设置合理的告警阈值
  • 定期review审计日志,发现异常模式

安全防护

  • 敏感词管理接口严格权限控制
  • 审计日志定期归档,防止数据泄露
  • 建立防刷机制,防止恶意攻击

12.3 注意事项

  1. 降级策略:务必配置降级保护,避免因检测服务异常影响核心业务
  2. 缓存一致性:多实例部署时,通过 redis 版本号通知其他节点刷新缓存
  3. 日志存储:审计日志量大,建议按月分区或使用 es 存储
  4. 性能测试:上线前必须进行压测,验证性能指标

12.4 常见问题 faq

q1: dfa 算法的时间复杂度是多少?

a: dfa 算法的时间复杂度为 o(n),其中 n 是文本长度。无论敏感词库有多大,检测时间只与文本长度相关。

q2: 如何处理敏感词的变体(如拆分、谐音)?

a: 可以启用模糊匹配模式(matchtype = matchtype.fuzzy),在过滤器中实现:

  • 去除特殊符号后重新匹配
  • 谐音转换后匹配
  • 拼音匹配

q3: 如何保证多节点缓存一致性?

a: 通过 redis 版本号机制:

  1. 敏感词更新时,版本号+1
  2. 各节点定时检查版本号
  3. 发现版本不一致时,清空本地缓存

q4: replace 策略如何修改原始对象?

a: 需要通过反射修改原始对象的字段值,示例代码:

field field = dto.getclass().getdeclaredfield("content");
field.setaccessible(true);
field.set(dto, result.getfilteredtext());

q5: 如何防止敏感词库被恶意获取?

a:

  • 管理接口严格权限控制
  • 不提供批量导出功能
  • 测试接口加频率限制
  • 审计日志记录所有访问

以上就是springboot实现企业级敏感词拦截检查系统的设计方案的详细内容,更多关于springboot敏感词拦截检查系统的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com