У меня есть .Net Core Web Application (используется .Net Core 2.2.0-preview3 и SignalR 1.0.4). Это приложение содержит концентратор для получения сообщений. Также у меня есть клиентское консольное приложение .Net, которое транслирует сообщения.
Без использования и аутентификации. Приложения работают отлично. Затем я добавил стандартную аутентификацию в мое веб-приложение и использовал атрибут [Authorize] на моем контроллере, который получает сообщения. После этого, когда я запускаю приложение, я вижу ниже проблемы;
- Соединения с веб-сокетами больше не работают
- Обычно клиентское приложение .net выдает 401 несанкционированную ошибку
Я не могу решить первую проблему, не могли бы вы помочь мне с этим? В любом случае, после этого мне нужно добавить аутентификацию токена Jwt в мое клиентское приложение .net. После внесите необходимые изменения. Я решил свою вторую проблему, но на этот раз я столкнулся с новой проблемой;
Я не могу войти в свое веб-приложение (или после входа в систему я все еще вижу ссылки регистрации и входа в верхнем правом углу вместо адреса электронной почты пользователя и выхода из системы). И до сих пор не может решить. Не могли бы вы помочь мне и в этом? Не знаю, могу ли я загрузить пример проекта здесь, но думаю, что нет. Из-за этого я вставляю необходимые части кода ниже;
веб-приложение
chat.js
function createLog(clientId) {
var log = document.getElementById('log');
var ul = document.createElement('ul');
ul.id = 'log' + clientId;
log.appendChild(ul);
}
function appendLog(clientId, entry) {
var listId = document.getElementById('log' + clientId);
if (listId.children.length > 11) {
listId.removeChild(listId.children[1]);
}
var child = document.createElement('li');
child.innerText = entry;
listId.appendChild(child);
}
function get(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.send();
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response || xhr.responseText);
}
else {
reject(new Error(xhr.statusText));
}
};
xhr.onerror = function () {
reject(new Error(xhr.statusText));
};
});
}
var tokens = {};
function refreshToken(clientId) {
var tokenUrl = 'http://' + document.location.host + '/generatetoken?user=' + clientId;
return get(tokenUrl)
.then(function (token) {
tokens[clientId] = token;
});
}
function runConnection(clientId, transportType) {
var connection;
refreshToken(clientId)
.then(function () {
var options = {
transport: transportType,
accessTokenFactory: function () { return tokens[clientId]; }
};
connection = new signalR.HubConnectionBuilder()
.withUrl("/broadcast", options)
.configureLogging(signalR.LogLevel.Information)
.build();
connection.on('Message', function (from, message) {
appendLog(clientId, from + ': ' + message);
});
return connection.start();
})
.then(function () {
appendLog(clientId, 'user ' + clientId + ' connected');
setInterval(function () {
appendLog(clientId, 'Refreshing token');
refreshToken(clientId);
}, 20000);
setTimeout(function sendMessage() {
connection.send('broadcast', clientId, 'Hello at ' + new Date().toLocaleString());
var timeout = 2000 + Math.random() * 4000;
setTimeout(sendMessage, timeout);
});
})
.catch(function (e) {
appendLog(clientId, 'Could not start connection');
});
}
[signalR.HttpTransportType.WebSockets, signalR.HttpTransportType.ServerSentEvents, signalR.HttpTransportType.LongPolling].forEach(function (transportType) {
var clientId = 'browser ' + signalR.HttpTransportType[transportType];
createLog(clientId);
appendLog(clientId, 'Log for user: ' + clientId);
runConnection(clientId, transportType);
});
ChatController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ChatServer3.Controllers
{
[Authorize]
public class ChatController : Controller
{
public IActionResult Index()
{
return View();
}
}
}
Broadcaster.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace ChatServer3.Hubs
{
[Authorize(JwtBearerDefaults.AuthenticationScheme)]
public class Broadcaster : Hub
{
public Task Broadcast(string sender, string message) => Clients.All.SendAsync("Message", sender, message);
}
}
Index.html
@{
ViewData["Title"] = "Chat Page";
}
<h1>Chat</h1>
<div id="log">
</div>
<script type="text/javascript" src="~/lib/signalr/dist/browser/signalr.js"></script>
<script type="text/javascript" src="~/js/chat.js"></script>
Program.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace ChatServer3
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureLogging(factory =>
{
factory.AddConsole();
factory.AddFilter("Console", level => level >= LogLevel.Information);
factory.AddDebug();
})
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>();
}
}
Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ChatServer3.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.Security.Claims;
using Microsoft.AspNetCore.Routing;
using ChatServer3.Hubs;
namespace ChatServer3
{
public class Startup
{
private readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray());
private readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler();
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSignalR();
services.AddAuthorization(options =>
{
options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim(ClaimTypes.NameIdentifier);
});
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters =
new TokenValidationParameters
{
LifetimeValidator = (before, expires, token, parameters) => expires > DateTime.UtcNow,
ValidateAudience = false,
ValidateIssuer = false,
ValidateActor = false,
ValidateLifetime = true,
IssuerSigningKey = SecurityKey
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken) &&
(context.HttpContext.WebSockets.IsWebSocketRequest || context.Request.Headers["Accept"] == "text/event-stream"))
{
context.Token = context.Request.Query["access_token"];
}
return Task.CompletedTask;
}
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
app.UseFileServer();
app.UseSignalR(options => options.MapHub<Broadcaster>("/broadcast"));
var routeBuilder = new RouteBuilder(app);
routeBuilder.MapGet("generatetoken", c => c.Response.WriteAsync(GenerateToken(c)));
app.UseRouter(routeBuilder.Build());
}
private string GenerateToken(HttpContext httpContext)
{
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, httpContext.Request.Query["user"]) };
var credentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken("SignalRTestServer", "SignalRTests", claims, expires: DateTime.UtcNow.AddSeconds(30), signingCredentials: credentials);
return JwtTokenHandler.WriteToken(token);
}
}
}
ПРИМЕНЕНИЕ КОНСОЛИ
Program.cs
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Collections.Concurrent;
using System.Net.Http;
using System.Threading.Tasks;
namespace ChatClient
{
class Program
{
static async Task Main(string[] args)
{
var app = new Program();
await Task.WhenAll(
app.RunConnection(HttpTransportType.WebSockets),
app.RunConnection(HttpTransportType.ServerSentEvents),
app.RunConnection(HttpTransportType.LongPolling));
}
private const string ServerUrl = "http://localhost:8719";
private readonly ConcurrentDictionary<string, Task<string>> _tokens = new ConcurrentDictionary<string, Task<string>>(StringComparer.Ordinal);
private readonly Random _random = new Random();
private async Task RunConnection(HttpTransportType transportType)
{
var userId = "C#" + transportType;
_tokens[userId] = GetJwtToken(userId);
var hubConnection = new HubConnectionBuilder()
.WithUrl(ServerUrl + "/broadcast", options =>
{
options.Transports = transportType;
options.AccessTokenProvider = () => _tokens[userId];
})
.Build();
var closedTcs = new TaskCompletionSource<object>();
hubConnection.Closed += e =>
{
closedTcs.SetResult(null);
return Task.CompletedTask;
};
hubConnection.On<string, string>("Message", (sender, message) => Console.WriteLine($"[{userId}] {sender}: {message}"));
await hubConnection.StartAsync();
Console.WriteLine($"[{userId}] Connection Started");
var ticks = 0;
var nextMsgAt = 3;
try
{
while (!closedTcs.Task.IsCompleted)
{
await Task.Delay(1000);
ticks++;
if (ticks % 15 == 0)
{
// no need to refresh the token for websockets
if (transportType != HttpTransportType.WebSockets)
{
_tokens[userId] = GetJwtToken(userId);
Console.WriteLine($"[{userId}] Token refreshed");
}
}
if (ticks % nextMsgAt == 0)
{
await hubConnection.SendAsync("Broadcast", userId, $"Hello at {DateTime.Now.ToString()}");
nextMsgAt = _random.Next(2, 5);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[{userId}] Connection terminated with error: {ex}");
}
}
private async Task<string> GetJwtToken(string userId)
{
var httpResponse = await new HttpClient().GetAsync(ServerUrl + $"/generatetoken?user={userId}");
httpResponse.EnsureSuccessStatusCode();
return await httpResponse.Content.ReadAsStringAsync();
}
}
}