在我的客户端中,我使用此代码来获取令牌,“http://localhost:5006”是我的IdentityServer(权限)
var httpClient = new HttpClient();
var discoveryDocument = httpClient.GetDiscoveryDocumentAsync("http://localhost:5006").Result;
var tokenResponse = httpClient.RequestClientCredentialsTokenAsync(
new ClientCredentialsTokenRequest
{
Address = discoveryDocument.TokenEndpoint,
ClientId = "client",
ClientSecret = "Prevo100",
Scope = "prevo100-api"
}).Result;
我的 signalR 中心是 http://localhost:5119/Prevo100”,当我运行 StartAsync 时遇到此异常:
System.AggregateException:“发生一个或多个错误。 (回复 状态码不表示成功:403(禁止)。)'
我也可能遇到 401 错误!
var url = "http://localhost:5119/Prevo100";
HubConnection connection = new HubConnectionBuilder()
.WithUrl(url, options =>
{
options.AccessTokenProvider = () => Task.FromResult(tokenResponse.AccessToken);
})
.WithAutomaticReconnect()
.Build();
var client = new Prevo100WebClient(connection);
connection.StartAsync().Wait(); // <== Exception here
在 SignalR 服务器(集线器)中,我使用以下代码:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "http://localhost:5006";
options.RequireHttpsMetadata = false;
options.Audience = "prevo100-api";
options.TokenValidationParameters =
new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateAudience = false
};
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
// Sending the access token in the query string is required when using WebSockets or ServerSentEvents
// due to a limitation in Browser APIs. We restrict it to only calls to the
// SignalR hub in this code.
// See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
// for more information about security considerations when using
// the query string to transmit the access token.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/Prevo100"))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
services.AddSignalR(hubOptions =>
{
//hubOptions.ClientTimeoutInterval // 30 secondes par défaut
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseFileServer();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<Prevo100Hub>("/Prevo100");
});
}
我添加了一个属性 hub 类:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class Prevo100Hub : Hub<IPrevo100Client>, IHubContract
{
//....
}
从存储库中,我们可以知道您的 JwtSample 项目中有一个 Broadcaster hub(网址为
/broadcast
)。
而且您还有一个广播中心(网址为 /Prevo100
)。
所有客户端都应该从 SignalRIdentityServerServer 中的 api (
/generatetoken
) 获取令牌。
我不知道为什么当我像下面这样设置启动时,SignalRIdentityServerClient 不能很好地工作。
测试结果
但是当我像下面这样设置启动时,SignalRIdentityServerClient 工作得很好。
测试结果
我通常喜欢在身份服务器应用程序中移动可以生成令牌的方法。
这是示例代码,我已经更改了它并且运行良好。
SignalRIdentityServerServer - Startup.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
namespace SignalRIdentityServerServer
{
public class Startup
{
//private readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(RandomNumberGenerator.GetBytes(32));
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.AddSignalR();
services.AddIdentityServer()
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(Config.Clients)
.AddInMemoryApiScopes(Config.Scopes)
.AddDeveloperSigningCredential();
services.AddAuthorization(options =>
{
options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim(ClaimTypes.NameIdentifier);
});
});
// Add services to the container.
services.AddCors(options => options.AddPolicy("CorsPolicy", builder =>
{
builder.AllowAnyMethod()
.SetIsOriginAllowed(_ => true)
.AllowAnyHeader()
.AllowCredentials();
}));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
var secretByte = Encoding.UTF8.GetBytes("Authentication:SecretKey");
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "SignalRTestServer",
ValidateAudience = true,
ValidAudience = "SignalRTests",
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(secretByte),
ClockSkew = TimeSpan.Zero
};
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
// Sending the access token in the query string is required when using WebSockets or ServerSentEvents
// due to a limitation in Browser APIs. We restrict it to only calls to the
// SignalR hub in this code.
// See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
// for more information about security considerations when using
// the query string to transmit the access token.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
/*var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(context.HttpContext.WebSockets.IsWebSocketRequest ||
context.Request.Headers["Accept"] == "text/event-stream")
//&& path.StartsWithSegments("/Prevo100")
)
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;*/
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) /*&&
path.StartsWithSegments("/Prevo100")*/)
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseCors("CorsPolicy");
app.UseFileServer();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseIdentityServer();
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<Broadcaster>("/Prevo100");
endpoints.MapGet("/generatetoken", context =>
{
return context.Response.WriteAsync(GenerateToken(context));
});
});
}
private string GenerateToken(HttpContext httpContext)
{
var singningAlgorithm = SecurityAlgorithms.HmacSha256;
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, httpContext.Request.Query["user"])
};
var secretByte = Encoding.UTF8.GetBytes("Authentication:SecretKey");
var signingkey = new SymmetricSecurityKey(secretByte);
var signingCredentials = new SigningCredentials(signingkey, singningAlgorithm);
var token = new JwtSecurityToken(
issuer: "SignalRTestServer",
audience: "SignalRTests",
claims,
notBefore: DateTime.Now,
expires: DateTime.Now.AddMinutes(3),
signingCredentials
);
//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);
var tokenStr = new JwtSecurityTokenHandler().WriteToken(token);
return tokenStr;
}
}
}
JwtSample - Startup.cs
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
namespace JwtSample;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
});
services.AddControllers();
services.AddAuthorization(options =>
{
options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim(ClaimTypes.NameIdentifier);
});
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "http://localhost:5155";
options.RequireHttpsMetadata = false;
options.Audience = "prevo100-api";
options.TokenValidationParameters = new TokenValidationParameters
{
//ValidateAudience = true,
//ValidateIssuer = true
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = "SignalRTestServer",
ValidAudience = "SignalRTests",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("Authentication:SecretKey"))
};
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 = accessToken;
}
return Task.CompletedTask;
}
};
});
}
public void Configure(IApplicationBuilder app)
{
app.UseFileServer();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseWebSockets();
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<Broadcaster>("/broadcast");
endpoints.MapControllers();
});
}
}
JwtSample -index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>SignalR JWT Sample</title>
</head>
<body>
<div id="log">
</div>
</body>
</html>
<script type="text/javascript" src="lib/signalr-client/signalr.js"></script>
<script>
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((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.send();
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response || xhr.responseText);
}
else {
reject(new Error(xhr.statusText));
}
};
xhr.onerror = () => {
reject(new Error(xhr.statusText));
}
});
}
var tokens = {};
function refreshToken(clientId) {
console.log("clientId: " + clientId);
//var tokenUrl = 'http://' + document.location.host + '/generatetoken?user=' + clientId;
var tokenUrl = 'http://localhost:5155/generatetoken?user=' + clientId;
return get(tokenUrl)
.then(function (token) {
tokens[clientId] = token;
});
}
function runConnection(clientId, transportType) {
var connection;
console.log("clientId: " + clientId);
refreshToken(clientId)
.then(function () {
var options = {
transport: transportType,
accessTokenFactory: function () { return tokens[clientId]; }
};
console.log(transportType)
console.log("tokens: " + tokens[clientId]);
connection = new signalR.HubConnectionBuilder()
.withUrl("/broadcast", { accessTokenFactory: () => tokens[clientId] })
.configureLogging(signalR.LogLevel.Debug)
.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];
console.log("transportType: " + signalR.HttpTransportType[transportType]);
createLog(clientId);
appendLog(clientId, 'Log for user: ' + clientId);
runConnection(clientId, transportType);
});
</script>
JwtClientSample - Program.cs
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
using System.Net.Http;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.SignalR.Client;
namespace JwtClientSample;
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:54543";
private const string ID4ServerUrl = "http://localhost:5155";
private readonly ConcurrentDictionary<string, Task<string>> _tokens = new ConcurrentDictionary<string, Task<string>>(StringComparer.Ordinal);
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();
hubConnection.Closed += e =>
{
closedTcs.SetResult();
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}");
nextMsgAt = Random.Shared.Next(2, 5);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[{userId}] Connection terminated with error: {ex}");
}
}
private static async Task<string> GetJwtToken(string userId)
{
var httpResponse = await new HttpClient().GetAsync(ID4ServerUrl + $"/generatetoken?user={userId}");
httpResponse.EnsureSuccessStatusCode();
return await httpResponse.Content.ReadAsStringAsync();
}
}
SignalRIdentityServerClient - Program.cs
using IdentityModel.Client;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.SignalR.Client;
using SignalRIdentityServerShared;
using System.Collections.Concurrent;
using System.Data.Common;
using TypedSignalR.Client;
namespace SignalRIdentityServerClient
{
public class WebClient : IClient, IHubConnectionObserver, IDisposable
{
private readonly IHubContract _hub;
private readonly IDisposable _subscription;
private readonly CancellationTokenSource _cancellationTokenSource = new();
public WebClient(HubConnection connection)
{
_hub = connection.CreateHubProxy<IHubContract>(_cancellationTokenSource.Token);
_subscription = connection.Register<IClient>(this);
}
Task IClient.ReceiveMessage(string sender, string message)
{
Console.WriteLine("[{0}] {1}", sender, message);
return Task.CompletedTask;
}
public Task OnClosed(Exception e)
{
Console.WriteLine($"[On Closed!]");
return Task.CompletedTask;
}
public Task OnReconnected(string connectionId)
{
Console.WriteLine($"[On Reconnected!]");
return Task.CompletedTask;
}
public Task OnReconnecting(Exception exception)
{
Console.WriteLine($"[On Reconnecting!]");
return Task.CompletedTask;
}
public Task RequestBroadcast()
{
return _hub.Broadcast("Client", "Hello Server !");
}
public void Dispose()
{
_subscription?.Dispose();
}
}
public class Program
{
private static readonly ConcurrentDictionary<string, Task<string>> _tokens = new ConcurrentDictionary<string, Task<string>>(StringComparer.Ordinal);
private const string ID4ServerUrl = "http://localhost:5155";
static async Task Main(string[] args)
{
var httpClient = new HttpClient();
//var discoveryDocument = httpClient.GetDiscoveryDocumentAsync("http://localhost:5155").Result;
//var tokenResponse = httpClient.RequestClientCredentialsTokenAsync(
// new ClientCredentialsTokenRequest
// {
// Address = discoveryDocument.TokenEndpoint,
// ClientId = "client",
// ClientSecret = "Prevo100",
// Scope = "prevo100-api"
// }).Result;
//Console.WriteLine($"Token : {tokenResponse.AccessToken}");
var userId = "C#" + HttpTransportType.WebSockets;
_tokens[userId] = GetJwtToken(userId);
var url = "http://localhost:5155/Prevo100";
HubConnection connection = new HubConnectionBuilder()
.WithUrl(url, options =>
{
options.Transports = HttpTransportType.WebSockets;
options.AccessTokenProvider = () => Task.FromResult(_tokens[userId].Result);
})
.WithAutomaticReconnect()
.Build();
var client = new WebClient(connection);
connection.StartAsync().Wait();
client.RequestBroadcast().Wait();
//connection.StopAsync().Wait();
client.Dispose();
}
private static async Task<string> GetJwtToken(string userId)
{
var httpResponse = await new HttpClient().GetAsync(ID4ServerUrl + $"/generatetoken?user={userId}");
httpResponse.EnsureSuccessStatusCode();
return await httpResponse.Content.ReadAsStringAsync();
}
}
}