Как обрабатывать конечную точку подписки webbocket с пружинным загрузчиком для авторизации пользователей - PullRequest
0 голосов
/ 20 февраля 2020

Мы успешно разработали webosocket + stomp + rabbitmq для нашего проекта с безопасностью. Он работает нормально, хотя у нас есть некоторые проблемы в решении следующего случая: Рабочий процесс этого веб-сокета работает следующим образом:

  • Первый пользователь подписывается на конечную точку веб-сокета, которая работает как положено

  • Через секунду после авторизации токеном пользователя, пользователь пытается подписаться на следующую конечную точку '/ user / queue /' + chatRoomId + '. Сообщения'. Здесь chatroomId определяет, к какому пользователь чата подключается, что также работает нормально, однако здесь пользователь может подключить любой chatroomid, который не проверяется в серверной части, что также является большой проблемой, которую мы пытаемся решить.

    stompClient.subscribe ('/ user / queue /' + chatRoomId + '.messages', ReceiveMessages);

Мой вопрос: как я могу проверять пользователей, когда они попытаться подписаться на эту конечную точку? Я имею в виду, есть ли способ обработать каждую конкретную c подписку

Это наш код переднего плана. если вам нужна полная страница, я загрузлю ее

 function connect() {
        socket = new SockJS('http://localhost:9600/wsss/messages');
        stompClient = Stomp.over(socket);
        // var stompClient = Stomp.client("ws://localhost:9600/ws/messages");
        // stompClient.connect({ 'chat_id' : chatRoomId,
        //     'X-Authorization' : 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0Iiwic2NvcGVzIjoiUk9MRV9BRE1JTiIsImVtYWlsIjoiYWRtaW5AZ21haWwuY29tIiwiaWF0IjoxNTc5MDgxMzg5LCJleHAiOjE1ODE2NzMzODl9.H3mnti0ZNtH6uLe-sOfrr5jzwssvGNcBiHGg-nUQ6xY' },
        //     stompSuccess, stompFailure);
        stompClient.connect({ 'chatRoomId' : chatRoomId,
            'login' : 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyODg5Iiwic2NvcGVzIjoiUk9MRV9VU0VSLFJPTEVfTU9ERVJBVE9SIiwiaWF0IjoxNTgyMDMxMDA0LCJleHAiOjE1ODQ2MjMwMDR9.NGAAed4R46FgrtgyDmrLSrmd-o3tkqbF60vOg8vAWYg' },
            stompSuccess, stompFailure);
    }

    function stompSuccess(frame) {
        enableInputMessage();
        successMessage("Your WebSocket connection was successfuly established!");
        console.log(frame);

        stompClient.subscribe('/user/queue/' + chatRoomId + '.messages', incomingMessages);
        stompClient.subscribe('/topic/notification', incomingNotificationMessage);
        // stompClient.subscribe('/app/join/notification', incomingNotificationMessage);
    }

А вот код, который я использую для моего бэкэнда

@Configuration
@EnableWebSocketMessageBroker
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
class WebSocketConfig @Autowired constructor(
        val jwtTokenUtil: TokenProvider
) : WebSocketMessageBrokerConfigurer {


    @Autowired
    @Resource(name = "userService")
    private val userDetailsService: UserDetailsService? = null

    @Autowired
    private lateinit var authenticationManager: AuthenticationManager

    @Value("\${spring.rabbitmq.username}")
    private val userName: String? = null
    @Value("\${spring.rabbitmq.password}")
    private val password: String? = null
    @Value("\${spring.rabbitmq.host}")
    private val host: String? = null
    @Value("\${spring.rabbitmq.port}")
    private val port: Int = 0
    @Value("\${endpoint}")
    private val endpoint: String? = null
    @Value("\${destination.prefix}")
    private val destinationPrefix: String? = null
    @Value("\${stomp.broker.relay}")
    private val stompBrokerRelay: String? = null

    override fun configureMessageBroker(config: MessageBrokerRegistry) {
        config.enableStompBrokerRelay("/queue/", "/topic/")
                .setRelayHost(host!!)
                .setRelayPort(port)
                .setSystemLogin(userName!!)
                .setSystemPasscode(password!!)
        config.setApplicationDestinationPrefixes(destinationPrefix!!)
    }

    override fun registerStompEndpoints(registry: StompEndpointRegistry) {
        registry.addEndpoint("/websocket").setAllowedOrigins("*").setAllowedOrigins("*")
        registry.addEndpoint("/websocket/messages").addInterceptors(customHttpSessionHandshakeInterceptor()).setAllowedOrigins("*")
        registry.addEndpoint("/wsss/messages").addInterceptors(customHttpSessionHandshakeInterceptor()).setAllowedOrigins("*").withSockJS()
    }

    @Bean
    fun customHttpSessionHandshakeInterceptor(): CustomHttpSessionHandshakeInterceptor {
        return CustomHttpSessionHandshakeInterceptor()
    }

    override fun configureClientInboundChannel(registration: ChannelRegistration) {
        registration.interceptors(object : ChannelInterceptor {
            override fun preSend(message: Message<*>, channel: MessageChannel): Message<*> {
                val accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor::class.java)
                if (StompCommand.CONNECT == accessor!!.command || StompCommand.STOMP == accessor.command) {
                    val authorization = accessor.getNativeHeader("login")
                    println("X-Authorization: {$authorization}")
                    val authToken = authorization!![0].split(" ")[1]

                    val username = jwtTokenUtil.getUsernameFromToken(authToken)

                    if (username != null) {
                        if(username.contains("@")) {
                            val userDetails = userDetailsService!!.loadUserByUsername(username)

                            if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                                val authentication = jwtTokenUtil.getAuthentication(authToken, SecurityContextHolder.getContext().authentication, userDetails)
                                accessor.user = authentication
                            }
                        } else {
                            val authorities = jwtTokenUtil.getAuthoritiesFromToken(authToken)
                            val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(username, "", authorities)
                            val authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken)
                            accessor.user = authentication
                        }
                    }


                }
                return message
            }
        })
    }
}

