欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

基于SignalR的消息推送与二维码扫描登录实现代码

程序员文章站 2022-06-29 17:41:50
1 概要说明 使用微信扫描登录相信大家都不会陌生吧,二维码与手机结合产生了不同应用场景,基于二维码的应用更是比较广泛。为了满足ios、android客户端与web短信平台...

1 概要说明

使用微信扫描登录相信大家都不会陌生吧,二维码与手机结合产生了不同应用场景,基于二维码的应用更是比较广泛。为了满足ios、android客户端与web短信平台的结合,特开发了基于singlarr消息推送机制的扫描登录。本系统涉及到以下知识点:

signalr: 这官网,asp.net signalr 是为 asp.net 开发人员提供的一个库,可以简化开发人员将实时 web 功能添加到应用程序的过程。实时 web 功能是指这样一种功能:当所连接的客户端变得可用时服务器代码可以立即向其推送内容,而不是让服务器等待客户端请求新的数据。

二维码:使用的qrcode类库,

mvc5:开发环境是基于mvc5

基于SignalR的消息推送与二维码扫描登录实现代码

2、系统关系图

在实现本功能前,有点不是太确定能否拿下。

所谓万事开头难,通过查询想资料及自己归纳分析:系统涉及到手机客户端、浏览者、服务端,实现扫描登录也就是三者之间是如何协调工作的。通过axure画出如下关系图:

基于SignalR的消息推送与二维码扫描登录实现代码

移动客户端、浏览者、服务端三者协作关系图

【m】:表示移动端 【b】:表示浏览者(浏览器客户端) 【s】:服务端,消息推送者及扫描认证接口发布者

步骤说明:

step(步骤)1 ,【b】浏览登录页面,step2【s】产生一个标识符uuid,并推送给b,生成登录二维码;

step3,【m】扫描二维码,前提条件是【m】已登录,step4【m】解析二维码信息获取uuid;

step5,【m】向服务端发送uuid+登录信息,step6【s】对uuid+登录信息进行相关解析认证,step6 uuid认证,不通过认证,则到step6-1 重新生成uuid循环step 2与并step6-2 返回给【m】uuid认证失败原因,step6 通过认证,step6-2转到登录信息认证,step 7登录信息认证,失败step7-3重新生成uuid循环step 2,成功则step7-1推送给【b】跳转到首页。

3、signalr循环消息推送

3.1 引用signalr

由于本人用的是vs15preview4,可以直接使用nuget可视化管理工具进行安装:tools—>nuget package manager—>manage nuget packages for solution…,打开以下界面:

基于SignalR的消息推送与二维码扫描登录实现代码

在browser 标签下输入signalr,查询到microsoft.aspnet.signalr

基于SignalR的消息推送与二维码扫描登录实现代码/p>

找到对应的项目,点击“install”安装按钮即可引用相关类库,同时应用下载相关js库。

关于signalr的知识点,可以到官网 进行深入学习。

3.2 服务端signalr实现

服务端要向客户端推送uuid,对于uuid唯一标识符,具有重要特性:(1)有时间限制,120秒之内扫码有效;(2)具有一定的状态。对应的声明周期就是:生成—>推送—>状态判断—>手机端扫描—>验证uuid—>状态判断—>销毁等系列过程。

服务端的核心代码将单独建立一个项目去实现:

基于SignalR的消息推送与二维码扫描登录实现代码

3.2.1 nofifier.cs通知类

本类将连接qrcodehub与sessiontimer

using microsoft.aspnet.signalr;namespace txsms.singalr
{
public static class notifier 
{ 
private static readonly ihubcontext context = globalhost.connectionmanager.gethubcontext<qrcodehub>(); 
public static void sessiontimeout(string connectionid, int time) 
{ 
context.clients.client(connectionid).alertclient(time); 
} 
public static void sendelapsedtime(string connectionid, int time) 
{
context.clients.client(connectionid).sendelapsedtime(time); 
} 
public static void sendqrcodeuuid(string connectionid, string uuid)
{ context.clients.client(connectionid).sendqrcodeuuid(uuid);
} 
}
}

3.2.2 qrcodehub.cs signalr核心实现

signalr的核心代码:

