SignalR: не удается получать сообщения, отправленные группе с помощью SignalR и функции Azure

#c# #azure #azure-functions #signalr #azure-signalr

#c# #azure #azure-функции #signalr #azure-signalr

Вопрос:

Я следил за примерами, приведенными в двунаправленном примере SignalR, и, похоже, я не могу получить ответ от образца SendToGroup. Когда я запускаю функцию Azure локально, я вижу, что согласование происходит успешно, и токен отправляется обратно, но ни один из других вызовов функции Azure не работает со страницы индекса.

Function.cs

 using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Mvc;

namespace FunctionApp
{
    public class SimpleChat : ServerlessHub
    {
        private const string NewMessageTarget = "newMessage";
        private const string NewConnectionTarget = "newConnection";

        [FunctionName("index")]
        public IActionResult GetHomePage([HttpTrigger(AuthorizationLevel.Anonymous)]HttpRequest req, ExecutionContext context)
        {
            var path = Path.Combine(context.FunctionAppDirectory, "content", "index.html");
            Console.WriteLine(path);
            return new ContentResult
            {
                Content = File.ReadAllText(path),
                ContentType = "text/html",
            };
        }

        [FunctionName("negotiate")]
        public SignalRConnectionInfo Negotiate([HttpTrigger(AuthorizationLevel.Anonymous)]HttpRequest req)
        {
            return Negotiate(req.Headers["x-ms-signalr-user-id"], GetClaims(req.Headers["Authorization"]));
        }

        [FunctionName(nameof(OnConnected))]
        public async Task OnConnected([SignalRTrigger]InvocationContext invocationContext, ILogger logger)
        {
            invocationContext.Headers.TryGetValue("Authorization", out var auth);
            await Clients.All.SendAsync(NewConnectionTarget, new NewConnection(invocationContext.ConnectionId, auth));
            logger.LogInformation($"{invocationContext.ConnectionId} has connected");
        }

        [FunctionAuthorize]
        [FunctionName(nameof(Broadcast))]
        public async Task Broadcast([SignalRTrigger]InvocationContext invocationContext, string message, ILogger logger)
        {
            await Clients.All.SendAsync(NewMessageTarget, new NewMessage(invocationContext, message));
            logger.LogInformation($"{invocationContext.ConnectionId} broadcast {message}");
        }

        [FunctionName(nameof(SendToGroup))]
        public async Task SendToGroup([SignalRTrigger]InvocationContext invocationContext, string groupName, string message)
        {
            await Clients.Group(groupName).SendAsync(NewMessageTarget, new NewMessage(invocationContext, message));
        }

        [FunctionName(nameof(SendToUser))]
        public async Task SendToUser([SignalRTrigger]InvocationContext invocationContext, string userName, string message)
        {
            await Clients.User(userName).SendAsync(NewMessageTarget, new NewMessage(invocationContext, message));
        }

        [FunctionName(nameof(SendToConnection))]
        public async Task SendToConnection([SignalRTrigger]InvocationContext invocationContext, string connectionId, string message)
        {
            await Clients.Client(connectionId).SendAsync(NewMessageTarget, new NewMessage(invocationContext, message));
        }

        [FunctionName(nameof(JoinGroup))]
        public async Task JoinGroup([SignalRTrigger]InvocationContext invocationContext, string connectionId, string groupName)
        {
            await Groups.AddToGroupAsync(connectionId, groupName);
        }

        [FunctionName(nameof(LeaveGroup))]
        public async Task LeaveGroup([SignalRTrigger]InvocationContext invocationContext, string connectionId, string groupName)
        {
            await Groups.RemoveFromGroupAsync(connectionId, groupName);
        }

        [FunctionName(nameof(JoinUserToGroup))]
        public async Task JoinUserToGroup([SignalRTrigger]InvocationContext invocationContext, string userName, string groupName)
        {
            await UserGroups.AddToGroupAsync(userName, groupName);
        }

        [FunctionName(nameof(LeaveUserFromGroup))]
        public async Task LeaveUserFromGroup([SignalRTrigger]InvocationContext invocationContext, string userName, string groupName)
        {
            await UserGroups.RemoveFromGroupAsync(userName, groupName);
        }

        [FunctionName(nameof(OnDisconnected))]
        public void OnDisconnected([SignalRTrigger]InvocationContext invocationContext)
        {
        }

        private class NewConnection
        {
            public string ConnectionId { get; }

