Skip to main content

黑名单设计-撤回拉黑


黑名单设计

如果用户在我们的网站里说了 一些不健康的内容,那么我们应该可以对其 进行拉黑或者封IP的操作。

数据库设计

CREATE TABLE `black`  (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'id',
  `type` int(11) NOT NULL COMMENT '拉黑目标类型 1.ip 2uid',
  `target` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '拉黑目标',
  `create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  `update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `idx_type_target`(`type`, `target`) USING BTREE
) COMMENT = '黑名单' ROW_FORMAT = Dynamic;
CREATE TABLE `role` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名称',
  `create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  `update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_create_time` (`create_time`) USING BTREE,
  KEY `idx_update_time` (`update_time`) USING BTREE
) COMMENT='角色表';
CREATE TABLE `user_role` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
  `uid` bigint(20) NOT NULL COMMENT 'uid',
  `role_id` bigint(20) NOT NULL COMMENT '角色id',
  `create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  `update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_uid` (`uid`) USING BTREE,
  KEY `idx_role_id` (`role_id`) USING BTREE,
  KEY `idx_create_time` (`create_time`) USING BTREE,
  KEY `idx_update_time` (`update_time`) USING BTREE
) COMMENT='用户角色关系表';
insert into role(id,`name`) values(1,'超级管理员');
insert into role(id,`name`) values(2,'群聊管理员');

图如下:

image-20240519195707675

一个用户可以有多个角色,一个角色也可以被多个用户拥有

user_role就相当于中间表

拉黑设计

拉黑主要体现在业务逻辑方面,没有什么特别难得地方,就是常规的增删改查

拉黑接口

UserController.java:

@PostMapping("/black")
@ApiOperation("拉黑用户")
public ApiResult<Void> black(@Valid @RequestBody BlackReq req) {
    Long uid = RequestHolder.get().getUid();
    boolean hasPower = userRoleService.hasPower(uid, RoleEnum.ADMIN);
    AssertUtil.isTrue(hasPower, "您没有权限拉黑用户");
    userService.black(req);
    return ApiResult.success();
}

权限缓存

UserCache.java

@Component
public class UserCache {// todo 多级缓存

    @Autowired
    @Lazy
    private UserRoleService userRoleService;

    @Cacheable(cacheNames = "user", key = "'roles:'+#uid")
    public Set<Long> getRoleSet(Long uid) {
        List<UserRole> userRoles = userRoleService.listByUid(uid);
        return userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toSet());
    }

}

权限(角色)判断

@Override
public boolean hasPower(Long uid, RoleEnum roleEnum) {
    Set<Long> roleSet = userCache.getRoleSet(uid);
    return (isAdmin(roleSet)) || roleSet.contains(roleEnum.getId());
}

private boolean isAdmin(Set<Long> roleSet) {
    return roleSet.contains(RoleEnum.ADMIN.getId());
}

角色枚举RoleEnum.java:

@AllArgsConstructor
@Getter
public enum RoleEnum {
    ADMIN(1L, "超级管理员"),
    CHAT_MANAGER(2L, "云群聊管理"),
    ;

    private final Long id;
    private final String desc;

    private static Map<Long, RoleEnum> cache;

    static {
        cache = Arrays.stream(RoleEnum.values()).collect(Collectors.toMap(RoleEnum::getId, Function.identity()));
    }

    public static RoleEnum of(Long type) {
        return cache.get(type);
    }
}

拉黑具体逻辑

这边主要是先去拉黑用户的ID

然后再去拉黑用户的IP,有两个IP,创建IP,和更新IP

还有一个监听事件,推送给前端,让其他人把这个人(IP)的消息删了。

@Override
@Transactional(rollbackFor = Exception.class)
public void black(BlackReq req) {
    Long uid = req.getUid();
    Black black = new Black();
    black.setType(BlackTypeEnum.UID.getType());
    black.setTarget(uid.toString());
    blackService.save(black);
    User user = this.getById(uid);
    blackIp(Optional.ofNullable(user.getIpInfo()).map(IpInfo::getCreateIp).orElse(null));
    blackIp(Optional.ofNullable(user.getIpInfo()).map(IpInfo::getUpdateIp).orElse(null));
    applicationEventPublisher.publishEvent(new UserBlackEvent(this, user));
}

