如何使用 IdentityServer 4 验证我的 signalR 客户端

问题描述 投票:0回答:1

在我的客户端中,我使用此代码来获取令牌,“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
{
//....
}
c# asp.net-core authentication signalr identityserver4
1个回答
0
投票

从存储库中,我们可以知道您的 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();
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.