业务场景

  • 在系统数据库中,可能存在一些用户名称,昵称,评论中有些词汇,出于保护隐私或是不符合规范等原因,不能直接展示在前端页面上,这样的敏感词需要用 * 号代替。

    实现步骤

  1. 将系统需要替换的敏感词保存在数据库中,在项目启动后,获取敏感词库,保存到Redis缓存中。
  2. 编写敏感词序列化类 SensitiveWordSerializer 。
  3. 在需要敏感词过滤的实体类对应的字段上加上注解 @JSONField(serializeUsing = SensitiveWordSerializer.class)。
  4. 编写FastJson配置类 设置fastjson的全局序列化和反序列化的特性,使用FastJsonHttpMessageConverter替换spring boot默认实现(MappingJackson2HttpMessageConverter)作为HttpMessageConverters的首选实现。

    具体代码如下

    需要的依赖坐标
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--  fastjson 依赖  -->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    </dependency>
    <!-- redis 依赖 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    创建敏感词数据库
  • 建库语句
    1
    2
    3
    4
    5
    6
    7
    8
    DROP TABLE IF EXISTS `sys_sensitive_word`;
    CREATE TABLE `sys_sensitive_word` (
    `id` varchar(100) NOT NULL COMMENT 'id',
    `name` varchar(100) DEFAULT NULL COMMENT '敏感词文本',
    `gmt_create` datetime DEFAULT NULL,
    `gmt_modified` datetime DEFAULT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='敏感词表';
  • 插入敏感词数据
    在这里插入图片描述
  • 编写相关的敏感词实体类,service,mapper等代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Data
    @TableName("sys_sensitive_word")
    public class SensitiveWord {
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private Integer id;

    @TableField("name")
    private String name;

    @ApiModelProperty(value = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private Date gmtCreate;

    @ApiModelProperty(value = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date gmtModified;
    }
    编写敏感词初始化类
  • 敏感词初始化类,负责读出数据库中的敏感词表,封装数据结构加载将redis中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    /**
    * 敏感词库初始化并放入redis中
    */
    @Slf4j
    @Component
    public class SensitiveWordInit {

    public static final String SENSITIVE_WORD_KEY = "SENSITIVE_WORD_KEY";
    /**
    * 敏感词库
    */
    private HashMap sensitiveWordMap;
    @Autowired
    private SensitiveWordService sensitiveWordService;

    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
    this.redisTemplate = redisTemplate;
    }
    /**
    * 初始化敏感词
    *
    * @return
    */
    public Map<String,Object> initKeyWord() {
    try {
    List<SensitiveWord> sensitiveWords = sensitiveWordService.selectAll();
    // 从敏感词集合对象中取出敏感词并封装到Set集合中
    Set<String> keyWordSet = new HashSet();
    for (SensitiveWord s : sensitiveWords) {
    keyWordSet.add(s.getName().trim());
    }
    // 将敏感词库加入到HashMap中
    addSensitiveWordToHashMap(keyWordSet);
    RedisOperation redisOperation = new RedisOperation(this.redisTemplate);
    redisOperation.set(SENSITIVE_WORD_KEY,sensitiveWordMap);
    } catch (Exception e) {
    log.error("初始化敏感词失败");
    }
    return sensitiveWordMap;
    }

    public Map getSensitiveWordFromRedis()
    {
    RedisOperation redisOperation = new RedisOperation(this.redisTemplate);
    Map map = redisOperation.get(SENSITIVE_WORD_KEY);
    return map;
    }

    /**
    * 封装敏感词库
    *
    * @param keyWordSet
    */
    private void addSensitiveWordToHashMap(Set<String> keyWordSet) {
    // 初始化HashMap对象并控制容器的大小
    sensitiveWordMap = new HashMap<String,Object>(keyWordSet.size());
    // 敏感词
    String key = null;
    // 用来按照相应的格式保存敏感词库数据
    Map nowMap = null;
    // 用来辅助构建敏感词库
    Map<String, String> newWorMap ;
    // 使用一个迭代器来循环敏感词集合
    Iterator<String> iterator = keyWordSet.iterator();
    while (iterator.hasNext()) {
    key = iterator.next();
    // 等于敏感词库,HashMap对象在内存中占用的是同一个地址,所以此nowMap对象的变化,sensitiveWordMap对象也会跟着改变
    nowMap = sensitiveWordMap;
    for (int i = MagicNum.ZERO; i < key.length(); i++) {
    // 截取敏感词当中的字,在敏感词库中字为HashMap对象的Key键值
    char keyChar = key.charAt(i);
    // 判断这个字是否存在于敏感词库中
    Object wordMap = nowMap.get(keyChar);
    if (wordMap != null) {
    nowMap = (Map) wordMap;
    } else {
    newWorMap = new HashMap();
    newWorMap.put("isEnd", "0");
    nowMap.put(keyChar, newWorMap);
    nowMap = newWorMap;
    // 如果该字是当前敏感词的最后一个字,则标识为结尾字
    if (i == key.length() - MagicNum.ONE) {
    nowMap.put("isEnd", "1");
    }
    }
    }
    }}}
  • 附上上面涉及到的RedisOperation工具类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
       /**
    * 主要把一些常用的redis操作使用redisTemplate包装为redis命令名的方式
    */
    public final class RedisOperation {

    private static final String UNCHECKED = "unchecked";
    private RedisTemplate<String, Object> redisTemplate;
    public RedisOperation(RedisTemplate<String, Object> redisTemplate) {
    this.redisTemplate = redisTemplate;
    }

    /**
    * 返回与键 key 相关联的值
    */
    @SuppressWarnings(UNCHECKED)
    public <V> V get(String key) {
    return (V) redisTemplate.opsForValue().get(key);
    }

    /**
    * 删除key
    */
    public Boolean del(String key) {
    return redisTemplate.delete(key);
    }

    /**
    * 批量删除
    */
    public Long del(Collection<String> keys) {
    return redisTemplate.delete(keys);
    }

    /**
    * 返回给定的一个或多个字符串键的值
    */

    public List<Object> mget(String... keys) {
    return redisTemplate.opsForValue().multiGet(Arrays.asList(keys));
    }

    /**
    * 将value对象序列化后的字符串 关联到key<br/>
    * 如果key已经持有其他值,SET就覆写旧值,无视类型。
    */
    public void set(String key, Object value) {
    redisTemplate.opsForValue().set(key, value);
    }

    /**
    * 同时为多个键设置值<br/>
    * 如果某个给定键已经存在,那么MSET将使用新值去覆盖旧值
    */
    public void mset(Map<String, Object> map) {
    redisTemplate.opsForValue().multiSet(map);
    }

    /**
    * 同时为多个键设置值<br/>
    * 如果键key已经存在,则MSETNX命令不做任何动作
    */
    public void msetnx(Map<String, Object> map) {
    redisTemplate.opsForValue().multiSetIfAbsent(map);
    }

    /**
    * 将键key的值设置为value对象的序列化字符串,并将键key的生存时间设置为timeout<br/>
    * 如果键key已经存在,那么将覆盖已有的值
    */
    public void setex(String key, Object value, long timeout, TimeUnit unit) {
    redisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    /**
    * 只在键key不存在的情况下, 将键key的值设置为value<br/>
    * 若键key已经存在,则SETNX命令不做任何动作
    */
    public void setnx(String key, Object value) {
    redisTemplate.opsForValue().setIfAbsent(key, value);
    }

    /**
    * 只在键key不存在的情况下,将键key的值设置为value,且同时设置过期时间<br/>
    * 若键key已经存在,则SETNX命令不做任何动作。
    */
    public void setnx(String key, Object value, long timeout, TimeUnit unit) {
    redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
    }

    /**
    * 为键key储存的数字值加上一
    */
    public Long incr(String key) {
    return incr(key, 1);
    }

    /**
    * 为键key储存的数字值加上增量increment
    */
    public Long incr(String key, long increment) {
    return redisTemplate.opsForValue().increment(key, increment);
    }

    /**
    * 将键key储存的整数值减去减量decrement
    */
    public Long decr(String key, long decrement) {
    return redisTemplate.opsForValue().decrement(key, decrement);
    }

    /**
    * 为键key储存的数字减一
    */
    public Long decr(String key) {
    return decr(key, 1);
    }

    /**
    * HGET命令在默认情况下返回给定域的值<br/>
    * 如果给定域不存在于哈希表中,又或者给定的哈希表并不存在,那么命令返回 nil
    */
    @SuppressWarnings(UNCHECKED)
    public <V> V hget(String key, String field) {
    return (V) redisTemplate.opsForHash().get(key, field);
    }

    /**
    * 如果给定的域不存在于哈希表,那么返回一个nil值<br/>
    * 因为不存在的key被当作一个空哈希表来处理,所以对一个不存在的key进行HMGET操作将返回一个只带有 nil 值的表
    */
    public List<Object> hmget(String key, String... fields) {
    return redisTemplate.opsForHash().multiGet(key, new ArrayList<>(Arrays.asList(fields)));
    }

    /**
    * 返回哈希表 key 中,所有的域和值
    */
    public Map<Object, Object> hgetAll(String key) {
    return redisTemplate.opsForHash().entries(key);
    }

    /**
    * 如果给定的哈希表并不存在,那么一个新的哈希表将被创建并执行HSET操作<br/>
    * 如果域field已经存在于哈希表中, 那么它的旧值将被新值 value 覆盖。
    */
    public <V> void hset(String key, String field, V value) {
    redisTemplate.opsForHash().put(key, field, value);
    }

    /**
    * 同时将多个field-value(域-值)对设置到哈希表key中<br/>
    * 此命令会覆盖哈希表中已存在的域<br/>
    * 如果 key 不存在,一个空哈希表被创建并执行 HMSET 操作。
    */
    public void hmset(String key, Map<String, ?> data) {
    redisTemplate.opsForHash().putAll(key, data);
    }

    /**
    * 检查给定field是否存在于哈希表hash当中
    */
    public Boolean hexists(String key, String field) {
    return redisTemplate.opsForHash().hasKey(key, field);
    }

    /**
    * 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略
    */
    public Long hdel(String key, Object... fields) {
    return redisTemplate.opsForHash().delete(key, fields);
    }

    /**
    * 将一个或多个值 value 插入到列表 key 的表尾(最右边)<br/>
    * 如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作<br />
    * 当 key 存在但不是列表类型时,返回一个错误
    */
    public Long rpush(String key, Object... values) {
    return redisTemplate.opsForList().rightPushAll(key, values);
    }

    /**
    * 将一个或多个值 value 插入到列表 key 的表头<br />
    * 如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作<br />
    * 当 key 存在但不是列表类型时,返回一个错误
    */
    public Long lpush(String key, Object... values) {
    return redisTemplate.opsForList().leftPushAll(key, values);
    }

    /**
    * 移除并返回列表 key 的头元素
    */
    @SuppressWarnings(UNCHECKED)
    public <V> V lpop(String key) {
    return (V) redisTemplate.opsForList().leftPop(key);
    }

    /**
    * 移除并返回列表 key 的尾元素
    */
    @SuppressWarnings(UNCHECKED)
    public <V> V rpop(String key) {
    return (V) redisTemplate.opsForList().rightPop(key);
    }

    /**
    * 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略
    */
    @SuppressWarnings(UNCHECKED)
    public <V> Long sadd(String key, V... members) {
    return redisTemplate.opsForSet().add(key, members);
    }

    /**
    * 判断 member 元素是否集合 key 的成员
    */
    public Boolean sismember(String key, Object member) {
    return redisTemplate.opsForSet().isMember(key, member);
    }

    /**
    * 返回集合中的一个随机元素
    */
    @SuppressWarnings(UNCHECKED)
    public <V> V srandmember(String key) {
    return (V) redisTemplate.opsForSet().randomMember(key);
    }

    /**
    * 返回集合中的随机元素。<p/>
    * 如果 count 为正数,且小于集合基数,那么命令返回一个包含 count 个元素的数组,数组中的元素各不相同。如果 count 大于等于集合基数,那么返回整个集合。<p/>
    * 如果 count 为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为 count 的绝对值。
    */

    public List<Object> srandmembers(String key, long count) {
    return redisTemplate.opsForSet().randomMembers(key, count);
    }

    /**
    * 返回集合 key 中的所有成员。<br />
    * 不存在的 key 被视为空集合
    */
    public Set<Object> smembers(String key) {
    return redisTemplate.opsForSet().members(key);
    }

    /**
    * 返回集合key的基数(集合中元素的数量)
    */
    public Long scard(String key) {
    return redisTemplate.opsForSet().size(key);
    }

    /**
    * 移除集合key中的一个或多个member元素,不存在的member元素会被忽略。<br />
    * 当key不是集合类型,返回一个错误
    */
    public Long srem(String key, Object... values) {
    return redisTemplate.opsForSet().remove(key, values);
    }

    /**
    * 将一个元素及其score值加入到有序集key当中
    */
    public <V> Boolean zadd(String key, V value, double score) {
    return redisTemplate.opsForZSet().add(key, value, score);
    }

    /**
    * 返回有序集key中,成员member的score值
    */
    public Double zscore(String key, Object member) {
    return redisTemplate.opsForZSet().score(key, member);
    }

    /**
    * 为有序集key的成员member的score值加上增量increment
    */
    public Double zincrby(String key, Object member, double increment) {
    return redisTemplate.opsForZSet().incrementScore(key, member, increment);
    }

    /**
    * 返回有序集key中,指定区间内的成员
    */
    public Set<Object> zrange(String key, long start, long end) {
    return redisTemplate.opsForZSet().range(key, start, end);
    }

    /**
    * 返回有序集key中,指定区间内的成员。且带有score
    */
    public Set<ZSetOperations.TypedTuple<Object>> zrangewithscore(String key, long start, long end) {
    return redisTemplate.opsForZSet().rangeWithScores(key, start, end);
    }

    /**
    * 返回有序集key中,所有score值介于min和max之间(包括等于min或max)的成员。<br />
    * 有序集成员按score值递增(从小到大)次序排列
    */
    public Set<Object> zrangebyscore(String key, double min, double max) {
    return redisTemplate.opsForZSet().rangeByScore(key, min, max);
    }

    /**
    * 返回有序集key中,指定区间内的成员<br />
    * 成员的位置按score值递减(从大到小)来排列
    */
    public Set<Object> zrevrange(String key, long start, long end) {
    return redisTemplate.opsForZSet().reverseRange(key, start, end);
    }

    /**
    * 返回有序集key中,指定区间内的成员。且带有score
    */
    public Set<ZSetOperations.TypedTuple<Object>> zrevrangewithscores(String key, long start, long end) {
    return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
    }

    /**
    * 返回有序集key中,所有score值介于min和max之间(包括等于min或max)的成员。<br />
    * 有序集成员按score值递增(从大到小)次序排列
    */
    public Set<Object> zrevrangebyscore(String key, double min, double max) {
    return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max);
    }

    /**
    * 移除有序集key中,所有score值介于min和max之间(包括等于min或max)的成员
    */
    public Long zremrangebyscore(String key, double min, double max) {
    return redisTemplate.opsForZSet().removeRangeByScore(key, min, max);
    }

    /**
    * 移除有序集key中,指定排名(rank)区间内的所有成员。
    */
    public Long zremrangebyrank(String key, long start, long end) {
    return redisTemplate.opsForZSet().removeRange(key, start, end);
    }

    /**
    * 移除有序集key中的一个或多个成员,不存在的成员将被忽略
    */
    public Long zrem(String key, Object... values) {
    return redisTemplate.opsForZSet().remove(key, values);
    }

    /**
    * 获取元素value在有序集合中的位置排名
    */
    public Long zrank(String key, Object value) {
    return redisTemplate.opsForZSet().reverseRank(key, value);
    }
    /**
    * 当key存在且是有序集类型时,返回有序集的基数。当key不存在时,返回0
    */
    public Long zcard(String key) {
    return redisTemplate.opsForZSet().size(key);
    }

    /**
    * 返回有序集key中,score值在min和max之间(默认包括score值等于min或max)的成员的数量
    */
    public Long zcount(String key, double min, double max) {
    return redisTemplate.opsForZSet().count(key, min, max);
    }


    /**
    * 设置过期时间
    *
    * @param timeout 过期时间,单位毫秒
    */
    public Boolean expire(String key, long timeout) {
    return redisTemplate.expire(key, timeout, TimeUnit.MILLISECONDS);
    }

    /**
    * 获取key的存活时长,单位毫秒
    */
    public Long ttl(String key) {
    return redisTemplate.getExpire(key, TimeUnit.MILLISECONDS);
    }

    /**
    * 判断key是否存在
    */
    public Boolean exist(String key) {
    return redisTemplate.hasKey(key);
    }

    /**
    * 获取底层redisTemplate
    */
    public RedisTemplate<String, Object> getRedisTemplate() {
    return redisTemplate;
    }
    }
    编写敏感词序列化类
  • 敏感词序列化类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
     /**
    * 敏感词过滤工具类
    * 使用方法 @JSONField(serializeUsing = SensitiveWordSerializer.class)
    */
    @Component
    public class SensitiveWordSerializer implements ObjectSerializer {

    /**
    * 敏感词库
    */
    private static Map<String,Object> sensitiveWordMap = null;

    private static SensitiveWordInit wordInit;

    @Autowired
    public void setRedisTemplate(SensitiveWordInit sensitiveWordInit) {
    SensitiveWordSerializer.wordInit = sensitiveWordInit;
    }
    @PostConstruct
    public void initSensitiveWord() {
    sensitiveWordMap = SensitiveWordSerializer.wordInit.initKeyWord();
    }

    /**
    * 获取敏感词内容
    *
    * @param txt
    * @param matchType
    * @return 敏感词内容
    */
    public Set<String> getSensitiveWord(String txt, int matchType) {
    Set<String> sensitiveWordList = new HashSet();
    for (int i = MagicNum.ZERO; i < txt.length(); i++) {
    int length = checkSensitiveWord(txt, i, matchType);
    if (length > MagicNum.ZERO) {
    // 将检测出的敏感词保存到集合中
    sensitiveWordList.add(txt.substring(i, i + length));
    i = i + length - MagicNum.ONE;
    }
    }
    return sensitiveWordList;
    }
    /**
    * 替换敏感词
    *
    * @param txt
    * @param matchType
    * @param replaceChar
    * @return
    */
    public String replaceSensitiveWord(String txt, int matchType, String replaceChar) {
    sensitiveWordMap = wordInit.getSensitiveWordFromRedis();
    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 replaceChar
    * @param length
    * @return
    */
    private String getReplaceChars(String replaceChar, int length) {
    StringBuilder resultReplace = new StringBuilder(replaceChar);
    for (int i = MagicNum.ONE; i < length; i++) {
    resultReplace.append(replaceChar);
    }

    return resultReplace.toString();
    }

    /**
    * 检查敏感词数量
    *
    * @param txt
    * @param beginIndex
    * @param matchType
    * @return
    */
    public int checkSensitiveWord(String txt, int beginIndex, int matchType) {
    boolean flag = false;
    // 记录敏感词数量
    int matchFlag = MagicNum.ZERO;
    String word ;
    Map<String,Object> nowMap = sensitiveWordMap;
    for (int i = beginIndex; i < txt.length(); i++) {
    word = String.valueOf(txt.charAt(i));
    // 判断该字是否存在于敏感词库中
    nowMap = (Map<String,Object>) nowMap.get(word);
    if (nowMap != null) {
    matchFlag++;
    // 判断是否是敏感词的结尾字,如果是结尾字则判断是否继续检测
    if ("1".equals(nowMap.get("isEnd"))) {
    flag = true;
    // 判断过滤类型,如果是小过滤则跳出循环,否则继续循环
    }
    } else {
    break;
    }
    }
    if (!flag) {
    matchFlag = 0;
    }
    return matchFlag;
    }

    @Override
    public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
    String value = replaceSensitiveWord((String) object, MagicNum.TWO, "*");
    serializer.write(value);
    }
    }
    在需要敏感词过滤的实体上加上注解
  • 加上注解 @JSONField(serializeUsing = SensitiveWordSerializer.class)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
     @Data
    @EqualsAndHashCode(callSuper = false)
    @Accessors(chain = true)
    @TableName("sys_user")
    @ApiModel(description = "用户实体类")
    public class UserDTO {

    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;

    @TableField(value = "username")
    // username需要敏感词过滤
    @JSONField(serializeUsing = SensitiveWordSerializer.class)
    private String username;

    //......
    }
    Fastjson配置类
  • 设置fastjson的全局序列化和反序列化的特性,使用FastJsonHttpMessageConverter替换spring boot默认实现(MappingJackson2HttpMessageConverter)作为HttpMessageConverters首选实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    /**
    * Fastjson全局序列化配置
    *
    */
    @Configuration
    @Slf4j
    public class FastJsonConfig {
    private static final Map<SerializerFeature, String> SERIALIZER_FEATURES = new EnumMap<>(SerializerFeature.class);
    private static final Map<Feature, String> PARSER_FEATURES = new EnumMap<>(Feature.class);

    static {
    SERIALIZER_FEATURES.put(SerializerFeature.WriteMapNullValue, "WriteMapNullValue:输出值为null的字段");
    SERIALIZER_FEATURES.put(SerializerFeature.WriteDateUseDateFormat, "WriteDateUseDateFormat:根据全局日期格式格式化");
    SERIALIZER_FEATURES.put(SerializerFeature.WriteBigDecimalAsPlain, "WriteBigDecimalAsPlain:大数序列化为文本");
    SERIALIZER_FEATURES.put(SerializerFeature.DisableCircularReferenceDetect, "DisableCircularReferenceDetect:关闭循环引用发现");
    PARSER_FEATURES.put(Feature.AllowISO8601DateFormat, "AllowISO8601DateFormat:支持ISO8601格式的日期");
    PARSER_FEATURES.put(Feature.DisableCircularReferenceDetect, "DisableCircularReferenceDetect:关闭循环引用发现");

    log.debug("全局启用Fastjson下列序列化选项");
    for (Map.Entry<SerializerFeature, String> entry : SERIALIZER_FEATURES.entrySet()) {
    JSON.DEFAULT_GENERATE_FEATURE |= entry.getKey().getMask();
    log.debug(entry.getValue());
    }
    log.debug("全局启用Fastjson下列反序列化选项");
    for (Map.Entry<Feature, String> entry : PARSER_FEATURES.entrySet()) {
    JSON.DEFAULT_PARSER_FEATURE |= entry.getKey().getMask();
    log.debug(entry.getValue());
    }
    }

    @Bean
    public HttpMessageConverters fastJsonHttpMessageConverters() {
    log.debug("启用FastJsonHttpMessageConverter,将其添加至同类HttpMessageConverter之前");
    return new HttpMessageConverters(new FastJsonHttpMessageConverter());
    }
    }

    执行效果

  • 数据库中有一条用户数据,用户昵称为”笨蛋芝麻馅笨蛋“,”笨蛋“是我们的敏感词,启动项目,调用分页查询的接口,返回的用户昵称敏感词部分被*号代替:
  • 数据库情况
    在这里插入图片描述
  • 调用接口查找用户信息结果,发现用户昵称中的敏感词成功被*号所代替。
    在这里插入图片描述