文章出處

1 概要說明

    使用微信掃描登錄相信大家都不會陌生吧,二維碼與手機結合產生了不同應用場景,基于二維碼的應用更是比較廣泛。為了滿足ios、android客戶端與web短信平臺的結合,特開發了基于SinglarR消息推送機制的掃描登錄。本系統涉及到以下知識點:

    SignalRhttp://signalr.net/ 這官網,ASP.NET SignalR 是為 ASP.NET 開發人員提供的一個庫,可以簡化開發人員將實時 Web 功能添加到應用程序的過程。實時 Web 功能是指這樣一種功能:當所連接的客戶端變得可用時服務器代碼可以立即向其推送內容,而不是讓服務器等待客戶端請求新的數據。

    二維碼:使用的QRCode類庫,https://github.com/jeromeetienne/jquery-qrcode

    MVC5:開發環境是基于MVC5

image

2、系統關系圖

    在實現本功能前,有點不是太確定能否拿下。

    所謂萬事開頭難,通過查詢想資料及自己歸納分析:系統涉及到手機客戶端、瀏覽者、服務端,實現掃描登錄也就是三者之間是如何協調工作的。通過axure畫出如下關系圖:

X)`F@Q9AS6GUKPE@P[PH$RV

移動客戶端、瀏覽者、服務端三者協作關系圖

    【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…,打開以下界面:

image

    在Browser 標簽下輸入SignalR,查詢到Microsoft.AspNet.SignalR

image

    找到對應的項目,點擊“Install”安裝按鈕即可引用相關類庫,同時應用下載相關js庫。

    關于SignalR的知識點,可以到官網 http://www.asp.net/signalr 進行深入學習。

3.2 服務端SignalR實現

    服務端要向客戶端推送UUID,對于UUID唯一標識符,具有重要特性:(1)有時間限制,120秒之內掃碼有效;(2)具有一定的狀態。對應的聲明周期就是:生成—>推送—>狀態判斷—>手機端掃描—>驗證UUID—>狀態判斷—>銷毀等系列過程。

    服務端的核心代碼將單獨建立一個項目去實現:

image

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 二維碼的生成   

    二維碼類庫選擇https://github.com/jeromeetienne/jquery-qrcode 一個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 class="ewmcode_login" id="ewmcode_login">
                                            <div class="codeText">安全登錄 防止被盜</div>
                                            <div id="divQRCode" class="codebox" style="background:none;"></div>
                                            <div class="coderemindText">掃一掃登錄</div>
                                        </div>
                        <!--二維碼登錄結束-->

    呈現二維碼:

                $("#divQRCode").html("");
                $('#divQRCode').qrcode({ width: 180, height: 180, correctLevel: 0, text: codeUUID });

    通過3與4,可實現具有180秒生命周期二維碼的生成,對于不同的瀏覽者,生成的二維碼是不同的,效果如下:

a

4.2 二維碼存儲的是什么

    二維碼生成了,但是存儲的是什么呢?首先我們看下以下的二維:

image U}}3TK4Q`JAGEZ4}3DUAQLS

hbuilder官網

千牛電腦客戶端二維碼登錄界面

    顯然,掃描這兩個圖片上的二維碼會得到不同的結果。對某些二維碼的解碼要對應配套的客戶端才能起到作用,否則用其他工具解析出來也就是字符串。

    在本系統中,二維碼存儲的是一個json對象,格式為:

{"connectionid":"19c12e95-26d7-410c-8292-2a3afdd1a4da","uuid":"a04702df-6a52-4e1c-be8b-9b3dbeef4d72","islogin":0,"isvalid":1}
connectionid:客戶端與SignalR聯系的id,其格式為Guid

    • uuid:對應connectionid產生的一個唯一標識符,其格式為Guid
    • islogin:當前connectionid連接是否已登錄,1—>表示登錄,0—>未登錄
    • isvalid:當前connectionid對應的uuid是否有效,1—>表示有效,0—>表示失效

    手機客戶端掃描之后,可根據這些參數情況進行判斷,是否向服務端發送請求。在做掃描應用(比如掃描登錄)時,要依據業務場景進行消息傳遞,生成對應二維碼,并不局限于json對象、url地址等。

    總結下來,二維碼應用場景,如下圖:

(NRX6LU]76HPY@KWVCJ_R$7

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的字符進行存儲,掃描就可以識別。但二維碼有不同的標準,不同標準下數據容量是不同的。建議不要存儲圖片,詳情可查看知乎,了解一下:http://www.zhihu.com/question/20387257

     M字母是一個圖片,來自http://www.dcloud.io/,只需要把想放的圖放到已生成的二維碼中間即可,但圖片不宜過大,調試一下,用手機識別一下。有興趣的朋友可以查看草榴二維碼:http://cli.im/

6.2 #17解答

疑問: 輸入參數有 用戶名和密碼,那個是每次都需要用戶輸入的?還是通過掃描二維碼獲得的? 還是哪種方式來給 輸入參數的用戶名和密碼賦值的。
我想了解樓主是按哪種方式實現的呢?

    首先要理解一下掃描登錄的流程,【M】掃描二維碼只獲取相關【B】的唯一標識符信息,掃碼之后,【M】(前提是【M】必須已經登錄成功)發送用戶名\密碼\UUID到【S】進行一系列的驗證;為了提高安全性,在【M】提交數據時,對密碼進行md5時間戳加密。

6.3 #23解答

可以這樣不 在手機端隨機生成碼 加密存在手機上并上傳服務器 后端生成帶有該碼加時間的二維碼 網頁掃的時候對比登陸

    要實現掃描登錄,弄懂一個問題:為什么掃描二維碼之后,提交給服務器的數據就是當前頁面所需的呢?在本項目中,是通過SignalR的固有通信connectionid來確認的。你所說的流程應該如下:

C@9HSTD8O3Z7HA[{W8U@MZL

    在本流程圖中,比方案中的步驟延長了;在Step2中,會出現問題,如何將【M】推送過來的UUID推送到你看到的【B】端?顯然缺少紐帶。本方法是不可行的。

7、總結與下載

    二維碼應用比較廣泛,記得去北京的故宮旁邊的中山公園,里面的古樹也有二維碼,掃描可查看相關聯信息。緊緊對于二維碼而言就是存儲有限信息,但就是這有限的信息,可以將龐大的信息系統連接一起,所用的應用不是前沿技術的突破,而是我們思考問題方式的轉變、思維角度的變化。由于二維碼具有信息存儲的獨特性,可在以下方面應用:

    • 信息獲取(名片、地圖、WIFI密碼、資料)
    • 網站跳轉(跳轉到微博、手機網站、網站)
    • 廣告推送(用戶掃碼,直接瀏覽商家推送的視頻、音頻廣告)
    • 手機電商(用戶掃碼、手機直接購物下單)
    • 防偽溯源(用戶掃碼、即可查看生產地;同時后臺可以獲取最終消費地)
    • 優惠促銷(用戶掃碼,下載電子優惠券,抽獎)
    • 會員管理(用戶手機上獲取電子會員信息、VIP服務)
    • 手機支付(掃描商品二維碼,通過銀行或第三方支付提供的手機端通道完成支付)

    由于最近在做短信業務平臺,將二維碼應用到營銷管理中,每個業務人員具有獨立的推廣二維碼,客戶掃碼可進行短信測試,若注冊成為會員則就是本業務人員的直屬客戶,可查看《二維碼在短信業務應用的初步構思》。

    最后,上傳《基于SignalR的消息推送與二維碼描登錄實現》主要文件下載:http://files.cnblogs.com/files/zsy/signalr%E4%B8%8Eqrcode.rar


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()