SignalR:身份认证
集线器允许任意客户端连接的话会有安全问题,所以应该对连接进行验证,只有通过验证的用户才能连接集线器。SignalR支持验证和授权机制,我们同样可以用Cookie、JWT等方式进行身份信息的传递。由于JWT更符合项目的要求,因此这里讲解SignalR与JWT验证方式的使用。
第1步:
先在配置系统中配置一个名字为JWT的节点,然后在JWT节点下创建SigningKey、ExpireSeconds两个配置项。再创建一个类JWTOptions,类中包含对应的SigningKey、ExpireSeconds两个属性。
public class JWTOptions { public string SigningKey { get; set; } public int ExpireSeconds { get; set; } }
第2步:
通过NuGet安装Microsoft.AspNetCore.Authentication.JwtBearer
。
第3步:
编写代码对JWT进行配置,把以下代码添加到Program.cs的builder.Build之前。
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; } }; });
可以看到,这段代码和之前鉴权授权JWT中不同的是,在SignalR中我们增加了第17~30行代码。在ASPNET Core Web中,我们把JWT放到名字为Authorization的报文头中,但是WebSocket不支持Authorization报文头,而且WebSocket中也不能自定义请求报文头。我们可以把JWT放到请求的URL中,然后在服务器端检测到请求的URL中有JWT,并且请求路径是针对集线器的,我们就把URL请求中的JWT取出来赋值给context.Token,接下来ASP.NET Core就能识别、解析这个JWT了。
第4步:
在Program.cs的app.UseAuthorization之前添加app.UseAuthentication
。
第5步:
在控制器类Test1Controller中增加登录并且创建JWT的操作方法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("用户名或者密码错误"); } 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); }
第6步:
在需要登录才能访问的集线器类上或者方法上添加[Authorize]
。
[Authorize] public class ChatRoomHub : Hub { public Task SendPublicMessage(string message) { // 从JWT中获取用户名,然后把用户名拼接到发送给客户端的消息中 string name = this.Context.User!.FindFirst(ClaimTypes.Name)!.Value; string msg = $"{name}{DateTime.Now}:{message}"; return Clients.All.SendAsync("ReceivePublicMessage", msg); } }
如果[Authorize]只添加到ChatRoomHub类的方法上,而不是ChatRoomHub类上的话,那么连接到这个集线器的过程是不需要验证的,这样就造成了任意的客户端都可以连接到这个集线器上监听消息,它们只是不能向服务器发送“SendPublicMessage”消息而已。大部分项目应该是不允许非验证用户连接集线器的,因此建议一定要把[Authorize]标注到Hub类上。标注到Hub类的方法上的[Authorize]应该用于更详细的权限控制,比如集线器中的某些方法只有管理员才能调用。
第7步:
修改前端代码。
<template> <fieldset> <legend>登录</legend> <div> 用户名:<input type="text" v-model="state.loginData.userName"/> </div> <div> 密码:<input type="password" v-model="state.loginData.password"> </div> <div> <input type="button" value="登录" v-on:click="loginClick"/> </div> </fieldset> 公屏:<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() { // state中增加一个对用户名、密码进行绑定的属性,以及一个保存登录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 }; // 通过options的accessTokenFactory回调函数把JWT传递给服务器端 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("登陆成功可以聊天了"); }; // 登录按钮的响应函数 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>
首先,我们在页面中增加包含用户名、密码和【登录】按钮的界面元素,然后在页面的state中增加一个对用户名、密码进行绑定的属性,以及一个保存登录JWT的属性。
因为这里需要完成登录验证后才能用获得的JWT去连接ChatRoomHub,所以我们把连接ChatRoomHub的代码从onMount中移到startConn函数中。
在loginClick函数中,我们先通过axios向登录接口发送登录请求,然后把获得的JWT赋值给state.accessToken,最后调用startConn函数创建连接。
最后:
运行上面的服务器端和前端项目代码,然后在浏览器端访问页面,登录后,我们就可以聊天了。
本文来自博客园,作者:一纸年华,转载请注明原文链接:https://www.cnblogs.com/nullcodeworld/p/16739666.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
2021-09-28 设置Visual Studio总是以管理员身份运行