[ ASP . NETコア]ウェブソケットを試してください


イントロ


今回は、WebSocketアプリケーションにいくつかの機能を追加してみます.

  • 環境

  • .ネットバージョン.6.0.202
  • Nlogウェブアスペルタルver .4.14.0
  • マイクロソフト.EntityFrameworkCoreバージョン.6.0.4
  • マイクロソフト.EntityFrameworkCore.デザインバージョン.6.0.4
  • NPGSQL.EntityFrameworkCore.PostgreSQLバージョン.6.0.3
  • マイクロソフト.アスピネット.アイデンティティ.EntityFrameworkCoreバージョン.6.0.4
  • マイクロソフト.アスピネット.認証.JWTearerバージョン.6.0.4
  • ノード.jsバージョン.17.9.0
  • タイプスクリプトver .4.6.3
  • バージョンver .7.4.0
  • バージョンアップ.5.70.0
  • 認証


    WebSocket認証に関する仕様はありません.
    私は多くのサンプルがクッキー、セッションまたは追加トークンをURLパラメータとして認証することを発見しました.

  • 今回はJWTをセッションに追加します.


  • プログラム。cs


    using System.Net;
    using System.Text;
    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.IdentityModel.Tokens;
    using NLog.Web;
    using WebRtcSample.Models;
    using WebRtcSample.Users;
    using WebRtcSample.Users.Repositories;
    using WebRtcSample.WebSockets;
    
    var logger = NLogBuilder.ConfigureNLog(Path.Combine(Directory.GetCurrentDirectory(), "Nlog.config"))
        .GetCurrentClassLogger();
    try 
    {
        var builder = WebApplication.CreateBuilder(args);
        builder.WebHost.UseUrls("http://0.0.0.0:5027");
        builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = builder.Configuration["Jwt:Issuer"],
                    ValidAudience = builder.Configuration["Jwt:Audience"],
                    ClockSkew = TimeSpan.FromSeconds(30),
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
                };
            });
        builder.Services.AddSession(options => {
            options.IdleTimeout = TimeSpan.FromSeconds(30);
            options.Cookie.HttpOnly = true;
            options.Cookie.IsEssential = true;
            options.Cookie.SameSite = SameSiteMode.Strict;
        });
        builder.Services.AddRazorPages();
        builder.Services.AddControllers();
        builder.Services.AddHttpContextAccessor();
        builder.Services.AddDbContext<SampleContext>(options =>
        {
            options.EnableSensitiveDataLogging();
            options.UseNpgsql(builder.Configuration["DbConnection"]);
        });
        builder.Services.AddIdentity<ApplicationUser, IdentityRole<int>>()
                    .AddUserStore<ApplicationUserStore>()
                    .AddEntityFrameworkStores<SampleContext>()
                    .AddDefaultTokenProviders();
        builder.Services.AddSingleton<IWebSocketHolder, WebSocketHolder>();
        builder.Services.AddScoped<IApplicationUserService, ApplicationUserService>();
        builder.Services.AddScoped<IUserTokens, UserTokens>();
        builder.Services.AddScoped<IApplicationUsers, ApplicationUsers>();
        var app = builder.Build();
        app.UseSession();
        // this line must be executed before UseRouting().
        app.Use(async (context, next) =>
        {
            var token = context.Session.GetString("user-token");
            if(string.IsNullOrEmpty(token) == false)
            {            
                context.Request.Headers.Add("Authorization", $"Bearer {token}");
            }
            await next();
        });
        app.UseStaticFiles();
        app.UseWebSockets();
        app.UseStatusCodePages(async context =>
        {
            if (context.HttpContext.Response.StatusCode == (int)HttpStatusCode.Unauthorized)
            {
                if(context.HttpContext.Request.Path.StartsWithSegments("/") ||
                    context.HttpContext.Request.Path.StartsWithSegments("/pages"))
                {
                    context.HttpContext.Response.Redirect("/pages/signin");
                    return;
                }
            }
            await context.Next(context.HttpContext);
        });
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        // this line must be executed after setting tokens and authentications. 
        app.MapWebSocketHolder("/ws");
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
        app.Run();
    }
    catch (Exception ex) {
        logger.Error(ex, "Stopped program because of exception");
        throw;
    }
    finally {
        NLog.LogManager.Shutdown();
    }
    
    セッション値はWebSocket接続後に保持されます.

    WebSocketThermal。cs


    using System.Collections.Concurrent;
    using System.Net.WebSockets;
    
    namespace WebRtcSample.WebSockets
    {
        public class WebSocketHolder: IWebSocketHolder
        {
            private readonly ILogger<WebSocketHolder> logger;
            private readonly IHttpContextAccessor httpContext;
            private readonly ConcurrentDictionary<string, WebSocket> clients = new ();
            private CancellationTokenSource source = new ();
            public WebSocketHolder(ILogger<WebSocketHolder> logger,
                IHostApplicationLifetime applicationLifetime,
                IHttpContextAccessor httpContext)
            {
                this.logger = logger;
                applicationLifetime.ApplicationStopping.Register(OnShutdown);
                this.httpContext = httpContext;   
            }
            private void OnShutdown()
            {
                source.Cancel();
            }
            public async Task AddAsync(HttpContext context)
            {
                WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
                if(clients.TryAdd(CreateId(), webSocket))
                {
                    await EchoAsync(webSocket);
                }
            }
            private string CreateId()
            {
                return Guid.NewGuid().ToString();
            }
            private async Task EchoAsync(WebSocket webSocket)
            {
                try
                {
                    // for sending data
                    byte[] buffer = new byte[1024 * 4];
                    while(true)
                    {
                        string? token = this.httpContext.HttpContext?.Session?.GetString("user-token");
                        string? userId = this.httpContext.HttpContext?.User?.Identity?.Name;
                        bool? authenticated = this.httpContext.HttpContext?.User?.Identity?.IsAuthenticated;
    
                        logger.LogDebug($"Echo Token: {token} User: {userId} auth?: {authenticated}");
    
                        WebSocketReceiveResult result = await webSocket.ReceiveAsync(
                            new ArraySegment<byte>(buffer), source.Token);
                        if(result.CloseStatus.HasValue)
                        {
                            await webSocket.CloseAsync(result.CloseStatus.Value,
                                result.CloseStatusDescription, source.Token);
                            clients.TryRemove(clients.First(w => w.Value == webSocket));
                            webSocket.Dispose();
                            break;
                        }
                        // Send to all clients
                        foreach(var c in clients)
                        {
                            if(c.Value == webSocket)
                            {
                                continue;
                            }
                            await c.Value.SendAsync(
                                new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType,
                                    result.EndOfMessage, source.Token);
                        }
                    }
                }
                catch(OperationCanceledException ex)
                {
                    logger.LogError($"Exception {ex.Message}");
                }
            }
        }
    }
    
    つの重要なことは、セッション値、クッキー値、およびステータスの署名は、ウェブソケット接続を閉じるまで保持されます.

    資源

  • RFC6455: The WebSocket Protocol
  • WebSocket - Web APIs|MDN