#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