[ASP.NET Core]WebSocket 사용해보기

29516 단어 charpaspnetcore

소개



이번에는 WebSocket 애플리케이션에 몇 가지 기능을 추가해 보겠습니다.


  • 환경


  • .NET 버전 6.0.202
  • NLog.Web.AspNetCore 버전 4.14.0
  • Microsoft.EntityFrameworkCore 버전 6.0.4
  • Microsoft.EntityFrameworkCore.Design 버전 6.0.4
  • Npgsql.EntityFrameworkCore.PostgreSQL 버전 6.0.3
  • Microsoft.AspNetCore.Identity.EntityFrameworkCore 버전 6.0.4
  • Microsoft.AspNetCore.Authentication.JwtBearer 버전 6.0.4
  • Node.js 버전 17.9.0
  • TypeScript 버전 4.6.3
  • ws ver.7.4.0
  • 웹팩 버전 5.70.0

  • 입증



    WebSocket에는 인증에 관한 사양이 없습니다.
    많은 샘플이 쿠키, 세션 또는 URL 매개변수로 토큰을 추가하여 인증하는 것을 발견했습니다.


  • 이번에는 세션에 JWT를 추가합니다.



  • Program.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에 연결한 후에도 유지됩니다.

    WebSocketHolder.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}");
                }
            }
        }
    }
    


    한 가지 중요한 것은 WebSocket 연결을 닫을 때까지 세션 값, 쿠키 값 및 로그인 상태가 유지된다는 것입니다.

    자원


  • RFC6455: The WebSocket Protocol
  • WebSocket - Web APIs|MDN
  • 좋은 웹페이지 즐겨찾기