2020年12月14日 星期一

在 SignalR 裡使用 .Net Core 內建的 JWT Token 機制來驗證連線

前幾天使用了 SignalR 設計出了一款史萊姆賽跑遊戲(如下圖),今天想要進階一點,想說不知道 SignalR 支不支援客戶端連線的驗證,查了一下微軟官方文件,欸嘿~果然也是可以的。不論是使用 Cookie 驗證的方式或是 Token 為基礎驗證的方式都行的通。所以這篇就筆記一下我如何使用 JWT Token 的方式來達成客戶端連線的驗證,若對 .Net Core 內建的 JWT Token 驗證機制還不太了解的讀者,可以先參考保哥這篇文章



1. 設定 Server 可透過 URL 查詢參數來驗證 JWT Token

當 SignalR 使用 WebSocketsServer-Sent Events 的傳輸型態時,SignalR 會以 URL 參數的方式來將 Token 傳遞給伺服器,因此,我們要先在專案的 Startup.cs 的 ConfigureServices 方法裡加上以下程式碼(這邊只列出主要的參數設定)

public void ConfigureServices(IServiceCollection services)
{
    /* 省略不相關的程式碼 */

    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            // NameClaimType 和 RoleClaimType 需與建立 Token 中的 ClaimType 一致,否則會造成 User.Identity.Name(或Roles)為 null
            NameClaimType = ClaimTypes.NameIdentifier,
            RoleClaimType = ClaimTypes.Role,

            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("Your Signing Key")),
            .....
            /* 省略不相關的程式碼 */
        };

        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                // SignalR 會將 Token 以參數名稱 access_token 的方式放在 URL 查詢參數裡
                var accessToken = context.Request.Query["access_token"];

                // 連線網址為 Hubs 相關路徑才檢查
                var path = context.HttpContext.Request.Path;
                if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
                {
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
         };
    });

    services.AddSignalR();

    /* 省略不相關的程式碼 */
}


2. 設定客戶端(Client)在建立連線時,附上 Token 給伺服端(Server)做驗證

客戶端為 Javascript 時

this.connection = new signalR.HubConnectionBuilder()
                .withUrl("/hubs/myhub", { accessTokenFactory: () => "Your JWT Token" })    // e.g. () => "eyJhbGciOiJIUzI1NiIsInR5cCI6I......"  
                .build();

客戶端為 C# 時

var connection = new HubConnectionBuilder()
               .WithUrl("https://example.com/hubs/myhub", options =>
               { 
                   options.AccessTokenProvider = () => Task.FromResult("Your JWT Token");  // e.g. () => Task.FromResult("eyJhbGciOiJIUzI1NiIsInR5cCI6I......")  
               })
               .Build();

當使用 Javascript 時,可以透過 Chrome 瀏覽器的開發者工具看到 SignalR 確實會將我們置放的 Token 透過網址參數access_token傳遞給後端

Image


3. 在 Hub 裡正確取得用戶名稱即完成

這邊以覆寫從 Hub 繼承來的 OnConnectedAsync 方法為例子

using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;

namespace MyWeb.Hubs
{
    [Authorize]
    public class MyHub : Hub
    {
        public override Task OnConnectedAsync()
        {
            //取得用戶名稱
            string name = Context.User.Identity.Name;

            /* 執行你拿到用戶名稱後想幹的任何事XD */

            return base.OnConnectedAsync();
        }

        /* 其它程式碼... */
    }
}



補充: 在 Stackoverflow 上看到另一種截取 Token 的方式

使用此種方式可移除第一步驟裡 OnMessageReceived 這段程式碼

services.AddAuthentication(options =>
{
    /* 省略程式碼... */  
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        /* 省略程式碼... */
    };

    options.Events = new JwtBearerEvents
    {
        /* 以下這整段可拿掉 */
        // OnMessageReceived = context =>
        // {
        //     // SignalR 會將 Token 以參數名稱 access_token 的方式放在 URL 查詢參數裡
        //     var accessToken = context.Request.Query["access_token"];

        //     // 連線網址為 Hubs 相關路徑才檢查
        //     var path = context.HttpContext.Request.Path;
        //     if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
        //     {
        //         context.Token = accessToken;
        //     }
        //     return Task.CompletedTask;
        // }
    };      
});

接著在專案的 Startup.cs 的 Configure 方法裡加上以下程式碼

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    /* 省略其它程式碼... */

    // 加上這一區段
    app.Use(async (context, next) =>
    {
        if (context.Request.Path.Value.StartsWith("/hubs"))
        {
            var bearerToken = context.Request.Query["access_token"].ToString();

            if (!String.IsNullOrEmpty(bearerToken))
              context.Request.Headers.Add("Authorization", "Bearer " + bearerToken);
        }

        await next();
    });

    app.UseAuthentication();

    app.UseAuthorization();

    /* 省略其它程式碼... */
}  




參考資料

[Microsoft] Authentication and authorization in ASP.NET Core SignalR

[Microsoft] ASP.NET Core SignalR configuration

[The Will Will Web] 如何在 ASP.NET Core 3 使用 Token-based 身分驗證與授權

訪客統計

103155