using microsoft.aspnet.signalr;
using system.threading.tasks;
namespace txsms.singalr
{ 
/// <summary> 
/// 二维码推送
/// 
</summary> 
//[hubname("qrcode")] public class qrcodehub : hub {
/// <summary> /// 给客户端发送时间间隔 /// 
</summary> /// <param name="time">
</param>
public void sendtimeoutnotice(int time) 
{
clients.client(context.connectionid).alertclient(time);
}
public void checkelapsedtime(int time) 
{
clients.client(context.connectionid).sendelapsedtime(time); } 
/// <summary> 
/// 发送二维码uuid内容
///
</summary>
/// <param name="uuid">
</param> 
public void sendqrcodeuuid(string uuid) 
{ 
clients.client(context.connectionid).sendqrcodeuuid(uuid); }
///
<summary>
/// called when the connection connects to this hub instance.
/// </summary> ///
<returns>a <see cref="t:system.threading.tasks.task" />
</returns> public override task onconnected() 
{
sessiontimer.starttimer(context.connectionid); return base.onconnected();
}
/// <summary> 
/// called when a connection disconnects from this hub gracefully or due to a timeout. 
///
</summary> 
/// 
<param name="stopcalled">
/// true, if stop was called on the client closing the connection gracefully;
/// false, if the connection has been lost for longer than the /// <see cref="p:microsoft.aspnet.signalr.configuration.iconfigurationmanager.disconnecttimeout" />. 
/// timeouts can be caused by clients reconnecting to another signalr server in scaleout. /// </param> /// <returns>a <see cref="t:system.threading.tasks.task" />
</returns> public override task ondisconnected(bool stopcalled) { sessiontimer.stoptimer(context.connectionid); return base.ondisconnected(stopcalled);
} 
/// <summary> /// 
called when the connection reconnects to this hub instance. /// </summary> ///
<returns>a <see cref="t:system.threading.tasks.task" />
</returns> public override task onreconnected()
{
if (!sessiontimer.timers.containskey(context.connectionid)) 
{ 
sessiontimer.starttimer(context.connectionid);
} 
return base.onreconnected(); }
///
<summary> /// 重置时钟 /// 
</summary> public void resettimer() 
{ sessiontimer timer;
if (sessiontimer.timers.trygetvalue(context.connectionid, out timer))
{ timer.resettimer(); 
} 
else
{
sessiontimer.starttimer(context.connectionid);
}
} /// 
<summary> /// 
发送普通消息 /// 
</summary> ///
<param name="name">
</param> /// <param name="message">
</param>
public void send(string name, string message) 
{ 
clients.all.addnewmessagetopage(name, message); 
} 
}
}

3.2.3 sessiontimer.cs 对应客户端时钟

对【b】来说,产生一个独立的timer,进行按1s间隔发送消息。

