一、JWT
服務端在respose中設置好cookie,瀏覽器發請求都會自動帶上,不需要做額外設置
但是如果客戶端是非瀏覽器,或者要兼容多種客戶端,這個方法就不行了
Js端
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> </head> <body> <div class="container"> <div class="row"> </div> <div class="row"> <div class="col-6"> </div> <div class="col-6"> User..........<input type="text" id="userInput" /> <br /> Message...<input type="text" id="messageInput" /> <input type="button" id="sendButton" value="Send Message" /> </div> </div> <div class="row"> <div class="col-12"> <hr /> </div> </div> <div class="row"> <div class="col-6"> </div> <div class="col-6"> <ul id="messagesList"></ul> </div> </div> </div> <script src="~/lib/signalr/dist/browser/signalr.js"></script> <script type="text/javascript"> "use strict"; var url = "/chatHub"; //本地站點可以直接寫"/chat" var loginToken = "token"; // JWT驗證碼。不帶Bearer var connection = new signalR.HubConnectionBuilder().withUrl(url, { accessTokenFactory: () => loginToken }).build(); //Disable send button until connection is established document.getElementById("sendButton").disabled = true; connection.on("ReceiveMessage", function (user, message) { var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); var encodedMsg = user + " says " + msg; var li = document.createElement("li"); li.textContent = encodedMsg; document.getElementById("messagesList").appendChild(li); }); connection.start().then(function () { document.getElementById("sendButton").disabled = false; }).catch(function (err) { return console.error(err.toString()); }); document.getElementById("sendButton").addEventListener("click", function (event) { var user = document.getElementById("userInput").value; var message = document.getElementById("messageInput").value; connection.invoke("SendMessage", user, message).catch(function (err) { return console.error(err.toString()); }); event.preventDefault(); }); </script> </body> </html>
Startup.cs配置端
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using System; using System.Text; using System.Threading.Tasks; using test.Hubs; //3、引用 處理客戶端 - 服務器通信的高級管道 namespace test { public class Startup { 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.Configure<CookiePolicyOptions>(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); //JWT令牌配置 services.AddAuthentication(options => { options.DefaultAuthenticateScheme = "JwtBearer"; options.DefaultChallengeScheme = "JwtBearer"; }).AddJwtBearer("JwtBearer", options => { options.Audience = "Audience"; options.TokenValidationParameters = new TokenValidationParameters { // The signing key must match! ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("SecurityKey")), // Validate the JWT Issuer (iss) claim ValidateIssuer = true, ValidIssuer = "Issuer", // Validate the JWT Audience (aud) claim ValidateAudience = true, ValidAudience = "Audience", // Validate the token expiry ValidateLifetime = true, // If you want to allow a certain Account of clock drift, set that here ClockSkew = TimeSpan.Zero }; options.Events = new JwtBearerEvents { OnMessageReceived = (context) => { if (!context.HttpContext.Request.Path.HasValue) { return Task.CompletedTask; } //重點在於這里;判斷是Signalr的路徑 var accessToken = context.HttpContext.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!(string.IsNullOrWhiteSpace(accessToken)) && path.StartsWithSegments("/chatHub")) { context.Token = accessToken; return Task.CompletedTask; } return Task.CompletedTask; } }; }); //1、添加服務 services.AddSignalR(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } //跨域 app.UseCors(builder => { builder.SetIsOriginAllowed(origin => true) .AllowAnyHeader() .WithMethods("GET", "POST") .AllowCredentials(); }); //默認靜態資源路徑wwwroot app.UseStaticFiles(); //默認啟動cookie app.UseCookiePolicy(); //添加授權服務 app.UseAuthentication(); //配置 app.UseSignalR(routes => //2、引用 { //SignalrHub為后台代碼中繼承Hub的類,"/chatHub"為請求路由地址; routes.MapHub<ChatHub>("/chatHub"); }); //啟動MVC app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } } }
使用場景,就是第一次鏈接的時候,傳輸手持令牌token,用於后台綁定Signalr用戶標識(這里第一次握手鏈接肯定要檢驗token,然后映射關系好,每次發送消息,通過此標識,可以指定和識別用戶)
二、實踐
前端代碼:
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> </head> <body> <div class="container"> <div class="row"> </div> <div class="row"> <div class="col-6"> </div> <div class="col-6"> User..........<input type="text" id="userInput" /> <br /> Message...<input type="text" id="messageInput" /> <input type="button" id="sendButton" value="Send Message" /> </div> </div> <div class="row"> <div class="col-12"> <hr /> </div> </div> <div class="row"> <div class="col-6"> </div> <div class="col-6"> <ul id="messagesList"></ul> </div> </div> </div> <script src="~/lib/signalr/dist/browser/signalr.js"></script> <script type="text/javascript"> "use strict"; var url = "/chatHub"; //本地站點可以直接寫"/chat" var loginToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiSm9obiIsImVtYWlsIjoiam9obi5kb2VAYmxpbmtpbmdjYXJldC5jb20iLCJleHAiOjE1NzU1MTUyNzYsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTMxMTEiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUzMTExIn0.jSN25YSXRjDs184SRPdmvBrFzH8-UoGYHorUUE8ttl8"; // JWT驗證碼。不帶Bearer var connection = new signalR.HubConnectionBuilder().withUrl(url, { accessTokenFactory: () => loginToken }).build(); //Disable send button until connection is established document.getElementById("sendButton").disabled = true; connection.on("ReceiveMessage", function (user, message) { var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); var encodedMsg = user + " says " + msg; var li = document.createElement("li"); li.textContent = encodedMsg; document.getElementById("messagesList").appendChild(li); }); connection.start().then(function () { document.getElementById("sendButton").disabled = false; }).catch(function (err) { return console.error(err.toString()); }); document.getElementById("sendButton").addEventListener("click", function (event) { var user = document.getElementById("userInput").value; var message = document.getElementById("messageInput").value; connection.invoke("SendMessage", user, message).catch(function (err) { return console.error(err.toString()); }); event.preventDefault(); }); </script> </body> </html>
appsettings.json(Jwt驗證Key)
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"Jwt": {
"Key": "veryVerySecretKey",
"Issuer": "http://localhost:53111"
},
"AllowedHosts": "*"
}
Startup.cs配置端
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using System; using System.Text; using System.Threading.Tasks; using test.Hubs; //3、引用 處理客戶端 - 服務器通信的高級管道 namespace test { public class Startup { 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.Configure<CookiePolicyOptions>(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); //JWT令牌配置 services.AddAuthentication(options => //默認cookie 或者Bearer { options.DefaultAuthenticateScheme = "JwtBearer";//schemes 字意:方案即自定義方案,那么authorize也去指定JwtBearer options.DefaultChallengeScheme = "JwtBearer"; }).AddJwtBearer("JwtBearer", options => { options.Audience = "Audience"; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true,//是否驗證Issuer ValidateAudience = true,//是否驗證Audience ValidateLifetime = true,//是否驗證失效時間 ValidateIssuerSigningKey = true,//是否驗證SecurityKey ValidIssuer = Configuration["Jwt:Issuer"],//appsettings.json文件中定義的Issuer ValidAudience = Configuration["Jwt:Issuer"],//appsettings.json文件中定義的Audience IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])) }; options.Events = new JwtBearerEvents { OnMessageReceived = (context) => { if (!context.HttpContext.Request.Path.HasValue) { return Task.CompletedTask; } //重點在於這里;判斷是Signalr的路徑 var accessToken = context.HttpContext.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!(string.IsNullOrWhiteSpace(accessToken)) && path.StartsWithSegments("/chatHub")) { context.Token = accessToken; return Task.CompletedTask; } return Task.CompletedTask; }, //此處為權限驗證失敗后觸發的事件 OnChallenge = context => { //此處代碼為終止.Net Core默認的返回類型和數據結果,這個很重要哦,必須 context.HandleResponse(); //自定義自己想要返回的數據結果,我這里要返回的是Json對象,通過引用Newtonsoft.Json庫進行轉換 var payload = new { StatusCode = 0, Message = "身份認證失敗!" }; //自定義返回的數據類型 context.Response.ContentType = "application/json"; //自定義返回狀態碼,默認為401 我這里改成 200 context.Response.StatusCode = StatusCodes.Status200OK; //context.Response.StatusCode = StatusCodes.Status401Unauthorized; //輸出Json數據結果 context.Response.WriteAsync(Convert.ToString(payload)); return Task.FromResult(0); } }; }); //1、添加服務 services.AddSignalR(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } //跨域 app.UseCors(builder => { builder.SetIsOriginAllowed(origin => true) .AllowAnyHeader() .WithMethods("GET", "POST") .AllowCredentials(); }); //默認靜態資源路徑wwwroot app.UseStaticFiles(); //默認啟動cookie app.UseCookiePolicy(); //添加授權服務 app.UseAuthentication(); //配置 app.UseSignalR(routes => //2、引用 { //SignalrHub為后台代碼中繼承Hub的類,"/chatHub"為請求路由地址; routes.MapHub<ChatHub>("/chatHub"); }); //啟動MVC app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } } }
Authorize
代碼如下:
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using test.Controllers; namespace test.Hubs { //[Authorize(AuthenticationSchemes =JwtBearerDefaults.AuthenticationScheme)] //cookie 方式 [Authorize(AuthenticationSchemes = "JwtBearer")] //自定義JwtBearer方案 public class ChatHub : Hub //引用 using Microsoft.AspNetCore.SignalR; { public async Task SendMessage(string user, string message) { await Clients.All.SendAsync("ReceiveMessage", user, message); } /// <summary> /// 客戶端連接的時候調用 /// </summary> /// <returns></returns> [Authorize]//添加Authorize標簽,可以加在方法上,也可以加在類上 public override async Task OnConnectedAsync() { System.Diagnostics.Trace.WriteLine("客戶端連接成功"); await base.OnConnectedAsync(); }//所有鏈接的客戶端都會在這里 /// <summary> /// 連接終止時調用。 /// </summary> /// <returns></returns> public override Task OnDisconnectedAsync(Exception exception) { System.Diagnostics.Trace.WriteLine("連接終止"); return base.OnDisconnectedAsync(exception); } } }
為了前端判斷授權失敗跳轉相應處理
Startup.cs文件更改:
//此處為權限驗證失敗后觸發的事件 OnChallenge = context => { //此處代碼為終止.Net Core默認的返回類型和數據結果,這個很重要哦,必須 context.HandleResponse(); //自定義自己想要返回的數據結果,我這里要返回的是Json對象,通過引用Newtonsoft.Json庫進行轉換 var payload = JsonConvert.SerializeObject(new { Code = "401", Message = "身份認證失敗!" }); //自定義返回的數據類型 context.Response.ContentType = "application/json"; //自定義返回狀態碼,默認為401 我這里改成 200 //context.Response.StatusCode = StatusCodes.Status200OK; context.Response.StatusCode = StatusCodes.Status401Unauthorized; //輸出Json數據結果 context.Response.WriteAsync(payload); return Task.FromResult(0); }
前端代碼更改:
connection.start().then(function (data) { console.log('已成功連接到signalr服務器'); document.getElementById("sendButton").disabled = false; }).catch(function (error) { if (error.statusCode) {//判斷undefined、null與NaN if (error.statusCode == "401") { alert("error.message == Unauthorized"); } } else { console.error(error.toString()); } });
如圖:
注意:
1、jwt自己會驗證。
2、jwt的token而不是自己隨便寫(token)
以及
以及
代碼如下:
//Token頒發機構 ValidIssuer = jwtSettings.Issuer, //頒發給誰 ValidAudience = jwtSettings.Audience, //這里的key要進行加密 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey)),
3、控制台查看日志(日志級別調低)