            public string Authentication { get; }

            public NewConnection(string connectionId, string authentication)
            {
                ConnectionId = connectionId;
                Authentication = authentication;
            }
        }

        private class NewMessage
        {
            public string ConnectionId { get; }
            public string Sender { get; }
            public string Text { get; }

            public NewMessage(InvocationContext invocationContext, string message)
            {
                Sender = string.IsNullOrEmpty(invocationContext.UserId) ? string.Empty : invocationContext.UserId;
                ConnectionId = invocationContext.ConnectionId;
                Text = message;
            }
        }
    }
}
 

Веб-клиент

 <html>

<head>
  <title>Serverless Chat</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css">
  <script>
    window.apiBaseUrl = window.location.origin;
  </script>
  <style>
    .slide-fade-enter-active,
    .slide-fade-leave-active {
      transition: all 1s ease;
    }

    .slide-fade-enter,
    .slide-fade-leave-to {
      height: 0px;
      overflow-y: hidden;
      opacity: 0;
    }
  </style>
</head>

<body>
  <p>amp;nbsp;</p>
  <div id="app" class="container">
    <h3>Serverless chat</h3>
    <div class="row" v-if="ready">
      <div class="signalr-demo col-sm">
        <hr />
        <div id='groupchecked'>
          <input type="checkbox" id="checkbox" v-model="checked">
          <label for="checkbox">Send To Default Group: {{ this.defaultgroup }}</label>
        </div>
        <form v-on:submit.prevent="sendNewMessage(checked)">
          <input type="text" v-model="newMessage" id="message-box" class="form-control" placeholder="Type message here..." />
        </form>
      </div>
    </div>
    <div class="row" v-if="!ready">
      <div class="col-sm">
        <div>Loading...</div>
      </div>
    </div>
    <div v-if="ready">
      <transition-group name="slide-fade" tag="div">
        <div class="row" v-for="message in messages" v-bind:key="message.id">
          <div class="col-sm">
            <hr />
            <div>
              <div style="display: inline-block; padding-left: 12px;">
                <div>
                  <a href="#" v-on:click.prevent="sendPrivateMessage(message.Sender)">
                    <span class="text-info small">
                      <strong>{{ message.Sender || message.sender }}</strong>
                    </span>
                  </a>
                  <span v-if="message.ConnectionId || message.connectionId">
                    <a href="#" v-on:click.prevent="sendToConnection(message.ConnectionId || message.connectionId)">
                      <span class="badge badge-primary">Connection: {{ message.ConnectionId || message.connectionId }}</span>
                    </a>
                  </span>
                  <a href="#" v-on:click.prevent="addUserToGroup(message.Sender || message.sender)">
                    <span class="badge badge-primary">AddUserToGroup</span>
                  </a>
                  <a href="#" v-on:click.prevent="removeUserFromGroup(message.Sender || message.sender)">
                    <span class="badge badge-primary">RemoveUserFromGroup</span>
                  </a>
                  <a href="#" v-on:click.prevent="addConnectionToGroup(message.ConnectionId || message.connectionId)">
                    <span v-if="message.ConnectionId || message.connectionId" class="badge badge-primary">AddConnectionToGroup</span>
                  </a>
                  <a href="#" v-on:click.prevent="removeConnectionIdFromGroup(message.ConnectionId || message.connectionId)">
                    <span v-if="message.ConnectionId || message.connectionId" class="badge badge-primary">RemoveConnectionFromGroup</span>
                  </a>
                  <span v-if="message.IsPrivate || message.isPrivate" class="badge badge-secondary">private message
                  </span>
                </div>
                <div>
                  {{ message.Text || message.text }}
                </div>
              </div>
            </div>
          </div>
        </div>
      </transition-group>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@aspnet/signalr@1.0.3/dist/browser/signalr.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/crypto-js@3.1.9-1/crypto-js.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/crypto-js@3.1.9-1/enc-base64.js"></script>
    <script>
      const data = {
        username: '',
        defaultgroup: 'AzureSignalR',
        checked: false,
        newMessage: '',
        messages: [],
        myConnectionId: '',
        ready: false
      };
      const app = new Vue({
        el: '#app',
        data: data,
        methods: {
          sendNewMessage: function (isToGroup) {
            if (isToGroup) {
              connection.invoke("sendToGroup", this.defaultgroup, this.newMessage);
            }
            else {
              connection.invoke("broadcast", this.newMessage);
            }
            this.newMessage = '';
          },
          sendPrivateMessage: function (user) {
            const messageText = prompt('Send private message to '   user);

            if (messageText) {
              connection.invoke("sendToUser", user, messageText);
            }
          },
          sendToConnection: function (connectionId) {
            const messageText = prompt('Send private message to connection '   connectionId);

            if (messageText) {
              connection.invoke("sendToConnection", connectionId, messageText);
            }
          },
          addConnectionToGroup: function(connectionId) {
            confirm('Add connection '   connectionId   ' to group: '   this.defaultgroup);
            connection.invoke("joinGroup", connectionId, this.defaultgroup);
          },
          addUserToGroup: function (user) {
            r = confirm('Add user '   user   ' to group: '   this.defaultgroup);
            connection.invoke("joinUserToGroup", user, this.defaultgroup);
          },
          removeConnectionIdFromGroup: function(connectionId) {
            confirm('Remove connection '   connectionId   ' from group: '   this.defaultgroup);
            connection.invoke("leaveGroup", connectionId, this.defaultgroup);
          },
          removeUserFromGroup: function(user) {
            confirm('Remove user '   user   ' from group: '   this.defaultgroup);
            connection.invoke("leaveUserFromGroup", user, this.defaultgroup);
          }
        }
      });
      const apiBaseUrl = window.location.origin;
      data.username = prompt("Enter your username");
      const isAdmin = confirm('Work as administrator? (only an administrator can broadcast messages)');
      if (!data.username) {
        alert("No username entered. Reload page and try again.");
        throw "No username entered";
      }
      const connection = new signalR.HubConnectionBuilder()
        .withUrl(apiBaseUrl   '/api', {
          accessTokenFactory: () => {
            return generateAccessToken(data.username)
          }
        })
        .configureLogging(signalR.LogLevel.Information)
        .build();
      connection.on('newMessage', onNewMessage);
      connection.on('newConnection', onNewConnection)
      connection.onclose(() => console.log('disconnected'));
      console.log('connecting...');
      connection.start()
        .then(() => {
          data.ready = true;
          console.log('connected!');
        })
        .catch(console.error);
      function getAxiosConfig() {
        const config = {
          headers: {
            'x-ms-signalr-user-id':  data.username,
            'Authorization': 'Bearer '   generateAccessToken(data.username)
          }
        };
        return config;
      }
      let counter = 0;
      function onNewMessage(message) {
        message.id = counter  ; // vue transitions need an id
        data.messages.unshift(message);
      };
      function onNewConnection(message) {
        data.myConnectionId = message.ConnectionId;
        authEnabled = false;
        if (message.Authentication)
        {
          authEnabled = true;
        }
        newConnectionMessage = {
          id : counter  ,
          text : `${message.ConnectionId} has connected, with Authorization: ${authEnabled.toString()}`
        };
        data.messages.unshift(newConnectionMessage);
      }

      function base64url(source) {
        // Encode in classical base64
        encodedSource = CryptoJS.enc.Base64.stringify(source);

        // Remove padding equal characters
        encodedSource = encodedSource.replace(/= $/, '');

        // Replace characters according to base64url specifications
        encodedSource = encodedSource.replace(/ /g, '-');
        encodedSource = encodedSource.replace(///g, '_');

        return encodedSource;
      }

      // this function should be in auth server, do not expose your secret
      function generateAccessToken(userName) {
        var header = {
          "alg": "HS256",
          "typ": "JWT"
        };

        var stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header));
        var encodedHeader = base64url(stringifiedHeader);

        // customize your JWT token payload here 
        var data = {
          "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": userName,
          "exp": 1699819025,
          'admin': isAdmin
        };

        var stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data));
        var encodedData = base64url(stringifiedData);

        var token = encodedHeader   "."   encodedData;

        var secret = "myfunctionauthtest"; // do not expose your secret here

        var signature = CryptoJS.HmacSHA256(token, secret);
        signature = base64url(signature);

        var signedToken = token   "."   signature;

        return signedToken;
      }
    </script>
</body>

</html>
 

Комментарии:

1. Настройка для этого образца несколько сложна. Вы пробовали пройти процесс настройки во второй раз? Обратите пристальное внимание на биты о шаблоне URL-адреса в восходящем потоке. Удачи вам!

2. Это может быть полезно: локальный эмулятор для SignalR github.com/Azure/azure-signalr/issues/969