using system;using system.collections.concurrent;using system.timers;namespace txsms.singalr{ public class sessiontimer : idisposable 
{ 
///
<summary> /// 存储客户端对应的timer /// 
</summary> 
public static readonly concurrentdictionary<string, sessiontimer> timers; private readonly timer _timer; static sessiontimer() 
{ timers = new concurrentdictionary<string, sessiontimer>(); 
}
/// <summary> /// 构造函数 /// </summary> /// 
<param name="connectionid"></param> private sessiontimer(string connectionid)
{
connectionid = connectionid; _timer = new timer { interval = utility.activitytimerinterval() }; _timer.elapsed += (s, e) => monitorelapsedtime(); _timer.start(); 
}
public int timecount { get; set; } /// <summary> /// 客户端连接id ///
</summary> public string connectionid { get; set; } /// <summary> /// 启动timer /// 
</summary> ///
<param name="connectionid">
</param>
public static void starttimer(string connectionid) 
{ 
var newtimer = new sessiontimer(connectionid);
if (!timers.tryadd(connectionid, newtimer)) 
{ newtimer.dispose();
}
} 
/// <summary>
/// 停止timer /// </summary> ///
<param name="connectionid">
</param> public static void stoptimer(string connectionid) 
{
sessiontimer oldtimer; 
if (timers.tryremove(connectionid, out oldtimer))
{ 
oldtimer.dispose();
} 
} 
/// <summary> /// 
重置timer /// 
</summary> public void resettimer() { timecount = 0; _timer.stop();
_timer.start(); 
} 
public void dispose()
{ 
// stop might not be necessary since we call dispose _timer.stop(); _timer.dispose(); 
} ///

<summary> ///
给客户端发送消息 /// 
</summary> private void monitorelapsedtime() 
{ utility.clearexpireduuid(); var uuid = utility.getuuid(connectionid); 
//if (timecount >= utility.timervalue()) 
//{ // stoptimer(connectionid);
// notifier.sendqrcodeuuid(connectionid, uuid); // notifier.sessiontimeout(connectionid, timecount); 
//} 
//else //
{ notifier.sendqrcodeuuid(connectionid, uuid);
notifier.sendelapsedtime(connectionid, timecount); 
//}
timecount++;
if (timecount > 1000) 
{ 
timecount = 0;
} } }}

3.2.4 utility.cs 基础配置

满足时钟、获取qrcode等

using txsms.actions;namespace txsms.singalr
{
internal class utility { public static int intnum = 0; /// 
<summary> /// 时间间隔 /// 
</summary> ///
<returns></returns> 
public static int timervalue() 
{ return 1000;
} 
public static double activitytimerinterval()
{ return 1000.0;
}
/// <summary> /// 获取当前uuid
/// </summary>
/// <returns></returns> public static string getuuid(string connectionid)
{
try 
{ 
var model = new qrcodeaction().getvalidmodel(connectionid); 
return model.tojson(connectionid); 
} 
catch 
{ return "error"; 
} } /// <summary> /// 删除过期uuid /// 
</summary> public static void clearexpireduuid() { intnum++; 
if (intnum <= 1000) return; 
new qrcodeaction().clearexpireduuid();
intnum = 0; } }}

3.2.5 signalr在mvc中启动配置

在mvc中,启动项目进行如下配置:

using microsoft.owin;using owin;
[assembly: owinstartup(typeof(txsms.web.startup))]namespace txsms.web{ public partial class startup 
{ 
public void configuration(iappbuilder app) 
{ //启动signalr app.mapsignalr(); configureauth(app); 
} }}

3.2.6 其他类库说明

qrcodeaction.cs:维护uuid,创建、保存、状态更改、删除等。

qrmodel.cs:uuid实体

所有文件,可在《7、总结与下载》中下载。

3.3 客户端signalr实现

添加signalr js库:

<script type="text/javascript" src="~/scripts/jquery.signalr-2.2.1.min.js">
</script> 
<script type="text/javascript" src="~/signalr/hubs">
</script

两者必须都引用。

调用接口如下:

 var codeuuid = "";
 $(function () {
 // reference the auto-generated proxy for the hub. var qrcode = $.connection.qrcodehub;
 // create a function that the hub can call back to display messages. qrcode.client.addnewmessagetopage = function (name, message)
 { // add the message to the page. console.log(message); //jquery('#divqrcode').qrcode({ width: 180, height: 180, correctlevel: 0, text: message }); 
 }; 
 qrcode.client.sendelapsedtime = function (time) { console.log(time); 
 }; 
 qrcode.client.sendqrcodeuuid = function (uuid) { console.log("sendqrcodeuuid");
 console.log(codeuuid); if (codeuuid === uuid) { return; } codeuuid = uuid; if (codeuuid !== "error") { var jsonuuid = $.parsejson(codeuuid);
 if (jsonuuid.islogin === 1) { //判断是否登录 window.location.href = "/home/index/@model.name"; } } $("#divqrcode").html("");
 $('#divqrcode').qrcode({ width: 180, height: 180, correctlevel: 0, text: codeuuid });
 };
 // start the connection. $.connection.hub.start().done(function () { //qrcode.server.updateconnectionid($.connection.hub.id); qrcode.server.send("qrcode", math.random());
 });
 });

以上代码包括相关二维码的生成。

4、二维码的生成与存储数据解析

4.1 二维码的生成

二维码类库选择 一个qrcode原生态js类库,jquery对其进行了扩展。

添加script标签:

<script type="text/javascript" src="~/scripts/qrcode.min.js">
</script> 
<script type="text/javascript" src="~/scripts/jquery.qrcode.min.js">
</script>

定义div标签,用来呈现二维码:

 <!--二维码登录开始--> 
<div> 
<div>安全登录 防止被盗</div>
 <div>
</div> 
<div>扫一扫登录</div> 
</div> 
<!--二维码登录结束-->

呈现二维码:

$("#divqrcode").html(""); 
$('#divqrcode').qrcode({ width: 180, height: 180, correctlevel: 0, text: codeuuid });

通过3与4,可实现具有180秒生命周期二维码的生成,对于不同的浏览者,生成的二维码是不同的,效果如下:

基于SignalR的消息推送与二维码扫描登录实现代码

4.2 二维码存储的是什么

二维码生成了,但是存储的是什么呢?首先我们看下以下的二维:

基于SignalR的消息推送与二维码扫描登录实现代码基于SignalR的消息推送与二维码扫描登录实现代码

hbuilder官网

千牛电脑客户端二维码登录界面

显然,扫描这两个图片上的二维码会得到不同的结果。对某些二维码的解码要对应配套的客户端才能起到作用,否则用其他工具解析出来也就是字符串。

在本系统中,二维码存储的是一个json对象,格式为:

{"connectionid":"19c12e95-26d7-410c-8292-2a3afdd1a4da","uuid":"a04702df-6a52-4e1c-be8b-9b3dbeef4d72","islogin":0,"isvalid":1}

connectionid:客户端与signalr联系的id,其格式为guid

uuid:对应connectionid产生的一个唯一标识符,其格式为guidislogin:当前connectionid连接是否已登录,1—>表示登录,0—>未登录isvalid:当前connectionid对应的uuid是否有效,1—>表示有效,0—>表示失效

手机客户端扫描之后,可根据这些参数情况进行判断,是否向服务端发送请求。在做扫描应用(比如扫描登录)时,要依据业务场景进行消息传递,生成对应二维码,并不局限于json对象、url地址等。

总结下来,二维码应用场景,如下图:

基于SignalR的消息推送与二维码扫描登录实现代码

5、扫描认证接口

为了满足【m】端扫描之后,提交uuid+用户信息进行认证,建立qrcode api接口。接口任务比较简单,就是对uuid合法性进行判断,然后判断用户信息登录情况,更改uuid的登录状态。

5.1 输入参数

using abp.application.services.dto;using system;using system.componentmodel.dataannotations;
namespace txsms.inputs{ /// <summary> /// 二维码登录认证 /// </summary> [serializable]
public class qrcodeverifyinput : iinputdto { /// <summary> /// 构造函数 /// </summary>
public qrcodeverifyinput() { connectionid = guid.empty.tostring();
uuid = guid.empty; username = password = ""; } /// <summary> /// 当前回话id /// </summary> 
[displayformat(convertemptystringtonull = false)] public string connectionid { get; set;
} /// <summary> /// 唯一标识符号 /// </summary> 
public guid uuid { get; set; } /// <summary> /// 用户账号 /// </summary>
[displayformat(convertemptystringtonull = false)]
public string username { get; set; } /// <summary> /// 登录密码 /// </summary>
[displayformat(convertemptystringtonull = false)] 
public string password { get; set; } /// <summary> /// 平台 /// </summary>
[displayformat(convertemptystringtonull = false)] 
public string platform { get; set; 
} }}

5.2 输出参数

using abp.application.services.dto;
using newtonsoft.json;using system.componentmodel.dataannotations;using system.web.mvc;using txsms.mvc;namespace txsms.outputs{ /// <summary> /// 输出基类 /// </summary> [modelbinder(typeof(emptystringmodelbinder))] public class txsmsoutputdto : ioutputdto { /// <summary> /// 构造函数 /// 
</summary> public txsmsoutputdto() 
{ result = 0; //默认为0,表示初始值或正确 message = ""; 
} /// <summary> /// 错误代码 ///
</summary> [jsonproperty("result")] public int result { get; set; } /// <summary> /// 错误信息 /// </summary>
[displayformat(convertemptystringtonull = false)] [jsonproperty("message")] 
public string message { get; set; } }}

