You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Spring WebSocket:如何拒绝群组消息同时向指定用户的错误队列发送提示

Spring WebSocket:如何拒绝群组消息同时向指定用户的错误队列发送提示

嘿,我来帮你理清楚这个问题的解决方案!从你的日志和代码来看,你已经走对了大部分路,核心需求其实已经实现了,我来给你拆解清楚:

先看你的当前逻辑:已经满足需求!

看你的日志输出:

2025-07-13T10:02:47.761Z DEBUG 1 --- [chat-service] [nio-8082-exec-2] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/errors/11 session=null payload=You are not a member of group!
2025-07-13T10:02:47.764Z DEBUG 1 --- [chat-service] [nio-8082-exec-2] o.s.m.s.ExecutorSubscribableChannel : WebSocketAuthChannelInterceptor returned null from preSend, i.e. precluding the send.

这说明两个核心目标都达成了:

  1. 错误提示成功发送到了用户的专属错误队列/queue/errors/11
  2. 原群组消息被成功拦截(返回null阻止了发送)

你可能疑惑为什么返回null不影响错误消息的发送?因为你用SimpMessagingTemplate发送的错误消息是一个完全独立的消息流程——它直接把消息推送给broker,不走当前被拦截的这条消息的preSend校验链路,所以两者完全不冲突,这正是你想要的效果!

针对代码的优化建议(覆盖更多场景)

你的基础逻辑没问题,但可以扩展一下,覆盖"用户已连接WebSocket后,尝试发送消息到非所属群组"的场景,同时优化细节:

1. 扩展拦截范围,覆盖MESSAGE命令

当前你的拦截器只在CONNECT阶段做验证,但如果用户已经成功连接,之后尝试发送消息到不属于自己的群组,当前逻辑不会处理。可以修改preSend方法,增加对MESSAGE命令的校验:

@Slf4j
@Component
@RequiredArgsConstructor
public class WebSocketAuthChannelInterceptor implements ChannelInterceptor {

    private final JwtService jwtService;
    private final GroupService groupService;
    private SimpMessagingTemplate messagingTemplate;

    @Autowired
    public void setMessagingTemplate(@Lazy SimpMessagingTemplate messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
    }

    // 匹配群组消息的正则,用于提取groupId
    private static final Pattern GROUP_DEST_PATTERN = Pattern.compile("/queue/group/(\\d+)");

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        assert accessor != null;

        // 处理WebSocket连接阶段的认证与群组校验
        if (StompCommand.CONNECT.equals(accessor.getCommand())) {
            String token = accessor.getFirstNativeHeader("Authorization");
            if (token == null || !token.startsWith("Bearer ")) {
                log.warn("No valid token provided for WebSocket connection");
                sendErrorToUserFromToken(token, "请提供有效的认证令牌!");
                return null;
            }

            token = token.substring(7);
            if (!jwtService.isTokenValid(token)) {
                log.warn("Invalid or expired WebSocket token");
                sendErrorToUserFromToken(token, "令牌无效或已过期!");
                return null;
            }

            String groupId = accessor.getFirstNativeHeader("groupId");
            long userId = jwtService.extractId(token);
            log.info("WebSocket connection attempt for group: {}", groupId);

            if (!groupService.validateUserInGroup(groupId, userId)) {
                log.warn("User {} is not a member of group {}", userId, groupId);
                sendErrorToUser(userId, "你不属于该群组,无法连接!");
                return null;
            }

            // 构建认证上下文
            String email = jwtService.extractEmail(token);
            List<String> roles = jwtService.extractRoles(token);
            Authentication auth = new UsernamePasswordAuthenticationToken(
                    email, null, mapRolesToAuthorities(roles)
            );
            accessor.setUser(auth);
            SecurityContextHolder.getContext().setAuthentication(auth);
            log.info("WebSocket authenticated user: {}", email);
            return message;
        }

        // 处理已连接用户发送群组消息的权限校验
        if (StompCommand.MESSAGE.equals(accessor.getCommand())) {
            String dest = accessor.getDestination();
            if (dest != null && dest.startsWith("/queue/group/")) {
                Matcher matcher = GROUP_DEST_PATTERN.matcher(dest);
                if (matcher.matches()) {
                    String groupId = matcher.group(1);
                    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
                    if (auth == null) {
                        log.warn("No authenticated user found for group message");
                        return null;
                    }

                    // 从认证上下文获取用户ID,根据你的业务调整实现
                    long userId = getUserIdFromAuth(auth);
                    if (!groupService.validateUserInGroup(groupId, userId)) {
                        log.warn("User {} tried to send message to non-member group {}", userId, groupId);
                        sendErrorToUser(userId, "你不属于该群组,无法发送消息!");
                        return null; // 拦截这条群组消息
                    }
                }
            }
            return message;
        }

        // 其他命令直接放行
        return message;
    }

    // 转换角色为Spring权限对象
    private Collection<? extends GrantedAuthority> mapRolesToAuthorities(List<String> roles) {
        return roles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toList());
    }

    // 向指定用户的错误队列发送提示
    private void sendErrorToUser(long userId, String errorMsg) {
        String dest = "/queue/errors/" + userId;
        messagingTemplate.convertAndSend(dest, errorMsg);
    }

    // 处理令牌无效时的错误消息发送(尽量解析用户ID)
    private void sendErrorToUserFromToken(String token, String errorMsg) {
        if (token != null && token.startsWith("Bearer ")) {
            try {
                long userId = jwtService.extractId(token.substring(7));
                sendErrorToUser(userId, errorMsg);
            } catch (Exception e) {
                log.warn("Failed to send error message: invalid token format");
            }
        }
    }

    // 从认证上下文获取用户ID,根据你的业务实现调整
    private long getUserIdFromAuth(Authentication auth) {
        // 示例:从认证信息的用户名(邮箱)反向查询用户ID,或者直接从JWT解析
        String email = auth.getName();
        return jwtService.extractIdFromEmail(email);
    }
}

2. 关键细节说明

  • @Lazy注入的必要性:你之前的代码已经做对了这一点,它能避免SimpMessagingTemplateChannelInterceptor之间的循环依赖问题,必须保留。
  • 独立的错误消息流:无论preSend返回null还是原消息,messagingTemplate.convertAndSend都会独立执行,因为它直接和broker通信,不受当前拦截的消息流程影响。
  • 日志的验证作用:你可以通过日志确认两个动作的执行顺序:错误消息先被broker处理,之后才会提示原消息被拦截,这完全符合预期。

最后再确认

你的原始代码已经实现了"拒绝原消息+发送错误提示"的核心逻辑,日志也已经验证了这一点。如果还有问题,大概率是没有覆盖到"已连接用户发送群组消息"的场景,按照上面的代码扩展即可。

内容来源于stack exchange

火山引擎 最新活动