Что я пытаюсь сделать
Я хочу проверить аутентификацию и авторизацию во время запроса SUBSCRIBE (например, пользователь не прошел аутентификацию или нет) разрешено подписаться на комнату).
Когда проверка не удалась, я хочу:
- Отправить кадр STOMP ERROR обратно клиенту
- Проинструктировать клиента закрыть соединение WebSocket
- Логика аутентификации внутри ChannelInterceptor#preSend работает правильно
- Я могу обнаружить недействительные или несанкционированные попытки подписки, опущенные в примере кода.
- Я не могу надежно отправить кадр STOMP ERROR обратно клиенту из preSend
- Возврат созданного вручную сообщения ERROR не достигает внешнего интерфейса, как ожидалось
- Возврат null только блокирует сообщение, но не уведомляет клиент
Когда подписка недействительна, клиент должен получить кадр STOMP ERROR с сообщением типа «Несанкционированный доступ» или «Неверная полезная нагрузка подписки», после чего клиент закрывает соединение.
Что на самом деле происходит
- Клиент не получает кадр ошибки
- В некоторых случаях соединение остается открытым
- Попытки решить эту проблему путем подключения дополнительных компонентов приводят к ошибкам циклической зависимости
- Внедрение clientOutboundChannel
- Насколько я понимаю, входящие каналы не могут напрямую отправлять кадры клиентам
- Это приводило к проблемам с циклическими зависимостями компонентов
- Использование SimpMessagingTemplate внутри перехватчик
- Также приводили к проблемам циклической зависимости с WebSocketConfig и брокером сообщений
- WebSocketConfig регистрирует конечные точки STOMP и настраивает перехватчик входящего канала
- SocketChannelInterceptor выполняет аутентификацию при CONNECT и авторизацию при SUBSCRIBE
- Когда авторизация не удалась, я пытаюсь создать и вернуть кадр STOMP ERROR с помощью StompHeaderAccessor
@Configuration()
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final SocketChannelInterceptor socketChannelInterceptor;
@Autowired
public WebSocketConfig(JwtService jwtService, SocketChannelInterceptor socketChannelInterceptor, UserService userService) {
this.socketChannelInterceptor = socketChannelInterceptor;
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*");
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
WebSocketMessageBrokerConfigurer.super.configureClientOutboundChannel(registration);
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/chat", "/queue");
registry.setUserDestinationPrefix("/user");
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(socketChannelInterceptor);
}
}
@Component()
public class SocketChannelInterceptor implements ChannelInterceptor {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Autowired()
SocketChannelInterceptor( JwtService jwtService, UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
this.jwtService = jwtService;
}
@Override
public Message preSend(Message message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
assert accessor != null;
if(StompCommand.CONNECT.equals(accessor.getCommand())) {
String authHeader = accessor.getFirstNativeHeader("Authorization");
assert authHeader != null;
if(authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
UserDetails user = userDetailsService.loadUserByUsername(jwtService.extractUsername(token));
if(jwtService.isTokenValid(token, user)){
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
user,
null,
user.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authToken);
accessor.setUser(authToken);
}
}
}
if (StompCommand.SUBSCRIBE == accessor.getCommand()) {
String destination = accessor.getDestination();
StompHeaderAccessor errorAccessor = StompHeaderAccessor.create(StompCommand.ERROR);
if (destination == null || destination.isBlank()) {
errorAccessor.setLeaveMutable(true);
errorAccessor.setMessage("Destination is blank");
errorAccessor.setSessionId(accessor.getSessionId());
return MessageBuilder.createMessage("", errorAccessor.getMessageHeaders());
}
if(destination.startsWith("/chat/")) {
String roomId = destination.substring("/chat/".length());
if(roomId.isBlank()) {
errorAccessor.setLeaveMutable(true);
errorAccessor.setMessage("Invalid Subscription Payload");
errorAccessor.setSessionId(accessor.getSessionId());
return null;
};
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) accessor.getUser();
if(token == null) {
errorAccessor.setLeaveMutable(true);
errorAccessor.setMessage("Unauthorized Access");
errorAccessor.setSessionId(accessor.getSessionId());
return MessageBuilder.createMessage(new Byte[0], errorAccessor.getMessageHeaders());
}
Users user = (Users) token.getPrincipal();
UUID userId = user.getUserId();
//check if user is in room
};
}
return message;
}
}
Подробнее здесь: https://stackoverflow.com/questions/798 ... nvalid-sub
Мобильная версия