5.3 api接口

using system;using system.threading.tasks;using system.web.http;using txsms.actions;using txsms.inputs;using txsms.outputs;namespace txsms{ /// <summary> /// 二维码接口 /// </summary> public class qrcodecontroller : txsmsapicontroller { /// <summary> /// 二维码登录认证 /// </summary> /// <returns> /// 0:登录成功;-1:参数错误 -2:connectionid、uuid、username、password不允许为空-3:connectionid回话id不存在-4:uuid输入错误-5:uuid已过期 /// -6:本uuid已登录-7:登录账号已停用-8:登录账号已删除-9:登录密码输入错误-10:登录账号不存在 /// </returns> [allowanonymous] [httppost] public async task<txsmsoutputdto> qrcodeverify([frombody]qrcodeverifyinput model) { txsmsoutputdto result = new txsmsoutputdto(); #region 参数验证 if (model.isnull()) { result.result = -1; result.message = "参数错误"; return result; } if (model.connectionid.isnullorempty() || model.uuid.equals(guid.empty) || model.username.isnullorempty() || model.password.isnullorempty()) { result.result = -2; result.message = "connectionid、uuid、username、password不允许为空"; return result; } #endregion 参数验证 #region 有效性判断 //验证connectionid合法性 if (qrcodeaction.qrcodelists.containskey(model.connectionid)) { result.result = -3; result.message = "connectionid回话id不存在"; return result; } //验证uuid有效性 var findcode = qrcodeaction.qrcodelists[model.connectionid]; if (!model.uuid.equals(findcode.uuid)) { result.result = -4; result.message = "uuid输入错误"; return result; } if (!findcode.isvalid()) { result.result = -5; result.message = "uuid已过期"; return result; } if (findcode.islogin) { result.result = -6; result.message = "本uuid已登录"; return result; } #endregion 有效性判断 loginusernameinput loginparam = new loginusernameinput { username = model.username, password = model.password, platform = model.platform }; loginoutput loginresult = await new sessioncontroller().loginusername(loginparam); switch (loginresult.result) { case -1: result.result = -7; result.message = "登录账号已停用"; break; case -2: result.result = -8; result.message = "登录账号已删除"; break; case -3: result.result = -9; result.message = "登录密码输入错误"; break; case -4: result.result = -10; result.message = "登录账号不存在"; break; } if (loginresult.result > 0) //登录成功,值为accid { result.result = 0; findcode.islogin = true; //更改登录状态 result.message = "成功登录"; } return result; } }}

6、疑难解答

6.1 #16解答

二维码中可以加入图片吗?文中二维码 有个图片上面有 m 字母是怎么处理的?

第一个问题:是把存储图片信息存储到二维码中,手机扫码可以识别吧?这个问题涉及到二维码的存储容量,理论上如果二维码的存储容量足够大,可把图片序列化成01的字符进行存储,扫描就可以识别。但二维码有不同的标准,不同标准下数据容量是不同的。建议不要存储图片,详情可查看知乎,了解一下:

m字母是一个图片,来自,只需要把想放的图放到已生成的二维码中间即可,但图片不宜过大,调试一下,用手机识别一下。有兴趣的朋友可以查看草榴二维码:

6.2 #17解答

疑问: 输入参数有 用户名和密码,那个是每次都需要用户输入的?还是通过扫描二维码获得的? 还是哪种方式来给 输入参数的用户名和密码赋值的。我想了解楼主是按哪种方式实现的呢?

首先要理解一下扫描登录的流程,【m】扫描二维码只获取相关【b】的唯一标识符信息,扫码之后,【m】(前提是【m】必须已经登录成功)发送用户名\密码\uuid到【s】进行一系列的验证;为了提高安全性,在【m】提交数据时,对密码进行md5时间戳加密。

6.3 #23解答

可以这样不 在手机端随机生成码 加密存在手机上并上传服务器 后端生成带有该码加时间的二维码 网页扫的时候对比登陆

要实现扫描登录,弄懂一个问题:为什么扫描二维码之后,提交给服务器的数据就是当前页面所需的呢?在本项目中,是通过signalr的固有通信connectionid来确认的。你所说的流程应该如下:

基于SignalR的消息推送与二维码扫描登录实现代码

在本流程图中,比方案中的步骤延长了;在step2中,会出现问题,如何将【m】推送过来的uuid推送到你看到的【b】端?显然缺少纽带。本方法是不可行的。

7、总结与下载

二维码应用比较广泛,记得去北京的故宫旁边的中山公园,里面的古树也有二维码,扫描可查看相关联信息。紧紧对于二维码而言就是存储有限信息,但就是这有限的信息,可以将庞大的信息系统连接一起,所用的应用不是前沿技术的突破,而是我们思考问题方式的转变、思维角度的变化。由于二维码具有信息存储的独特性,可在以下方面应用:

  • 信息获取(名片、地图、wifi密码、资料)
  • 网站跳转(跳转到微博、手机网站、网站)
  • 广告推送(用户扫码,直接浏览商家推送的视频、音频广告)
  • 手机电商(用户扫码、手机直接购物下单)
  • 防伪溯源(用户扫码、即可查看生产地;同时后台可以获取最终消费地)优惠促销(用户扫码,下载电子优惠券,抽奖)
  • 会员管理(用户手机上获取电子会员信息、vip服务)
  • 手机支付(扫描商品二维码,通过银行或第三方支付提供的手机端通道完成支付)

由于最近在做短信业务平台,将二维码应用到营销管理中,每个业务人员具有独立的推广二维码,客户扫码可进行短信测试,若注册成为会员则就是本业务人员的直属客户,可查看《》。

最后,上传《基于signalr的消息推送与二维码描登录实现》主要文件下载: