前言
敏感字过滤这个事情呢,说大不大,说小不小。一般合法用户是不会出很大问题的,但是一些非法用户故意搞你事情,就不是很友好了!毕竟现在网络这么发达,啥事传到互联网上,影响都不会小!!!
今天基于DFA算法,使用AOP和自定义注解实现敏感字过滤方案!
这里注明一下:从网上找了一个DFA算法工具类,将他修改了一下,结合AOP,使用起来更加的方便了!!!
操练起来
DFA算法的JAVA工具类
说下我对这个工具类的改进:
- 他本来是一个静态的工具类,字库的路径每次需要修改代码,修改为了使用Spring注入的方式,将字库路径注入到该工具类中(具体原理可以度一下)
- 对字库的初始化,加了一些日志
- Spring扫描该bean时,加了一些条件(当开启敏感字过滤功能时,才加载该bean)
package com.zyu.boot.demo.utils.sensitiveword;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.*;
import java.util.*;
/**
* 敏感词处理工具 - DFA算法实现
*/
@Component
@ConditionalOnProperty(name = "sensitiveWord.enable", havingValue = "true")
public class SensitiveWordUtil {
//字库路径
@Value("${sensitiveWord.path}")
private String filePath;
private static String SENSITIVE_WORD_PATH;
private static Logger logger = LoggerFactory.getLogger(SensitiveWordUtil.class);
/**
* 敏感词匹配规则
*/
public static final int MinMatchTYpe = 1; //最小匹配规则,如:敏感词库["中国","中国人"],语句:"我是中国人",匹配结果:我是[中国]人
public static final int MaxMatchType = 2; //最大匹配规则,如:敏感词库["中国","中国人"],语句:"我是中国人",匹配结果:我是[中国人]
/**
* 敏感词集合
*/
@SuppressWarnings("rawtypes")
public static HashMap sensitiveWordMap;
/**
* 初始化敏感词库,构建DFA算法模型
*/
@PostConstruct
private synchronized void init(){
SENSITIVE_WORD_PATH = filePath;
Set<String> sensitiveWordSet = new HashSet<>();
// 读取指定路径下的敏感字库
try {
logger.info("正在初始化敏感字库....{}",SENSITIVE_WORD_PATH);
File wordFileDir = new File(SENSITIVE_WORD_PATH);
File[] wordFiles = wordFileDir.listFiles();
for (File wordFile : wordFiles) {
logger.info("加载{}字库",wordFile.getName());
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(wordFile),"utf-8"));
String line;
while ((line = reader.readLine()) != null) {
sensitiveWordSet.add(line);
logger.trace("加载敏感字{}",line);
}
reader.close();
}
} catch (IOException e) {
e.printStackTrace();
logger.error("初始化敏感字库失败,未加载字库....");
}
logger.info("加载{}个敏感字",sensitiveWordSet.size());
initSensitiveWordMap(sensitiveWordSet);
}
/**
* 初始化敏感词库,构建DFA算法模型
*
* @param sensitiveWordSet 敏感词库
*/
@SuppressWarnings({"rawtypes", "unchecked"})
private static void initSensitiveWordMap(Set<String> sensitiveWordSet) {
//初始化敏感词容器,减少扩容操作
sensitiveWordMap = new HashMap(sensitiveWordSet.size());
String key;
Map nowMap;
Map<String, String> newWorMap;
//迭代sensitiveWordSet
Iterator<String> iterator = sensitiveWordSet.iterator();
while (iterator.hasNext()) {
//关键字
key = iterator.next();
nowMap = sensitiveWordMap;
for (int i = 0; i < key.length(); i++) {
//转换成char型
char keyChar = key.charAt(i);
//库中获取关键字
Object wordMap = nowMap.get(keyChar);
//如果存在该key,直接赋值,用于下一个循环获取
if (wordMap != null) {
nowMap = (Map) wordMap;
} else {
//不存在则,则构建一个map,同时将isEnd设置为0,因为他不是最后一个
newWorMap = new HashMap<>();
//不是最后一个
newWorMap.put("isEnd", "0");
nowMap.put(keyChar, newWorMap);
nowMap = newWorMap;
}
if (i == key.length() - 1) {
//最后一个
nowMap.put("isEnd", "1");
}
}
}
}
/**
* 判断文字是否包含敏感字符
*
* @param txt 文字
* @param matchType 匹配规则 1:最小匹配规则,2:最大匹配规则
* @return 若包含返回true,否则返回false
*/
public static boolean contains(String txt, int matchType) {
boolean flag = false;
for (int i = 0; i < txt.length(); i++) {
int matchFlag = checkSensitiveWord(txt, i, matchType); //判断是否包含敏感字符
if (matchFlag > 0) { //大于0存在,返回true
flag = true;
}
}
return flag;
}
/**
* 判断文字是否包含敏感字符
*
* @param txt 文字
* @return 若包含返回true,否则返回false
*/
public static boolean contains(String txt) {
return contains(txt, MaxMatchType);
}
/**
* 获取文字中的敏感词
*
* @param txt 文字
* @param matchType 匹配规则 1:最小匹配规则,2:最大匹配规则
* @return
*/
public static Set<String> getSensitiveWord(String txt, int matchType) {
Set<String> sensitiveWordList = new HashSet<>();
for (int i = 0; i < txt.length(); i++) {
//判断是否包含敏感字符
int length = checkSensitiveWord(txt, i, matchType);
if (length > 0) {//存在,加入list中
sensitiveWordList.add(txt.substring(i, i + length));
i = i + length - 1;//减1的原因,是因为for会自增
}
}
return sensitiveWordList;
}
/**
* 获取文字中的敏感词
*
* @param txt 文字
* @return
*/
public static Set<String> getSensitiveWord(String txt) {
return getSensitiveWord(txt, MaxMatchType);
}
/**
* 替换敏感字字符
*
* @param txt 文本
* @param replaceChar 替换的字符,匹配的敏感词以字符逐个替换,如 语句:我爱中国人 敏感词:中国人,替换字符:*, 替换结果:我爱***
* @param matchType 敏感词匹配规则
* @return
*/
public static String replaceSensitiveWord(String txt, char replaceChar, int matchType) {
String resultTxt = txt;
//获取所有的敏感词
Set<String> set = getSensitiveWord(txt, matchType);
Iterator<String> iterator = set.iterator();
String word;
String replaceString;
while (iterator.hasNext()) {
word = iterator.next();
replaceString = getReplaceChars(replaceChar, word.length());
resultTxt = resultTxt.replaceAll(word, replaceString);
}
return resultTxt;
}
/**
* 替换敏感字字符
*
* @param txt 文本
* @param replaceChar 替换的字符,匹配的敏感词以字符逐个替换,如 语句:我爱中国人 敏感词:中国人,替换字符:*, 替换结果:我爱***
* @return
*/
public static String replaceSensitiveWord(String txt, char replaceChar) {
return replaceSensitiveWord(txt, replaceChar, MaxMatchType);
}
/**
* 替换敏感字字符
*
* @param txt 文本
* @param replaceStr 替换的字符串,匹配的敏感词以字符逐个替换,如 语句:我爱中国人 敏感词:中国人,替换字符串:[屏蔽],替换结果:我爱[屏蔽]
* @param matchType 敏感词匹配规则
* @return
*/
public static String replaceSensitiveWord(String txt, String replaceStr, int matchType) {
String resultTxt = txt;
//获取所有的敏感词
Set<String> set = getSensitiveWord(txt, matchType);
Iterator<String> iterator = set.iterator();
String word;
while (iterator.hasNext()) {
word = iterator.next();
resultTxt = resultTxt.replaceAll(word, replaceStr);
}
return resultTxt;
}
/**
* 替换敏感字字符
*
* @param txt 文本
* @param replaceStr 替换的字符串,匹配的敏感词以字符逐个替换,如 语句:我爱中国人 敏感词:中国人,替换字符串:[屏蔽],替换结果:我爱[屏蔽]
* @return
*/
public static String replaceSensitiveWord(String txt, String replaceStr) {
return replaceSensitiveWord(txt, replaceStr, MaxMatchType);
}
/**
* 获取替换字符串
*
* @param replaceChar
* @param length
* @return
*/
private static String getReplaceChars(char replaceChar, int length) {
String resultReplace = String.valueOf(replaceChar);
for (int i = 1; i < length; i++) {
resultReplace += replaceChar;
}
return resultReplace;
}
/**
* 检查文字中是否包含敏感字符,检查规则如下:<br>
*
* @param txt
* @param beginIndex
* @param matchType
* @return 如果存在,则返回敏感词字符的长度,不存在返回0
*/
@SuppressWarnings("rawtypes")
private static int checkSensitiveWord(String txt, int beginIndex, int matchType) {
//敏感词结束标识位:用于敏感词只有1位的情况
boolean flag = false;
//匹配标识数默认为0
int matchFlag = 0;
char word;
Map nowMap = sensitiveWordMap;
for (int i = beginIndex; i < txt.length(); i++) {
word = txt.charAt(i);
//获取指定key
nowMap = (Map) nowMap.get(word);
if (nowMap != null) {//存在,则判断是否为最后一个
//找到相应key,匹配标识+1
matchFlag++;
//如果为最后一个匹配规则,结束循环,返回匹配标识数
if ("1".equals(nowMap.get("isEnd"))) {
//结束标志位为true
flag = true;
//最小规则,直接返回,最大规则还需继续查找
if (MinMatchTYpe == matchType) {
break;
}
}
} else {//不存在,直接返回
break;
}
}
if (matchFlag < 2 || !flag) {//长度必须大于等于1,为词
matchFlag = 0;
}
return matchFlag;
}
// public static void main(String[] args) {
//
// System.out.println("敏感词的数量:" + SensitiveWordUtil.sensitiveWordMap.size());
// String string = "太多的伤感情怀也许只局限于饲养基地 荧幕中的情节。"
// + "然后我们的扮演的角色就是跟随着主人公的喜红客联盟 怒哀乐而过于牵强的把自己的情感也附加于银幕情节中,然后感动就流泪,"
// + "难过就躺在某一个人的怀里尽情的阐述心扉或者手机卡复制器一个贱人一杯红酒一部电影在夜 深人静的晚上,关上电话静静的发呆着。";
// System.out.println("待检测语句字数:" + string.length());
//
// //是否含有关键字
// boolean result = SensitiveWordUtil.contains(string);
// System.out.println(result);
// result = SensitiveWordUtil.contains(string, SensitiveWordUtil.MinMatchTYpe);
// System.out.println(result);
//
// //获取语句中的敏感词
// Set<String> set = SensitiveWordUtil.getSensitiveWord(string);
// System.out.println("语句中包含敏感词的个数为:" + set.size() + "。包含:" + set);
// set = SensitiveWordUtil.getSensitiveWord(string, SensitiveWordUtil.MinMatchTYpe);
// System.out.println("语句中包含敏感词的个数为:" + set.size() + "。包含:" + set);
//
// //替换语句中的敏感词
// String filterStr = SensitiveWordUtil.replaceSensitiveWord(string, '*');
// System.out.println(filterStr);
// filterStr = SensitiveWordUtil.replaceSensitiveWord(string, '*', SensitiveWordUtil.MinMatchTYpe);
// System.out.println(filterStr);
//
// String filterStr2 = SensitiveWordUtil.replaceSensitiveWord(string, "[*敏感词*]");
// System.out.println(filterStr2);
// filterStr2 = SensitiveWordUtil.replaceSensitiveWord(string, "[*敏感词*]", SensitiveWordUtil.MinMatchTYpe);
// System.out.println(filterStr2);
// }
}
一条华丽的分割线
自定义一个敏感字过滤的标志注解
也就是在方法或类上,标了该注解后,表示对该类中方法的入参进行敏感字过滤
package com.zyu.boot.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 敏感字过滤的标志注解
* 只有显示指定value为true,过滤功能才会开启。
* 方法上的注解优先生效,类上次之(若类中不是所有方法都开启过滤功能,可以在不过滤方法上加false)
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveWordFilter {
/**
* 默认不开启过滤
* @return
*/
public boolean value() default false;
}
定义一个AOP的切面
以上面的注解作为切点,主要是处理注解和敏感字替换的流程
当然,要实现AOP,要加一个aop的依赖
<!-- 集成Aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
以下是AOP的切面实现类
同样,使用@ConditionalOnProperty进行条件加载
再一条华丽的分割线
- 处理注解:方法上注解不为空,直接生效;为空时,再取类上的注解,判断为true时,执行过滤
- 敏感字替换:其实只用替换String类型和复杂类型。获取到参数的类型,如果是String,直接进行敏感字替换。如果是复杂类型,就暴力反射,获取每隔字段的类型,将String类型的字段进行敏感字替换。
package com.zyu.boot.demo.aop.sensitiveword;
import com.zyu.boot.demo.annotation.SensitiveWordFilter;
import com.zyu.boot.demo.utils.sensitiveword.SensitiveWordUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* 敏感字过滤功能的切面类
* 对标注SensitiveWordFilter注解的方法进行敏感字校验
*/
@Aspect
@Component
@ConditionalOnProperty(name = "sensitiveWord.enable", havingValue = "true")
public class SensitiveWordAspect {
@Pointcut("@within(com.zyu.boot.demo.annotation.SensitiveWordFilter) || @annotation(com.zyu.boot.demo.annotation.SensitiveWordFilter)")
public void sensitiveWordPointCut() {
}
@Around("sensitiveWordPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
boolean enableFilter = false;
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class<?> clazz = method.getDeclaringClass();
SensitiveWordFilter methodSensitiveWordFilter = method.getAnnotation(SensitiveWordFilter.class);
SensitiveWordFilter clazzSensitiveWordFilter = clazz.getAnnotation(SensitiveWordFilter.class);
if(methodSensitiveWordFilter != null){//优先取方法上的注解
enableFilter = methodSensitiveWordFilter.value();
}else{//其次取类上的注解
enableFilter = clazzSensitiveWordFilter.value();
}
Class<?>[] parameterTypes = method.getParameterTypes();
Object[] paramValues = point.getArgs();
if (enableFilter == true) {
for (int i = 0; i < paramValues.length; i++) {
Object value = paramValues[i];
if (parameterTypes[i].isAssignableFrom(String.class)) {//String类型参数直接过滤
if(null != value){
value = SensitiveWordUtil.replaceSensitiveWord((String) value, '*', SensitiveWordUtil.MinMatchTYpe);
}
} else if (!isBasicType(parameterTypes[i])) {//对象类型遍历参数,对String类型过滤
Field[] fields = value.getClass().getDeclaredFields();
for (Field field : fields) {
Class<?> type = field.getType();
if(type.isAssignableFrom(String.class)){
field.setAccessible(true);
String fieldValue = (String)field.get(value);
if(null != fieldValue){
fieldValue = SensitiveWordUtil.replaceSensitiveWord((String) fieldValue, '*', SensitiveWordUtil.MinMatchTYpe);
field.set(value,fieldValue);
}
}
}
}
paramValues[i] = value;
}
}
return point.proceed(paramValues);
}
/**
* 判断一个参数类型是否是基本类型
*
* @param clazz
* @return
*/
private boolean isBasicType(Class clazz) {
if (clazz.isAssignableFrom(Integer.class) ||
clazz.isAssignableFrom(Byte.class) ||
clazz.isAssignableFrom(Long.class) ||
clazz.isAssignableFrom(Double.class) ||
clazz.isAssignableFrom(Float.class) ||
clazz.isAssignableFrom(Character.class) ||
clazz.isAssignableFrom(Short.class) ||
clazz.isAssignableFrom(Boolean.class)) {
return true;
}
return false;
}
}
修改application.yml配置文件
#敏感字过滤的配置
sensitiveWord:
enable: true #是否开启敏感字过滤功能
path: D:\test\sensitive-words #字库的加载路径
准备字库
这里为了不让各位看官们说我很黄很暴力,就伪造一些字库吧!真实情况下,可以去网上down一些敏感字库,添加到敏感字目录里就好!
测试一下
在一个方法上标注上@SensitiveWordFilter(true)
@SensitiveWordFilter(true)
public User createUser(User user) {
System.out.println(user);
return user;
}
写个测试方法
package com.zyu.boot.demo;
import com.zyu.boot.demo.entity.User;
import com.zyu.boot.demo.service.UserService;
import com.zyu.boot.demo.utils.pwd.PasswordHash;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Date;
@SpringBootTest(classes = {DemoApplication.class})
@RunWith(SpringRunner.class)
public class UserTest {
@Autowired
private UserService userService;
@Test
public void sensitiveWordTest() throws InvalidKeySpecException, NoSuchAlgorithmException {
User user = new User();
user.setUserid("zyufocus二笔");
user.setPassword(PasswordHash.createHash("zyufocus"));
user.setName("我是铁憨憨");
user.setAge(18);
user.setGender(false);
user.setCreateDate(new Date());
user.setRole("admin");
user = userService.createUser(user);
System.out.println(user);
}
}
启动测试
- 项目启动时,扫描了敏感字库
- 测试结果
User{userid='zyufocus**', password='1000:9c785ced38921934eeee7572cd0146109efaadcf4bc8e5d65d33b648afd0e9b5:75caa0d93b8a40d7bcdf3dada9f4b732e4b5af07fca7982c0087f6e76eab1b6e9195fc86439877a3254d0c8ca726d4c43369b0d923afae0b1c5ebb091baed921', name='我是***', gender=false, age=18, createDate=Sat Jun 27 08:52:08 CST 2020, role='admin'}
User{userid='zyufocus**', password='1000:9c785ced38921934eeee7572cd0146109efaadcf4bc8e5d65d33b648afd0e9b5:75caa0d93b8a40d7bcdf3dada9f4b732e4b5af07fca7982c0087f6e76eab1b6e9195fc86439877a3254d0c8ca726d4c43369b0d923afae0b1c5ebb091baed921', name='我是***', gender=false, age=18, createDate=Sat Jun 27 08:52:08 CST 2020, role='admin'}
到这里就大功告成了!
改进方案
其实这个功能还是有改进空间的,比如敏感字的更新,移除等等
- 可以将敏感字放在redis里,每天定时去redis里更新下敏感字
结束语
学无止境,诸君共勉