跳到主要内容

黑名单设计

如果用户在我们的网站里说了 一些不健康的内容,那么我们应该可以对其 进行拉黑或者封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/**");
}