##**1. 設定 Server 可透過 URL 查詢參數來驗證 JWT Token**
當 SignalR 使用 WebSockets 和 Server-Sent Events 的傳輸型態時,SignalR 會以 URL 參數的方式來將 Token 傳遞給伺服器,因此,我們要先在專案的 Startup.cs 的 `ConfigureServices` 方法裡加上以下程式碼(這邊只列出主要的參數設定)
``` c#
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 時 ``` jacascript this.connection = new signalR.HubConnectionBuilder() .withUrl("/hubs/myhub", { accessTokenFactory: () => "Your JWT Token" }) // e.g. () => "eyJhbGciOiJIUzI1NiIsInR5cCI6I......" .build(); ``` 客戶端為 C# 時 ``` 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](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgFYmR8bsUgu4mD7tSMe_dietrPrHA_OfsUPH5aP0Y65d5zlTrx6GQU_X0ExtLiYYr5I2ltc-ZyKhP9unZfRPQxpgyPlJzBuc0vOtgVPujKCoea8H0U2NsPsLCnHjqbW5qpHQqgoQrWbq_C/s782/sigr.jpg)
##**3. 在 Hub 裡正確取得用戶名稱即完成** 這邊以覆寫從 Hub 繼承來的 `OnConnectedAsync` 方法為例子 ``` c# 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](https://stackoverflow.com/a/59466828) 上看到另一種截取 Token 的方式 使用此種方式可移除第一步驟裡 `OnMessageReceived` 這段程式碼 ```c# 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` 方法裡加上以下程式碼 ```c# 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](https://docs.microsoft.com/en-gb/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1) [\[Microsoft\] ASP.NET Core SignalR configuration](https://docs.microsoft.com/en-gb/aspnet/core/signalr/configuration?view=aspnetcore-3.1&tabs=dotnet#configure-bearer-authentication-1) [\[The Will Will Web\] 如何在 ASP.NET Core 3 使用 Token-based 身分驗證與授權](https://blog.miniasp.com/post/2019/12/16/How-to-use-JWT-token-based-auth-in-aspnet-core-31)
##**2. 設定客戶端(Client)在建立連線時,附上 Token 給伺服端(Server)做驗證** 客戶端為 Javascript 時 ``` jacascript this.connection = new signalR.HubConnectionBuilder() .withUrl("/hubs/myhub", { accessTokenFactory: () => "Your JWT Token" }) // e.g. () => "eyJhbGciOiJIUzI1NiIsInR5cCI6I......" .build(); ``` 客戶端為 C# 時 ``` 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](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgFYmR8bsUgu4mD7tSMe_dietrPrHA_OfsUPH5aP0Y65d5zlTrx6GQU_X0ExtLiYYr5I2ltc-ZyKhP9unZfRPQxpgyPlJzBuc0vOtgVPujKCoea8H0U2NsPsLCnHjqbW5qpHQqgoQrWbq_C/s782/sigr.jpg)
##**3. 在 Hub 裡正確取得用戶名稱即完成** 這邊以覆寫從 Hub 繼承來的 `OnConnectedAsync` 方法為例子 ``` c# 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](https://stackoverflow.com/a/59466828) 上看到另一種截取 Token 的方式 使用此種方式可移除第一步驟裡 `OnMessageReceived` 這段程式碼 ```c# 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` 方法裡加上以下程式碼 ```c# 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](https://docs.microsoft.com/en-gb/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1) [\[Microsoft\] ASP.NET Core SignalR configuration](https://docs.microsoft.com/en-gb/aspnet/core/signalr/configuration?view=aspnetcore-3.1&tabs=dotnet#configure-bearer-authentication-1) [\[The Will Will Web\] 如何在 ASP.NET Core 3 使用 Token-based 身分驗證與授權](https://blog.miniasp.com/post/2019/12/16/How-to-use-JWT-token-based-auth-in-aspnet-core-31)