Azure无服务器SignalR客户端调用失败及直连403问题咨询
客户端间RPC通信实现问题
目标
实现客户端兼具服务端与客户端角色的RPC通信,采用Azure无服务器SignalR服务搭配Azure Function触发器绑定。
当前问题
客户端无法调用相关方法,SignalR触发器的广播功能未触发;直接连接SignalR Service时返回403错误(未启用托管标识)。
疑问
- 实现思路是否正确?
- 是否必须通过Azure Function作为中转,才能向SignalR Service发送消息并由其推送给监听客户端?
Azure Function 代码
using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Azure.Functions.Worker.SignalRService; using Microsoft.Extensions.Logging; namespace DotnetIsolated_ClassBased { [SignalRConnection("AzureSignalRConnectionString")] public class Functions : ServerlessHub<IChatClient> { private const string HubName = "<hub_name>"; // 修正原代码缺少的分号 private readonly ILogger _logger; public Functions(IServiceProvider serviceProvider, ILogger<Functions> logger) : base(serviceProvider) { _logger = logger; } [Function(nameof(BroadcastToAll))] [SignalROutput(HubName = HubName, ConnectionStringSetting = "AzureSignalRConnectionString")] public static SignalRMessageAction BroadcastToAll([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req) { using var bodyReader = new StreamReader(req.Body); return new SignalRMessageAction("newMessage") { Arguments = new[] { bodyReader.ReadToEnd() }, }; } [Function("negotiate")] public async Task<HttpResponseData> Negotiate([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req) { _logger.LogInformation($"Negotiating with user: {req.Headers.GetValues("userId").FirstOrDefault()}"); var negotiateResponse = await NegotiateAsync(new() { UserId = req.Headers.GetValues("userId").FirstOrDefault() }); var response = req.CreateResponse(); await response.WriteBytesAsync(negotiateResponse.ToArray()); return response; } [Function("OnConnected")] public Task OnConnected([SignalRTrigger(HubName, "connections", "connected")] SignalRInvocationContext invocationContext) { invocationContext.Headers.TryGetValue("Authorization", out var auth); _logger.LogInformation($"{invocationContext.ConnectionId} has connected"); return Clients.All.newConnection(new NewConnection(invocationContext.ConnectionId, auth)); } [Function("Broadcast")] public Task Broadcast( [SignalRTrigger(HubName, "messages", "broadcast", "message")] SignalRInvocationContext invocationContext, string message) { _logger.LogInformation($"Broadcasting message: {message}"); return Clients.All.newMessage(new NewMessage(invocationContext, message)); } [Function("SendToGroup")] public Task SendToGroup([SignalRTrigger(HubName, "messages", "SendToGroup", "groupName", "message")] SignalRInvocationContext invocationContext, string groupName, string message) { return Clients.Group(groupName).newMessage(new NewMessage(invocationContext, message)); } [Function("SendToUser")] public Task SendToUser([SignalRTrigger(HubName, "messages", "SendToUser", "userName", "message")] SignalRInvocationContext invocationContext, string userName, string message) { return Clients.User(userName).newMessage(new NewMessage(invocationContext, message)); } [Function("SendToConnection")] public Task SendToConnection([SignalRTrigger(HubName, "messages", "SendToConnection", "connectionId", "message")] SignalRInvocationContext invocationContext, string connectionId, string message) { return Clients.Client(connectionId).newMessage(new NewMessage(invocationContext, message)); } [Function("JoinGroup")] public Task JoinGroup([SignalRTrigger(HubName, "messages", "JoinGroup", "connectionId", "groupName")] SignalRInvocationContext invocationContext, string connectionId, string groupName) { return Groups.AddToGroupAsync(connectionId, groupName); } [Function("LeaveGroup")] public Task LeaveGroup([SignalRTrigger(HubName, "messages", "LeaveGroup", "connectionId", "groupName")] SignalRInvocationContext invocationContext, string connectionId, string groupName) { return Groups.RemoveFromGroupAsync(connectionId, groupName); } [Function("JoinUserToGroup")] public Task JoinUserToGroup([SignalRTrigger(HubName, "messages", "JoinUserToGroup", "userName", "groupName")] SignalRInvocationContext invocationContext, string userName, string groupName) { return UserGroups.AddToGroupAsync(userName, groupName); } [Function("LeaveUserFromGroup")] public Task LeaveUserFromGroup([SignalRTrigger(HubName, "messages", "LeaveUserFromGroup", "userName", "groupName")] SignalRInvocationContext invocationContext, string userName, string groupName) { return UserGroups.RemoveFromGroupAsync(userName, groupName); } [Function("OnDisconnected")] public void OnDisconnected([SignalRTrigger(HubName, "connections", "disconnected")] SignalRInvocationContext invocationContext) { _logger.LogInformation($"{invocationContext.ConnectionId} has disconnected"); } } public class NewConnection { public string ConnectionId { get; } public string Authentication { get; } public NewConnection(string connectionId, string auth) { ConnectionId = connectionId; Authentication = auth; } } public class NewMessage { public string ConnectionId { get; } public string Sender { get; } public string Text { get; } public NewMessage(SignalRInvocationContext invocationContext, string message) { Sender = string.IsNullOrEmpty(invocationContext.UserId) ? string.Empty : invocationContext.UserId; ConnectionId = invocationContext.ConnectionId; Text = message; } } public interface IChatClient { Task newMessage(NewMessage message); Task newConnection(NewConnection connection); } }
客户端代码
// 所有客户端代码结构如下 var connection = new HubConnectionBuilder() .WithUrl("<azure_function_endpoint>/api", (options) => { options.Headers.Add("UserId", "<some_user_id>"); options.AccessTokenProvider = async () => { return await Task.FromResult("<access_token>"); // 已知硬编码token,仅用于POC测试 }; }) .Build(); // 修正监听方法名称与参数类型 connection.On<NewMessage>("newMessage", msg => Console.WriteLine($"{msg.Sender}: {msg.Text}")); await connection.StartAsync(); await connection.SendAsync("Broadcast", "This is a broadcast");
问题解答
1. 实现思路正确性
整体思路符合Azure无服务器SignalR的典型使用场景,是可行的。但当前代码存在几个关键问题导致功能失效:
- 客户端监听的方法名称(
Broadcast)与Function中广播的方法名称(newMessage)不匹配,导致客户端收不到消息。 - Function代码存在语法错误(
HubName常量缺少分号),会导致编译失败。 - 连接字符串配置项不统一(类上用
AzureSignalRConnectionString,BroadcastToAll方法用SignalRConnection),导致Function无法正确连接SignalR Service。
2. 是否必须通过Azure Function中转?
是的。无服务器模式下的Azure SignalR Service依赖Azure Function完成身份验证、连接管理和消息路由,不允许客户端直接建立连接,直接连接会返回403错误。所有客户端必须通过Function的协商端点获取合法的连接凭证后,才能与SignalR Service建立连接。
修复建议
- 统一客户端监听方法与Function定义的客户端方法名称、参数类型(如示例代码中修正的
connection.On<NewMessage>("newMessage", ...))。 - 修正Function代码中的语法错误,确保编译通过。
- 统一所有SignalR相关配置的连接字符串名称,避免配置不一致。
- 确保Azure Function的系统分配身份已被授予SignalR Service的SignalR App Server角色,否则Function无法与SignalR Service交互。
内容的提问来源于stack exchange,提问作者Pultea Robert
相关产品推荐
相关产品推荐