监听拉黑事件

拉黑用户后,要推送给前端所有在线的用户,删了这个拉黑人的聊天记录

@Component
public class UserBlackListener {

    @Resource
    private UserService userService;

    @Resource
    private WebSocketService webSocketService;

    @Async
    @TransactionalEventListener(classes = UserBlackEvent.class,
            phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
    public void sendMsg(UserBlackEvent event) {
        User user = event.getUser();
        webSocketService.sendMsgToAll(WSAdapter.buildBlack(user));
    }

    @Async
    @TransactionalEventListener(classes = UserBlackEvent.class,
            phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
    public void changeUserStatus(UserBlackEvent event) {
        userService.invalidUid(event.getUser().getId());
    }

}

线程池推送

使用线程池推送,人数较多,可以设置的大一点,但是如果过多了,那么队列满了之后的就不要了

@Bean(YUNFEICHAT_EXECUTOR)
@Primary
public ThreadPoolTaskExecutor websocketChatExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setWaitForTasksToCompleteOnShutdown(true);// 等待任务执行完成后关闭线程池 优雅关闭
    executor.setCorePoolSize(16);
    executor.setMaxPoolSize(16);
    executor.setQueueCapacity(1000);
    executor.setThreadNamePrefix("websocket-executor-");
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());// 满了调用线程执行,认为重要任务
    executor.setThreadFactory(new MyThreadFactory(executor));
    executor.initialize();
    return executor;

具体推送的代码



@Override
public void sendMsgToAll(WSBaseResp<?> msg) {
    ONLINE_WS_MAP.forEach(((channel, wsChannelExtraDTO) -> {
        threadPoolTaskExecutor.execute(() -> {
            sendMsg(channel, msg);
        });
    }));
}

黑名单拦截

黑名单缓存

将所有的黑名单进行缓存

@Cacheable(cacheNames = "user", key = "'blackList'")
public Map<Integer, Set<String>> getBlackMap() {
    Map<Integer, List<Black>> collect = blackService.list().stream().collect(Collectors.groupingBy(Black::getType));
    Map<Integer, Set<String>> result = new HashMap<>(collect.size());
    collect.forEach((k, v) -> {
        result.put(k, v.stream().map(Black::getTarget).collect(Collectors.toSet()));
    });
    return result;
}

@CacheEvict(cacheNames = "user", key = "'blackList'")
public Map<Integer, Set<String>> clearBlackList() {
    return new HashMap<>();
}

清除缓存

在监听器里面清除缓存,这个就很好的体现了监听的好处,事件解耦合

@Async
@TransactionalEventListener(classes = UserBlackEvent.class,
        phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
public void evictCache(UserBlackEvent event) {
    userCache.clearBlackList();
}

登录拦截

登录之前拦截黑名单用户,包括IP和用户ID

public class BlackInterceptor implements HandlerInterceptor {

    @Resource
    private UserCache userCache;


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Map<Integer, Set<String>> blackMap = userCache.getBlackMap();
        RequestInfo requestInfo = RequestHolder.get();
        if (inBlackList((requestInfo.getUid()), blackMap.get(BlackTypeEnum.UID.getType()))) {
            HttpErrorEnum.ACCESS_DENIED.sendHttpError(response);
            return false;
        }
        if (inBlackList((requestInfo.getUid()), blackMap.get(BlackTypeEnum.IP.getType()))) {
            HttpErrorEnum.ACCESS_DENIED.sendHttpError(response);
            return false;
        }
        return true;
    }

    private boolean inBlackList(Object target, Set<String> set) {
        if (Objects.isNull(target) || CollectionUtil.isEmpty(set)) {
            return false;
        }
        return set.contains(target.toString());
    }
}

拦截器配置:

注意要把上面的拦截器加入配置:

@Resource
private BlackInterceptor blackInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(tokenInterceptor)
            .addPathPatterns("/capi/**");
    registry.addInterceptor(collectorInterceptor)
            .addPathPatterns("/capi/**");
    registry.addInterceptor(blackInterceptor)
            .addPathPatterns("/capi/**");
}