There are security issues with the hub allowing any client to connect, so the connection should be authenticated and only authenticated users can connect to the hub. SignalR supports authentication and authorization mechanisms, and we can also use cookies, JWT, etc. to transmit identity information. Since JWT is more in line with the requirements of the project, the use of SignalR and JWT verification methods is explained here.
step 1:
First configure a node named JWT in the configuration system, and then create two configuration items, SigningKey and ExpireSeconds, under the JWT node. Create another class JWTOptions, the class contains the corresponding SigningKey, ExpireSeconds two properties.
public class JWTOptions { public string SigningKey { get; set; } public int ExpireSeconds { get; set; } }
Step 2:
Install Microsoft.AspNetCore.Authentication.JwtBearer via NuGet.
Step 3:
To write code to configure the JWT, add the following code before builder.Build in Program.cs.
var services = builder.Services; services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT")); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(x => { var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTOptions>(); byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey); var secKey = new SymmetricSecurityKey(keyBytes); x.TokenValidationParameters = new() { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = secKey }; x.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/Hubs/ChatRoomHub"))) { context.Token = accessToken; } return Task.CompletedTask; } }; });
As you can see, the difference between this code and the previous authentication and authorization JWT is that we added lines 17 to 30 in SignalR. In ASPNET Core Web, we put the JWT in the header named Authorization, but WebSocket does not support the Authorization header, and the request header cannot be customized in WebSocket. We can put the JWT in the requested URL, and then detect that there is a JWT in the requested URL on the server side, and the request path is for the hub, we take out the JWT in the URL request and assign it to context.Token, then ASP.NET Core can identify and parse this JWT.
Step 4:
Add app.UseAuthentication before app.UseAuthorization in Program.cs.
Step 5:
In the controller class Test1Controller, add login and create a JWT operation method Login.
[HttpPost] public async Task<IActionResult> Login(LoginRequest req, [FromServices] IOptions<JWTOptions> jwtOptions) { string userName = req.UserName; string password = req.Password; User? user = UserManager.FindByName(userName); if (user == null || user.Password != password) { return BadRequest("Username or password is incorrect"); } var claims = new List<Claim>(); claims.Add(new Claim(ClaimTypes.Name, userName)); claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())); string jwtToken = BuildToken(claims, jwtOptions.Value); return Ok(jwtToken); } private static string BuildToken(IEnumerable<Claim> claims, JWTOptions options) { DateTime expires = DateTime.Now.AddSeconds(options.ExpireSeconds); byte[] keyBytes = Encoding.UTF8.GetBytes(options.SigningKey); var secKey = new SymmetricSecurityKey(keyBytes); var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature); var tokenDescriptor = new JwtSecurityToken(expires: expires, signingCredentials: credentials, claims: claims); return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); }
Step 6:
Add [Authorize] on hub classes or methods that require login to access.
[Authorize] public class ChatRoomHub : Hub { public Task SendPublicMessage(string message) { // Get the username from the JWT, then concatenate the username into the message sent to the client string name = this.Context.User!.FindFirst(ClaimTypes.Name)!.Value; string msg = $"{name}{DateTime.Now}:{message}"; return Clients.All.SendAsync("ReceivePublicMessage", msg); } }
If [Authorize] is only added to the method of the ChatRoomHub class, not the ChatRoomHub class, then the process of connecting to the hub does not require authentication, thus causing any client to connect to the hub to listen for messages , they just cannot send "SendPublicMessage" messages to the server. Most projects should not allow non-authenticated users to connect to the hub, so it is recommended to mark [Authorize] on the Hub class. [Authorize] on the methods marked on the Hub class should be used for more detailed permission control, such as some methods in the hub that only administrators can call.
Step 7:
Modify the front-end code.
<template> <fieldset> <legend>Log in</legend> <div> username:<input type="text" v-model="state.loginData.userName"/> </div> <div> password:<input type="password" v-model="state.loginData.password"> </div> <div> <input type="button" value="Log in" v-on:click="loginClick"/> </div> </fieldset> Public screen:<input type="text" v-model="state.userMessage" v-on:keypress="txtMsgOnkeypress" /> <div> <ul> <li v-for="(msg,index) in state.messages" :key="index">{{msg}}</li> </ul> </div> </template> <script> import { reactive, onMounted } from 'vue'; import * as signalR from '@microsoft/signalr'; import axios from 'axios'; let connection; export default {name: 'Login', setup() { // Add an attribute to the state to bind the username and password, and an attribute to save the login JWT const state = reactive({ accessToken:"",userMessage: "", messages: [], loginData: { userName: "", password: "" }, privateMsg: { destUserName:"",message:""}, }); const startConn = async function () { const transport = signalR.HttpTransportType.WebSockets; const options = { skipNegotiation: true, transport: transport }; // Pass the JWT to the server through the accessTokenFactory callback function of options options.accessTokenFactory = () => state.accessToken; connection = new signalR.HubConnectionBuilder() .withUrl('https://localhost:7002/Hubs/ChatRoomHub', options) .withAutomaticReconnect().build(); try { await connection.start(); } catch (err) { alert(err); return; } connection.on('ReceivePublicMessage', msg => { state.messages.push(msg); }); alert("Log in successfully and you can chat"); }; // Response function for login button const loginClick = async function () { const resp = await axios.post('https://localhost:7002/Test1/Login', state.loginData); state.accessToken = resp.data; startConn(); }; const txtMsgOnkeypress = async function (e) { if (e.keyCode != 13) return; try { await connection.invoke("SendPublicMessage", state.userMessage); }catch (err) { alert(err); return; } state.userMessage = ""; }; const txtPrivateMsgOnkeypress = async function (e) { if (e.keyCode != 13) return; const destUserName = state.privateMsg.destUserName; const msg = state.privateMsg.message; try { const ret = await connection.invoke("SendPrivateMessage", destUserName, msg); if (ret != "ok") { alert(ret);}; } catch (err) { alert(err); return; } state.privateMsg.message = ""; }; return { state, loginClick, txtMsgOnkeypress, txtPrivateMsgOnkeypress }; }, } </script> <style scoped> </style>
First, we add interface elements containing the username, password, and [Login] button to the page, and then add a property to the state of the page that binds the username and password, and a property that saves the login JWT.
Because it is necessary to complete the login verification, the obtained JWT can be used to connect to ChatRoomHub, so we move the code for connecting ChatRoomHub from onMount to the startConn function.
In the loginClick function, we first send a login request to the login interface through axios, then assign the obtained JWT to state.accessToken, and finally call the startConn function to create a connection.
at last:
Run the above server-side and front-end project code, then visit the page on the browser side, after logging in, we can chat.