IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(三)


后台服务用户与认证


新建一个空的.net core web项目Demo.Chat,端口配置为5001,安装以下nuget包

1.IdentityServer4.AccessTokenValidation,IdentityServer4客户端认证所用;

2.Microsoft.AspNetCore.SignalR

3.RabbitMQ.Client

添加appsettings.json

{
"RabbitMQ": {
"Host": "192.168.1.107",
"User": "admin",
"Password": "123123"
},
"Authentication": {
"Authority": "http://localhost:5000"
}
}

这里我们新增两个Dto类,一个消息传输类MsgDto,一个用户数据类UserDto

    public class MsgDto
{
public UserDto FromUser { get; set; }
public UserDto ToUser { get; set; }
public string Content { get; set; }
public DateTime SendTime { get; set; }
}
public class UserDto
{
// signalr当前的连接id
public string ConnectionId { get; set; }
public Guid Id { get; set; }
public string UserName { get; set; }
public string EMail { get; set; }
public string Avatar { get; set; }
}

当用户认证通过后,从Identity返回的token中我们已经返回了用户的基础信息了,那这里我们如何获取呢?很简单在上下文的User中Claims属性里面,所以这里我们增加一个扩展方法来转换为UserDto

        public static UserDto GetUser(this ClaimsPrincipal claimsPrincipal)
{
return new UserDto
{
Id = new Guid(claimsPrincipal.Claims.FirstOrDefault(r => r.Type == "sub").Value),
EMail = claimsPrincipal.Claims.FirstOrDefault(r => r.Type == "email").Value,
UserName = claimsPrincipal.Claims.FirstOrDefault(r => r.Type == "username").Value,
Avatar = claimsPrincipal.Claims.FirstOrDefault(r => r.Type == "avatar").Value,
};
}

既然是在线聊天那必须得存储当前所有的在线用户对吧?新建一个OnlineUsers类,这里我们就不用数据库了,Demo嘛,里面就3个用户,嘿嘿。当然你完全可以自由发挥使用其他redis,mongo什么什么的。

    public class OnlineUsers
{
/// <summary>
/// 用户id作为key
/// </summary>
private static ConcurrentDictionary<Guid, UserDto> onlineUsers { get; } = new ConcurrentDictionary<Guid, UserDto>(); public void AddOrUpdateUser(UserDto user)
{
onlineUsers.AddOrUpdate(user.Id, user, (id, r) => user);
} public List<UserDto> Get()
{
return onlineUsers.Values.ToList();
} public UserDto Get(Guid userId)
{
onlineUsers.TryGetValue(userId, out UserDto user);
return user;
} public void Remove(Guid userId)
{
if (onlineUsers.ContainsKey(userId))
onlineUsers.TryRemove(userId, out UserDto user);
}
}

后台服务RabbitMQ消息处理


RabbitMQ消息队列相关的知识这里我也不再赘述,园子里面很多,大家自行研究,RabbitMQ大概有2个种模式:生产消费者模式和发布/订阅模式,生产消费者模式即消息只能被使用一次,比如一个商品生产出来你只能卖给一个消费者对吧,发布/订阅即只要订阅了都会收到该消息。这里我们用到的是生产消费者模式,参考官方文档

消息发送和收到消息的处理,这里我们分为2个类单独处理,MsgSender和MsgHandler。

MsgSender:当用户发送了一条消息,后端收到后就将消息添加到消息队列,MsgHandler:一直处于运行状态,当收到队列的消息时,开始处理消息,调用SignalR的方法,发送消息到客户端,RabbitMQ的连接配置在appsettings.json中,注入IConfiguration获取

MsgSender

    public class MsgSender
{
public MsgSender(IConfiguration configuration)
{
factory = new ConnectionFactory();
factory.HostName = configuration.GetValue<string>("RabbitMQ:Host");
factory.UserName = configuration.GetValue<string>("RabbitMQ:User");
factory.Password = configuration.GetValue<string>("RabbitMQ:Password");
}
ConnectionFactory factory; public void Send(MsgDto msg)
{
using (var connection = factory.CreateConnection())
{
using (var channel = connection.CreateModel())
{
channel.QueueDeclare("chat_queue", false, false, false, null);//创建一个名称为hello的消息队列
var body = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(msg));
channel.BasicPublish("", "chat_queue", null, body); //开始传递
}
}
}
}

