一、设计背景与目标
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 设计原则
- 性能优先:采用高效的 dfa(deterministic finite automaton)算法 + trie 树数据结构
- 降级保护:检测失败时不阻塞业务,允许配置降级策略
- 缓存分层:本地缓存(caffeine)+ 分布式缓存(redis)+ 数据库(mysql)三级存储
- 异步处理:敏感内容标记审核场景使用 mq 异步通知
- 可插拔策略:支持多种处理策略,通过配置动态切换
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 | 短信 + 电话 |
| 平均响应时间 | > 50ms | p2 | 短信 |
| 缓存命中率 | < 95% | p3 | 邮件 |
| 敏感词拦截数 | > 1000/min | p3 | 邮件 |
十、安全设计
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 核心优势
- 高性能:dfa + trie 树算法,o(n) 时间复杂度,支持 15,000+ qps
- 高可用:三级缓存 + 降级保护,可用性 ≥ 99.9%
- 低侵入:注解驱动,业务代码改动最小
- 易维护:热更新敏感词库,无需重启服务
- 可扩展:支持多种策略、模糊匹配、白名单等扩展能力
12.2 最佳实践
敏感词库管理
- 定期更新敏感词库,及时响应热点事件
- 对敏感词按等级分类,区别对待
- 建立白名单机制,避免误杀正常内容
性能优化
- 预热缓存,避免冷启动影响
- 使用异步日志,避免阻塞主流程
- 合理设置缓存过期时间,平衡命中率与实时性
监控告警
- 建立完善的监控指标体系
- 设置合理的告警阈值
- 定期review审计日志,发现异常模式
安全防护
- 敏感词管理接口严格权限控制
- 审计日志定期归档,防止数据泄露
- 建立防刷机制,防止恶意攻击
12.3 注意事项
- 降级策略:务必配置降级保护,避免因检测服务异常影响核心业务
- 缓存一致性:多实例部署时,通过 redis 版本号通知其他节点刷新缓存
- 日志存储:审计日志量大,建议按月分区或使用 es 存储
- 性能测试:上线前必须进行压测,验证性能指标
12.4 常见问题 faq
q1: dfa 算法的时间复杂度是多少?
a: dfa 算法的时间复杂度为 o(n),其中 n 是文本长度。无论敏感词库有多大,检测时间只与文本长度相关。
q2: 如何处理敏感词的变体(如拆分、谐音)?
a: 可以启用模糊匹配模式(matchtype = matchtype.fuzzy),在过滤器中实现:
- 去除特殊符号后重新匹配
- 谐音转换后匹配
- 拼音匹配
q3: 如何保证多节点缓存一致性?
a: 通过 redis 版本号机制:
- 敏感词更新时,版本号+1
- 各节点定时检查版本号
- 发现版本不一致时,清空本地缓存
q4: replace 策略如何修改原始对象?
a: 需要通过反射修改原始对象的字段值,示例代码:
field field = dto.getclass().getdeclaredfield("content");
field.setaccessible(true);
field.set(dto, result.getfilteredtext());
q5: 如何防止敏感词库被恶意获取?
a:
- 管理接口严格权限控制
- 不提供批量导出功能
- 测试接口加频率限制
- 审计日志记录所有访问
以上就是springboot实现企业级敏感词拦截检查系统的设计方案的详细内容,更多关于springboot敏感词拦截检查系统的资料请关注代码网其它相关文章!
发表评论