SignalR:无法接收使用 SignalR 和 Azure 函数发送到组的消息

问题描述

我一直在关注 SignalR Bidirectional sample 中提供的示例,但似乎无法从 SendToGroup 示例中获得响应。当我在本地运行 Azure 函数时,我可以看到协商成功发生并且令牌被发回,但对 Azure 函数的其他调用都无法从索引页面工作。

函数.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,message));
        }

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

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

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

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

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

        [FunctionName(nameof(LeaveUserFromGroup))]
        public async Task LeaveUserFromGroup([SignalRTrigger]InvocationContext invocationContext,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>&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,addConnectionToGroup: function(connectionId) {
            confirm('Add connection ' + connectionId + ' to group: ' + this.defaultgroup);
            connection.invoke("joinGroup",this.defaultgroup);
          },addUserToGroup: function (user) {
            r = confirm('Add user ' + user + ' to group: ' + this.defaultgroup);
            connection.invoke("joinUserToGroup",removeConnectionIdFromGroup: function(connectionId) {
            confirm('Remove connection ' + connectionId + ' from group: ' + this.defaultgroup);
            connection.invoke("leaveGroup",removeUserFromGroup: function(user) {
            confirm('Remove user ' + user + ' from group: ' + this.defaultgroup);
            connection.invoke("leaveUserFromGroup",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>

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)