MsgHandler,需要注入IHubContext接口,用于发送消息到客户端,ps:在Hub类中,可以通过Clients直接发送消息到客户端,在其他类里面可以使用这个接口,获取到Clients。

    public class MsgHandler : IDisposable
{
public MsgHandler(IConfiguration configuration, IHubContext<MessageHub> hubContext)
{
factory = new ConnectionFactory();
factory.HostName = configuration.GetValue<string>("RabbitMQ:Host");
factory.UserName = configuration.GetValue<string>("RabbitMQ:User");
factory.Password = configuration.GetValue<string>("RabbitMQ:Password");
this.hubContext = hubContext;
connection = factory.CreateConnection();
channel = connection.CreateModel(); }
ConnectionFactory factory;
// 注入SignalR的消息处理器上下文,用以发送消息到客户端
IHubContext<MessageHub> hubContext;
IConnection connection;
IModel channel;
public void BeginHandleMsg()
{
channel.QueueDeclare("chat_queue", false, false, false, null);
var consumer = new EventingBasicConsumer(channel);
channel.BasicConsume("chat_queue", false, consumer);
consumer.Received += (model, arg) =>
{
var body = arg.Body;
var message = Encoding.UTF8.GetString(body);
var msg = JsonConvert.DeserializeObject<MsgDto>(message);
// 通过消息处理器上下文发送消息到客户端
hubContext.Clients?.Client(msg.ToUser.ConnectionId)
?.SendAsync("Receive", msg); channel.BasicAck(arg.DeliveryTag, false);
};
} public void Dispose()
{
channel?.Dispose();
connection?.Dispose();
}
}

后台服务SignalR消息处理器


关于SignalR,官方文档

SignalR的核心就是继承自Hub消息处理类,这个类中所有的public 方法都可以给客户端调用。我们的聊天室比较简陋,只需要一个Send方法给客户端就够了,是吧?当然服务端需要2个主动发送消息到客户端的方法,1.当有用户登录时通知所有客户端刷新在线用户列表,2.有什么错误的时候发送错误消息给客户端,比如我们不允许离线发送,用户发了条消息给一个不在线的用户。

另外当用户登录和离开时需要在OnlineUsers中进行注册和注销。

MessageHub,我们的聊天室必须登录,所以加上Authorize特性。

    [Authorize]
public class MessageHub : Hub
{
MsgSender msgSender;
MsgHandler msgQueueHandler;
OnlineUsers onlineUsers;
public MessageHub(MsgSender msgSender, MsgHandler msgQueueHandler, OnlineUsers onlineUsers)
{
this.msgSender = msgSender;
this.msgQueueHandler = msgQueueHandler;
this.onlineUsers = onlineUsers;
} public async Task Send(string toUserId, string message)
{
string timestamp = DateTime.Now.ToShortTimeString();
var toUser = onlineUsers.Get(new Guid(toUserId));
if (toUser == null)
{
await SendErrorAsync("用户已离线");
return;
}
var fromUser = Context.User.GetUser();
msgSender.Send(new Dtos.MsgDto
{
Content = message,
FromUser = fromUser,
SendTime = DateTime.Now,
ToUser = toUser
});
} /// <summary>
/// 当有用户登录时 添加在线用户,并设置用户的ConnectionId
/// </summary>
/// <returns></returns>
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
var user = Context.User.GetUser();
if (user == null)
{
await SendErrorAsync("您没有登录");
return;
}
user.ConnectionId = Context.ConnectionId;
onlineUsers.AddOrUpdateUser(user);
await SendUserInfo();
await RefreshUsersAsync();
} /// <summary>
/// 当有用户离开时,注销用户登录
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
public override async Task OnDisconnectedAsync(Exception exception)
{
//disconnection
await base.OnDisconnectedAsync(exception);
var userId = Context.User?.GetUser()?.Id;
if (userId.HasValue)
onlineUsers.Remove(userId.Value);
await RefreshUsersAsync();
} private async Task RefreshUsersAsync()
{
var users = onlineUsers.Get().Where(r => r.Id != Context.User.GetUser().Id).ToList();
// 发送给所有的在线客户端,通知刷新在线用户
await Clients.All.SendAsync("Refresh", users);
} private async Task SendErrorAsync(string errorMsg)
{
// 发送错误消息给调用者
await Clients.Caller.SendAsync("Error", errorMsg);
} }

这里就冒出来另外一个新的问题了,SignalR使用的是websocket,据我了解到的是没有header头这个东西的,而jwt token默认是通过header中Authorization信息进行认证的。那这个授权又如何实现呢?想办法咯,既然header传不进来,那直接url传进来总可以吧。

后台服务:服务注册与认证授权


好了,我们先将需要的服务先配置下。

AddIdentityServerAuthentication实际上是AddJwtBearer的扩展,你要喜欢也可以用AddJwtBearer配置,由IdentityServer4.AccessTokenValidation提供,配置认证Authority为http://localshot:5000(Demo.Identity配置的端口号为5000,appsetting.json中配置),ApiName和Secret与Identity端配置的ApiResource一致。

        public void ConfigureServices(IServiceCollection services)
{
// 注册消息处理器 消息发送器,在线用户类
services.AddSingleton<MsgHandler>()
.AddSingleton<MsgSender>()
.AddSingleton<OnlineUsers>(); // 增加认证服务
services.AddAuthentication(r =>
{
r.DefaultScheme = "JwtBearer";
})
// 增加jwt认证
.AddIdentityServerAuthentication("JwtBearer", r =>
{
// 配置认证服务器
r.Authority = Configuration.GetValue<string>("Authentication:Authority");
// 配置无需验证https
r.RequireHttpsMetadata = false;
// 配置 当前资源服务器的名称
r.ApiName = "chatapi";
// 配置 当前资源服务器的连接密码
r.ApiSecret = "";
r.SaveToken = true;
}); // 跨域
services.AddCors(r =>
{
r.AddPolicy("all", policy =>
{
policy
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
;
});
});
// 增加授权服务
services.AddAuthorization();
// 增加SignalR 服务
services.AddSignalR();
}

刚刚提到SignalR认证的问题,具体如何实现呢?这里也有2种方式,1.使用中间件在认证之前从url中获取token并添加到header中;2.r.MapHub<MessageHub>("/msg"),可以配置在参数中添加自定义的IAuthorizeData接口,可以自己实现获取token验证,我觉得比较麻烦,这里我们使用第一种方式。

添加中间件,这个中间件一定要在UseAuthentication之前:

            // signalr jwt认证 token添加
app.Use(async (context, next) =>
{
// 这里从url中获取token参数,实际应用请实际考虑,加一些过滤条件
if (context.Request.Query.TryGetValue("token", out var token))
{
// 从url中拿到header,再添加到header中,一定要在UseAuthentication之前
context.Request.Headers.Add("Authorization", $"Bearer {token}");
}
await next.Invoke();
});

好了,还有一个问题,前面写的MsgHandler什么时候开始处理消息?Dispose什么时候调用?这里我们使用IApplicationLifetime接口,该接口提供了应用的整个生命周期事件处理。在应用启动的时候我们注册消息处理,应用结束时Dispose。

            // 应用启动时开始处理消息
applicationLifetime.ApplicationStarted.Register(msgHandler.BeginHandleMsg);
// 应用退出时,释放资源
applicationLifetime.ApplicationStopping.Register(msgHandler.Dispose);

完整的Configure代码:

        public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
MsgHandler msgHandler,
IApplicationLifetime applicationLifetime)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseCors("all"); app.UseAuthentication();
// 使用SignalR 并添加MessageHub类的消息处理器
app.UseSignalR(r =>
{
r.MapHub<MessageHub>("/msg");
}); // 应用启动时开始处理消息
applicationLifetime.ApplicationStarted.Register(msgHandler.BeginHandleMsg);
// 应用退出时,释放资源
applicationLifetime.ApplicationStopping.Register(msgHandler.Dispose);
}

另外用户登录后需要展示用户信息,邮件地址啊头像什么的,这里我们也有2种方式,1是消息处理器中,当用户连接后主动发送消息给用户;2是建一个Api接口,当然放在消息处理器中会显得更纯洁,web项目里面没有一个controller,这里我们使用第一种方式。

在MessageHub中添加方法,在OnConnectedAsync方法中调用

        private async Task SendUserInfo()
{
await Clients.Caller.SendAsync("UserInfo", Context.User.GetUser());
}

聊天室web前端


官方提供了js库,可以用npm安装,npm install @aspnet/signalr。

