主要参考了他人实现的DFA算法,他里面对短词没有处理,自己改了一下,并提供AOP实现拦截(该部分有点粗糙)
参考:DFA算法-简易Java敏感词过滤(原文提供了代码和敏感词库)
具体算法可以看看原文哈,下面讲下改进的方法。主要就是针对原文作者提到的较短的敏感词会被长的敏感词覆盖这个问题。
方法也是作者提到的,增加一个结束标志,代码就是isEnd,默认是设置为false
public class Word implements Comparable<Word>{
public char c;
public List next = null;
// 处理短字符
public boolean isEnd = false;
public Word(char c){
this.c = c;
}
@Override
public int compareTo(Word word) {
return c - word.c;
} // 返回两者差值
public String toString(){
return c + "(" + (next == null ? null : next.size()) + ")";
}
}
具体就是在加载敏感词构建树结构的过程中,做一个结束标记的判断,需要注意的是,如果已经存在树分支,应该是更新标记,而不是新建一个树分支。
主要改动就在后面短词标记部分
/**
* 加载敏感词列表
* @param words 敏感词数组
*/
public void loadWord(ArrayList<String> words){
if(words == null) return;
char[] chars;
List now;
Word word;
wordList = new List();
for(String __word__:words){
if(__word__ == null) continue;
chars = __word__.toCharArray();
now = wordList;
word = null;
for(char c:chars){
if(word != null) {
if(word.next == null) word.next = new List();
now = word.next;
}
word = now.get(c);
//if(word == null) word = now.add(c);
if(word == null){
word = now.add(c);
// 短词标记
if(c==chars[chars.length-1]){
word.isEnd = true;
}
} else{
// 当前有词则不添加新词,修改标志
if(c==chars[chars.length-1]){
word.isEnd = true;
}
}
}
}
// 这个排序有问题
sort(wordList);
}
敏感词检测过程,增加了如果是isEnd,就及时存放位置lastJ,并修改是否有敏感词标志,然后继续匹配。
如果没有更长的敏感词,就按照短的敏感词进行替换。
/**
* 敏感词替换
* @param text 待替换文本
* @return 替换后的文本
*/
public static String Filter(String text){
//public static String Filter(String text, final int step){
if(wordList == null || wordList.size() == 0) return text;
char[] __char__ = text.toCharArray(); // 把String转化成char数组,便于遍历
int i,j,lastJ;
Word word;
boolean flag; // 是否需要替换
int count=0; // 统计跳过字符数
for(i=0;i<__char__.length;i++){ // 遍历所有字符
char c = __char__[i];
word = wordList.binaryGet(c); // 使用二分查找来寻找字符,提高效率
if(word != null && word.c != '的'){ // word != null说明找到了,对的临时处理
flag = false;
j = i+1;
lastJ = j;
while (j < __char__.length){ // 开始逐个比较后面的字符
if(skip(__char__[j])) { // 跳过空格之类的无关字符
j++;
continue;
}
if(word.next != null){ // 字符串尚未结束,不确定是否存在敏感词
/*
以下代码并没有使用二分查找,因为以同一个字符开头的敏感词较少
例如,wordList中记录了所有敏感词的开头第一个字,它的数量通常会有上千个
假如现在锁定了字符“T”开头的敏感词,而“T”开头的敏感词只有10个,这时使用二分查找的效率反而低于顺序查找
*/
word = word.next.get(__char__[j]);
if(word == null){
break;
}
j++;
// 短字符
if(word.isEnd){
flag = true;
lastJ = j;
}
}else { // 字符串已结束,存在敏感词汇
flag = true;
break;
}
}
if(word != null && word.next == null){
flag = true;
lastJ = j;
}
if(flag){ // 如果flag==true,说明检测出敏感粗,需要替换
while (i<lastJ){
if(skip(__char__[i])){ // 跳过空格之类的无关字符,如果要把空格也替换成'*',则删除这个if语句
i++;
continue;
}
__char__[i] = replace;
i++;
}
i--;
}
}
}
return new String(__char__);
}
本文作者,只提供了替换及跳过符号,具体应该将检测敏感词位置单独提取一个方法,方便替换或者提示之类,并增加skip,还有黑名单和白名单等,可以更加丰富。具体可以看看这份代码wordfilter,有介绍。
不过具体测试,前者效率更高效,可能是由于查询和排序提高了效率,没有具体去看。
另外写个初始化的方法继承CommandLineRunner接口,对敏感词库进行初始化。
@Slf4j
@Component
public class InitDataRunner implements CommandLineRunner {
@Resource
private SensitiveWordFilterService sensitiveWordFilterService;
@Resource
private DictDataService dictDataService;
@Override
public void run(String... args) throws Exception {
sensitiveWordFilterService.loadWordFromDataBase();
}
使用AOP进行敏感词过滤,整套可以参考Springboot敏感词过滤,没有复现过,看起来感觉是可靠的。
ProceedingJoinPoint 和JoinPoint在这里貌似没有区别,我没有具体去看。这里一定要在Around里面操作,不然拿不到参数。这里是通过Field反射的方法,参考的是这篇文章敏感字过滤:AOP+注解+DFA算法,将整个参数遍历并且过滤了,想要拿到具体想要的参数感觉不太行。
如果只想过滤具体参数,有两种方法,一个是使用过滤器,另一种是在get获取之后,在service层再去过滤,我是使用第二种方法。
另外建议采用自定义注解来实现拦截,只要在需要的接口做拦截就好了。
@Pointcut(“@annotation(com.xxx.ops.common.annotation.Sensitive)”)
@Slf4j
@Aspect
@Component
public class SensitiveAspect {
private static final Log Logger = LogFactory.getLog(com.xxx.ops.forum.aop.SensitiveAspect.class);
@Resource
SensitiveWordFilterService sensitiveWordFilterService;
// 拦截controller所有方法
//@Pointcut("execution(public * com.xxx.ops.forum.controller..*.*(..))")
// 只拦截post和put请求
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)||@annotation(org.springframework.web.bind.annotation.PutMapping)")
//@Pointcut("@annotation(com.xxx.ops.common.annotation.Sensitive)") //自定义注解
public void params() {
}
@Around("params()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 加载词库
//sensitiveWordFilterService.loadWordFromFile("E:/project/wordfilter/SensitiveWord/SensitiveWordList.txt");
//sensitiveWordFilterService.loadWordFromDataBase();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
//获取请求参数以及类型
Object[] args = point.getArgs();
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class<?>[] paramTypes = method.getParameterTypes();
// 遍历args
for (int i = 0; i < args.length; i++) {
Object value = args[i];
Logger.info("输入参数为-[" + args[i] + "]");
//String类型参数直接过滤
if (paramTypes[i].isAssignableFrom(String.class)) {
if (null != value) {
value = args[i];
value = SensitiveWordFilterService.Filter((String) value);
}
} else { //对象类型遍历参数,对String类型过滤
Field[] fields = value.getClass().getDeclaredFields(); // 通过反射获取对象
for (Field field : fields) {
Class<?> type = field.getType();
if(type.isAssignableFrom(String.class)){ // 如果是String则过滤
field.setAccessible(true);
String fieldValue = (String)field.get(value);
if(null != fieldValue){
fieldValue = SensitiveWordFilterService.Filter(fieldValue);;
field.set(value,fieldValue);
}
}
}
}
args[i] = value;
}
//if(result.length()>=1){
// //自定义的异常
// Logger.info("敏感词是-" + result);
// //throw new BizException("500","您输入的内容有敏感词");
//}
Logger.info("当前调用接口-[" + request.getRequestURL() + "]");
return point.proceed(args);
}
//@AfterReturning(returning = "ret", pointcut = "params()")
//public void doAfterReturning(Object ret) {
//}
}
自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Sensitive {
}
使用3的话,会在输入就对参数做拦截,参数被屏蔽了就不好复原;且可能会屏蔽不相关的参数,例如url路径,毕竟只是通过类型做判断。
改进的方法:
@Around("params()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 加载词库
//sensitiveWordFilterService.loadWordFromFile("E:/project/wordfilter/SensitiveWord/SensitiveWordList.txt");
//sensitiveWordFilterService.loadWordFromDataBase();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
//获取请求参数以及类型
Object[] args = point.getArgs();
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class<?>[] paramTypes = method.getParameterTypes();
// 遍历args
List<String> sensitiveWord = new ArrayList<>(); // 记录被替换敏感词
String userId = "";
for (int i = 0; i < args.length; i++) {
Object value = args[i];
LOGGER.info("输入参数为-[" + args[i] + "]");
Field[] fields = value.getClass().getDeclaredFields(); // 通过反射获取对象
for (Field field : fields) {
if(Arrays.asList(types).contains(field.getName())){ // 如果是存在包含类型则过滤
field.setAccessible(true);
String fieldValue = (String)field.get(value);
//System.out.println(fieldValue);
if(null != fieldValue){
List<String> tempList = SensitiveWordFilterService.FilterMap(fieldValue);
if(tempList.size()>0)
sensitiveWord.add(field.getName()+':'+tempList.toString());
//field.set(value,result.get("text"));
field.set(value,fieldValue); // 不替换敏感词
}
}
}
args[i] = value;
}
if(sensitiveWord.size()>0){
SensitiveMark sensitiveMark = new SensitiveMark();
sensitiveMark.setSensitiveWord(sensitiveWord.toString());
sensitiveMarkService.saveOrUpdate(sensitiveMark);
System.out.println(sensitiveWord.toString());
LOGGER.info("拦截并屏蔽的敏感词-[" + sensitiveWord.toString() + "]");
}
//if(result.length()>=1){
// //自定义的异常
// Logger.info("敏感词是-" + result);
// //throw new BizException("500","您输入的内容有敏感词");
//}
LOGGER.info("当前调用接口-[" + request.getRequestURL() + "]");
return point.proceed(args);
}
使用@ControllerAdvice这个注解来处理,判断接口返回的dto类型是哪个,从来实现对返回值内容进行敏感词屏蔽。我这里主要是帖子和收藏等列表、帖子详情内容及消息通知内容做屏蔽。
@ControllerAdvice("com.xxx.ops")
public class AppResponseAdvice implements ResponseBodyAdvice<Object> {
@Resource
SensitiveWordFilterService sensitiveWordFilterService;
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
/**
* @Description: 该方法是拦截到返回值(即response中的数据),然后操作返回值,并返回
*
**/
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
ServletServerHttpResponse responseTemp = (ServletServerHttpResponse) serverHttpResponse;
HttpServletResponse response = responseTemp.getServletResponse();
// 处理帖子列表和我的帖子,我的收藏
if (body instanceof ContentResultDto) {
ContentResultDto result = (ContentResultDto) body;
List<ContentDto> data = result.getContentDto();
if(data!=null) {
for (ContentDto contentDto : data) {
if (StringUtils.isNotBlank(contentDto.getTitle())) {
contentDto.setTitle(sensitiveWordFilterService.Filter(contentDto.getTitle()));
}
if (StringUtils.isNotBlank(contentDto.getSubtitle())) {
contentDto.setSubtitle(sensitiveWordFilterService.Filter(contentDto.getSubtitle()));
}
}
}
}
// 处理帖子详情
if (body instanceof DetailDto) {
DetailDto result = (DetailDto) body;
List<DetailDto.Comment> data = result.getComments();
if (StringUtils.isNotBlank(result.getTitle())) {
result.setTitle(sensitiveWordFilterService.Filter(result.getTitle()));
}
if (StringUtils.isNotBlank(result.getSubtitle())) {
result.setSubtitle(sensitiveWordFilterService.Filter(result.getSubtitle()));
}
if (StringUtils.isNotBlank(result.getContent())) {
result.setContent(sensitiveWordFilterService.Filter(result.getContent()));
}
if(data!=null) {
for (DetailDto.Comment comment : data) {
if (StringUtils.isNotBlank(comment.getContent())) {
comment.setContent(sensitiveWordFilterService.Filter(comment.getContent()));
}
}
}
}
// 处理消息通知
if (body instanceof Map) {
List<MessageDto> data = (List) ((Map) body).get("data");
if(data!=null) {
for (MessageDto messageDto : data) {
if (StringUtils.isNotBlank(messageDto.getMessageInfo())) {
messageDto.setMessageInfo(sensitiveWordFilterService.Filter(messageDto.getMessageInfo()));
}
}
}
}
return body;
}
}