Вот обработчик событий

@Component
class WebSocketEvents  {


    @EventListener
    fun handleSessionConnected(event: SessionConnectEvent) {

        val headers = SimpMessageHeaderAccessor.wrap(event.message)
        if ( headers.getNativeHeader("chatRoomId") != null && headers.getNativeHeader("chatRoomId")!!.isNotEmpty()){
            val chatId = headers.getNativeHeader("chatRoomId")!![0]
            if (headers.sessionAttributes != null)
                headers.sessionAttributes!!["chatRoomId"] = chatId
        }
    }

    @EventListener
    fun handleSessionDisconnect(event: SessionDisconnectEvent) {
        val headers = SimpMessageHeaderAccessor.wrap(event.message)
        val chatRoomId = headers.sessionAttributes!!["chatRoomId"].toString()
    }
}

Пока что я попробовал: Как вы можете видеть выше, когда пользователь впервые подключается к конечной точке websocket http://localhost: 9600 / wsss / messages он отправляет токен и идентификатор чата (заголовки), и я обрабатываю это в компоненте прослушивателя событий, сбрасывая chatroomid в атрибуты заголовка. Что мне действительно нужно сделать, так это взять идентификатор чата, пока пользователь подписывается на это конкретное описание, и применить проверку, принадлежит ли он этому чату, и если да, просто дать ему разрешение | пусть он присоединится к чату, если не вернет ошибку. Я действительно ценю любые мысли или обходные пути!

1 Ответ

0 голосов
/ 24 февраля 2020

Я потратил пару дней на поиски ответа, но не нашел ни одного, поэтому сам разобрался. Вот мое решение этой проблемы, хотя оно и не является полным.

Я создал отдельный класс-перехватчик для обработки всех типов соединений, как я делал при перехвате команды подписки. Мне пришло в голову, почему бы не использовать команду «Подписаться» для прослушивания действий пользователей и правильного реагирования на них. Например, вот так:

    @Component
class WebSocketTopicHandlerInterceptor constructor() : ChannelInterceptor {


    override fun preSend(message: Message<*>, channel: MessageChannel): Message<*>? {
        val accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor::class.java)
        if (StompCommand.CONNECT == accessor!!.command || StompCommand.STOMP == accessor.command) {
            val authorization = accessor.getNativeHeader("login").apply { if (isNullOrEmpty()) throw LoggedError(AuthorizationException()) }
            val authToken = authorization!![0].split(" ").apply { if (size <= 1) throw LoggedError(InvalidTokenException("Token is not valid")) }[1]

            val username = jwtTokenUtil.getUsernameFromToken(authToken)

            //DO YOUR AUTHENTICATION HERE


        }



        if (StompCommand.SUBSCRIBE == accessor.command) {
            val destination = accessor.destination
            if (destination.isNullOrBlank()) throw LoggedError(CustomBadRequestException("Subscription destionation cannot be null! U DUMB IDIOT!"))
            val chatPattern = "/user/queue/+[a-zA-Z0-9-]+.messages".toRegex()
            val notificationPattern = "/topic/notification".toRegex()

            if (chatPattern.matches(accessor.destination!!)) println("working")

            // FINDING OUT WHERE USER IS TRYING TO SUBSCRIBE ALL ROUTING LOGIC GOES HERE...
            when {
                chatPattern.matches(destination) -> {
                   //do your all logic here
                }
                notificationPattern.matches(destination) -> {
                    //do your all logic here
                }
            }

        }
        return message
    }
}

ЭТО НИЧЕГО НЕ ДЕЛАЕТ, ЧТО ТОЛЬКО ДАЙТЕ МНЕ, ЧТО Я БУДУ ОЧЕНЬ СЧАСТЛИВ СВОЮ РАБОТУ С ДАЛЬШЕ.

Что у меня есть в моем случае сделано то, что я выяснил, куда направляется пользователь, и проведу там всю мою проверку, иначе пользователь не сможет подписаться на какой-либо канал, что означает, что он очень безопасен.

...