这个前端嘛,我就不花大功夫去做得漂亮高大上了,暂时就把代码直接丢在Demo.chat里面吧,2个页面,登录页login,聊天室页面chat。

关于前端就不啰嗦了,再啰嗦就是关公面前耍大刀了,什么angular,vue,老夫写代码统统jquery。其他的大家自己发挥了。

login.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>登录聊天室</title>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
<div>
<fieldset>
<legend>登录聊天室</legend>
<div>
<input type="text" name="uername" id="username" value="" />
</div>
<div>
<input type="password" name="password" id="password" value="" />
</div>
<div>
<button id="login" type="button">登录</button>
</div>
</fieldset>
</div>
<script type="text/javascript">
$(function () {
var identityUrl = 'http://localhost:5000/connect/token'; $('#login').click(function () { $.post(identityUrl, {
client_id: 'chat_client',
grant_type: 'password',
scope: 'openid chatapi profile offline_access',
username: $('#username').val(),
password: $('#password').val()
}, function (result) {
if (result && result.access_token) {
sessionStorage['token'] = result.access_token;
window.location = "http://localhost:5001/chat.html";
}
}, 'json');
});
});
</script>
</body>
</html>

chat.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="/lib/signalr.js"></script>
</head>
<body>
<div>
<div>
<input type="hidden" id="userId" value="" />
<p><label>UserName:</label><i id="userName"></i></p>
<p><label>EMail:</label><i id="email"></i></p>
<p><label>Avatar:</label><img id="avatar" style="width:48px; height:48px; border-radius:50%; overflow:hidden;" /> </p>
</div>
<div style="width:700px;height:500px;border:1px solid red;">
<ul id="msgList"></ul>
</div>
<div>
<select id="users"></select>
</div>
<div>
<textarea id="msgSendContent" placeholder="请输入发送消息" cols="" rows=""></textarea>
<br />
<button id="send" type="button">发送</button>
</div> </div> <script type="text/javascript">
$(function () {
var token = sessionStorage['token']; if (!token) {
alert('请先登录!');
window.location = 'http://localhost:5001/login.html';
return;
}
function timeFormat(time) {
time = new Date(time)
return time.toLocaleDateString() + ' ' + time.toLocaleTimeString();
} var connection = new signalR.HubConnectionBuilder()
.withUrl("/msg?token=" + token)
.configureLogging(signalR.LogLevel.Information)
.build(); connection.on('Receive', function (msg) {
var $ul = $('#msgList');
var $li = $('<li>' + msg.fromUser.userName + '[' + timeFormat(msg.sendTime) + '] : ' + msg.content + '</li>');
$ul.append($li);
}); connection.on('UserInfo', function (userInfo) {
$('#userName').text(userInfo.userName);
$('#email').text(userInfo.eMail);
$('#avatar').attr('src', userInfo.avatar);
$('#userId').val(userInfo.id);
}); connection.on('Refresh', function (users) {
$('#users').empty();
users.forEach(function (user) {
if (user.id != $('#userId').val())
$('#users').append('<option value="' + user.id + '">' + user.userName + '</option>');
});;
}); connection.on('Error', function (err) {
alert(err);
}); connection.start().catch(err => console.error(err.toString()));
$('#send').click(function () {
var msg = $('#msgSendContent').val();
var toUerId = $('#users').val();
connection.invoke('Send', toUerId, msg).catch(err => console.error(err));
var $ul = $('#msgList');
var $li = $('<li>我[' + timeFormat(new Date()) + '] : ' + msg + '</li>');
$ul.append($li);
});
});
</script>
</body>
</html>

好了,代码就写完了,同时运行Demo.Identity和Demo.Chat。打开2个浏览器:http://localhost:5001/login.html。

输入用户名密码登录;

发送个消息试试:

是不是很简陋?嘿嘿

好了,到处为止。其他不完善的地方,自己动手,丰衣足食,如离线消息,token自动刷新等等.

IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(三)的更多相关文章

  1. IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(二)

    IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(二) IdentityServer4 用户中心生成数据库 上文已经创建了所有的数据库上下文迁移代码 ...

  2. IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯(一)

    IdentityServer4 + SignalR Core +RabbitMQ 构建web即时通讯 前言 .net core 2.1已经正式发布了,signalr core1.0随之发布,是时候写个 ...

  3. 一套简单的web即时通讯——第一版

    前言 我们之前已经实现了 WebSocket+Java 私聊.群聊实例,后面我们模仿layer弹窗,封装了一个自己的web弹窗 自定义web弹窗/层:简易风格的msg与可拖放的dialog,生成博客园 ...

  4. web即时通讯2--基于Spring websocket达到web聊天室

    如本文所用,Spring4和websocket要构建web聊天室,根据框架SpringMVC+Spring+Hibernate的Maven项目,后台使用spring websocket进行消息转发和聊 ...

  5. 为什么 web 开发人员需要迁移到. NET Core, 并使用 ASP.NET Core MVC 构建 web 和 webservice/API

    2018 .NET开发者调查报告: .NET Core 是怎么样的状态,这里我们看到了还有非常多的.net开发人员还在观望,本文给大家一个建议.这仅代表我的个人意见, 我有充分的理由推荐.net 程序 ...

  6. 利用Eclipse中的Maven构建Web项目(三)

    利用Eclipse中的Maven构建Web项目 1.将Maven Project转换成动态Web项目,鼠标右键项目,输入"Project Facets" 2.依据Dynamic W ...

  7. 使用 ASP.NET Core MVC 创建 Web API(三)

    使用 ASP.NET Core MVC 创建 Web API 使用 ASP.NET Core MVC 创建 Web API(一) 使用 ASP.NET Core MVC 创建 Web API(二) 十 ...

  8. .Net core 3.0 SignalR+Vue 实现简单的即时通讯/聊天IM (无jq依赖)

    .Net core 中的SignalR JavaScript客户端已经不需要依赖Jquery了 一.服务端 1.nuget安装 Microsoft.AspNetCore.SignalR 2.在star ...

  9. Mysql EF Core 快速构建 Web Api

    (1)首先创建一个.net core web api web项目; (2)因为我们使用的是ef连接mysql数据库,通过NuGet安装MySql.Data.EntityFrameworkCore,以来 ...

随机推荐

  1. eWebEditor不支持IE7以上版本Bug修改

    修改: \Include\Editor.js //把此行 if (element.YUSERONCLICK) eval(element.YUSERONCLICK + "anonymous() ...

  2. hdu 2242 无向图/求用桥一分为二后使俩个bcc点权值和之差最小并输出 /缩点+2次新图dfs

    题意如标题所述, 先无向图缩点,统计出每个bcc权,建新图,然后一遍dfs生成树,标记出每个点(新图)以及其子孙的权值之和.这样之后就可以dfs2来枚举边(原图的桥),更新最小即可. 调试了半天!原来 ...

  3. js计算数组中每个元素出现的次数

    计算数组中每个元素出现的次数 var names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice']; var countedNames = names.redu ...

  4. AC日记——[ZJOI2006]物流运输 bzoj 1003

    1003 思路: 最短路+dp: 节点在a-b天里不能使用 那么我们准备每一组a-b求一条最短路,如果没有,则用极大值表示: cost[a,b]记录这个最短路: 然后,开始dp: dp[i]=min( ...

  5. HDU 1223 还是畅通过程【最小生成树模板】

    还是畅通工程 Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Subm ...

  6. Codeforces 371B Fox Dividing Cheese(简单数论)

    题目链接 Fox Dividing Cheese 思路:求出两个数a和b的最大公约数g,然后求出a/g,b/g,分别记为c和d. 然后考虑c和d,若c或d中存在不为2,3,5的质因子,则直接输出-1( ...

  7. github如何实现fork的项目与原项目同步

    refer to https://www.jianshu.com/p/fede3333205f 作者:hitchc 链接:https://www.jianshu.com/p/fede3333205f ...

  8. [Math Review] Linear Algebra for Singular Value Decomposition (SVD)

    Matrix and Determinant Let C be an M × N matrix with real-valued entries, i.e. C={cij}mxn Determinan ...

  9. python画直线

    #!/usr/bin/env python import matplotlib.pyplot as plt import numpy as np #beita = 1 #gama = 0.5 #x:f ...

  10. 2013年9月29日 iOS 周报

    新闻 Apple Tech Talks 2013 在中国上海的iOS Tech Talks活动将于11月12日展开,活动主要针对iOS 7.活动分为App开放日和游戏开放日,主要内容可查看链接.当你看 ...