溫故知新,signalR、RSA加密、ConcurrentQueue佇列

2023-06-27 15:00:33

這是一個使用者密碼非同步加解密的Demo,包含了RSA非對稱加密,ConcurrentQueue執行緒安全佇列使用,signalR實時推播加解密資訊等內容。

說在前面的話

距離上次更新已然快過去一年了,這中間日子裡進入了非常繁忙的專案迭代開發中,時至今日終於有空停下來寫一寫之前的部落格計劃,續更後的第一篇,溫故知新,用一個Demo介紹技術點的落地實操,如有不同意見評論區留下你的想法,Of course ,如果你槓精就是你對。

依照慣例,原始碼在文末,需要自取~

不同解決方案?

直接看看執行效果,看完之後,你是否會與我選擇同樣的技術方案呢?

實現效果

一個使用者列表,可以展示所有使用者的資訊,需要對其中的密碼進行加密,加密使用非對稱加密,點選加密按鈕以及解密按鈕,實時地可以看到加密和解密的資料。

乍一看看可太簡單,RSA的非對稱加密,網上直接ctrl C V一套已有的就完事,就是要解決實時性的問題。

這裡我選擇了SignlR做實時推播,然後為了可以看到效果與效能考慮,使用了ConcurrentQueue執行緒安全佇列,控制加解密的速度。

好了磚頭丟擲來了,看各位大佬騷操作

拉程式碼看程式碼

如果你不曉得這幾個技術點該如何加入你的框架中,或者知道一些概念,但是沒用過,下文適合你食用!

RSA加解密

非對稱加密的使用現在已然太多範例,提供的專案原始碼中,專門提供了一個可以直接跑的Demo,拉下來F5,偵錯一下完事,貼心的為你提供了Web API介面與測試頁面。

QueueDemo 作為WebApi啟動, RSAProcessing.MVC 作為前端頁面啟動

一下就是核心 RSA加密處理程式的核心程式碼,Ctrl C V之後, 使用 RSAProcessing.GenerateKeys(out string publicKey, out string pricateKey); 即可生成公鑰和祕鑰,加密解密使用方式同上。

    /// <summary>
    /// RSA加密處理程式
    /// </summary>
    public static class RSAProcessing
    {
        /// <summary>
        ///  生成RSA金鑰對
        /// </summary>
        /// <param name="publicKey"></param>
        /// <param name="privateKey"></param>
        public static void GenerateKeys(out string publicKey, out string privateKey)
        {
            using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
            {
                publicKey = rsa.ToXmlString(false);
                privateKey = rsa.ToXmlString(true);
            }
        }

        /// <summary>
        ///  使用公鑰加密文字
        /// </summary>
        /// <param name="plainText"></param>
        /// <param name="publicKey"></param>
        /// <returns></returns>
        public static string Encrypt(string plainText, string publicKey)
        {
            byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);

            using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
            {
                rsa.FromXmlString(publicKey);
                byte[] encryptedBytes = rsa.Encrypt(plainBytes, false);
                return Convert.ToBase64String(encryptedBytes);
            }
        }

        /// <summary>
        ///  使用私鑰解密文字
        /// </summary>
        /// <param name="encryptedText"></param>
        /// <param name="privateKey"></param>
        /// <returns></returns>
        public static string Decrypt(string encryptedText, string privateKey)
        {
            byte[] encryptedBytes = Convert.FromBase64String(encryptedText);

            using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
            {
                rsa.FromXmlString(privateKey);
                byte[] decryptedBytes = rsa.Decrypt(encryptedBytes, false);
                return Encoding.UTF8.GetString(decryptedBytes);
            }
        }

......

SignlR的實時推播

SignlR是什麼?這個不用我去百度,ChatGPT可以給你一個簡潔有效的答案。

看到這裡,可能你會有和我一樣感受,這個作為實時通訊,是不是我直接做一個仿QQ和微信的聊天工具來,做大做強? 看看 GPT的回答:

可以看到ChatGPT可以給予我們絕大部分答案,但是這裡給大家補充一下:

  • 通訊方式的選擇取決於瀏覽器版本以及伺服器端和使用者端能力範圍內的最佳通訊方式,通常是WebSocket > Server-Sent Events > Long Poling
  • SignalR不僅僅可線上聊天的通訊軟體,做事件推播也非常好用,例如直播或者視訊的觀看人數統計等等

SignlR的.net 6程式碼實現

使用之前,一定先去看看微軟的官方Demo!
https://learn.microsoft.com/zh-cn/aspnet/core/signalr/introduction?view=aspnetcore-6.0

以上是簡易的兩個Web應用的架構圖,5102的WebApi作為伺服器端提供資料,MVC 應用作為使用者端接受資料,以及傳送SignalR連線,因為他們是兩個Web應用,所屬的web域不同,使用者端請求伺服器端,需要伺服器端設定允許跨域請求。

註冊服務

在5102的伺服器端中註冊SignalR ,並且設定允許跨域

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(builder =>
    {
        builder.AllowAnyMethod()
               .AllowAnyHeader()
               .WithOrigins("http://localhost:5067")
               .AllowCredentials(); // 允許包含憑據;
    });
});
builder.Services.AddSignalR();

...註冊其他的服務...  

var app = builder.Build();

app.UseCors();
app.MapHub<QueueHub>("/queueHub");  // 設定路由

設定訊息處理中心

在永續性連線的基礎上,SignalR提供了一個更高層次的抽象層:Hub,基於javascript的靈活性和C#的動態特性,Hub是一個至關重要的開發模式,它消弭了使用者端和伺服器端這兩個獨立的物理環境之間的界限。 在Web環境中最通用的使用模式允許我們透明地在使用者端和伺服器端之間進行方法呼叫。

簡單的說,就是雙向RPC,即可以直接從使用者端呼叫伺服器端的方法,同時伺服器端也可以呼叫使用者端的方法。

using Microsoft.AspNetCore.SignalR;

namespace QueueDemo.Core
{
    /// <summary>
    /// 佇列的signalR匯流排
    /// </summary>
    public class QueueHub : Hub
    {
        /// <summary>
        /// 加入連線的事件
        /// </summary>
        /// <returns></returns>
        public override async Task OnConnectedAsync()
        {
            GlobalUserInfo.Clients = Clients;
            await base.OnConnectedAsync();
        }

        /// <summary>
        /// signalR推播加密資訊
        /// </summary>
        /// <param name="userId">使用者id</param>
        /// <param name="message">加密資料</param>
        /// <returns></returns>
        public async Task SendEncryptDequeue(int userId, string message)
        {
            await GlobalUserInfo.Clients.All.SendAsync("ReceiveEncrypt", userId, message);
        }

        /// <summary>
        ///  signalR推播解密資訊
        /// </summary>
        /// <param name="userId">使用者id</param>
        /// <param name="message">解密資料</param>
        /// <returns></returns>
        public async Task SendDecryptDequeue(int userId, string message)
        {
            await GlobalUserInfo.Clients.All.SendAsync("ReceiveDecrypt", userId, message);
        }
    }
}

使用者端JS設定

使用者端主要需要做的就是,與SignalR伺服器端建立連線,接受伺服器端推播過來的資料。

<script>
    let queueHost = 'http://localhost:5102';

    // 建立signalR連線
    var connection = new signalR.HubConnectionBuilder().withUrl(queueHost + "/queueHub").build();

    // 接收到  ReceiveEncrypt 的訊息
    connection.on("ReceiveEncrypt", function (userId, message) {
        console.log(userId);
        console.log(message);
        // 使用特定 id 來定位並修改文字內容
        $('#en_' + userId).text(message);
    });

    //  接收到  ReceiveDecrypt 的訊息
    connection.on("ReceiveDecrypt", function (userId, message) {
        console.log(userId);
        console.log(message);
        // 使用特定 id 來定位並修改文字內容
        $('#de_' + userId).text(message);
    });

    // 連線成功
    connection.start().then(function () {
        console.log("Connection Success")
    }).catch(function (err) {
        return console.error(err.toString());
    });

</script>

ConcurrentQueue佇列連線使用者端與伺服器端

SignalR與QueueHub的連線已然搞定,就是如何觸發推播加解密資訊。

這裡使用的方案是ConcurrentQueue佇列,將所有的使用者資訊推播到加密佇列(&解密佇列)中,出隊一個UserInfo,就加密(&解密)一個使用者資訊,隨後利用SignalR推播一個加密解密資訊。

再說到佇列,大家熟知都是RabbitMQ ,Kafka , RocketMQ,然而在實戰中,急著要用一個佇列,如果此時用上RabbitMQ,那麼還需要額外部署一個應用,開防火牆等等,這一套搞下來,加上走流程快的話一週過去了,此時用一個記憶體佇列就是最合適的,用執行緒加記憶體佇列可以做一個低配版的rabbitmq,先實現業務需求,再後期去升級。

初始化佇列

依上述所言,佇列為了簡單易用,在StartUp中建立兩個執行緒去跑。

// 建立並啟動後臺任務            
UserQueueHandler ledgerQueue = new(new QueueHub());
Task task = Task.Run(() => ledgerQueue.DeProcessQueue(builder.Services, GlobalUserQueue.DecryptCancelToken.Token));
Task task2 = Task.Run(() => ledgerQueue.EnProcessQueue(builder.Services, GlobalUserQueue.EncryptCancelToken.Token));

兩個全域性的靜態變數儲存佇列的設定,並且建立兩個中斷迴圈的開關。

  /// <summary>
  /// 全域性使用者佇列初始化
  /// </summary>
  public static class GlobalUserQueue
  {
      /// <summary>
      /// 解密佇列 退出迴圈開關
      /// </summary>
      public static CancellationTokenSource DecryptCancelToken = new();
      /// <summary>
      /// 加密佇列 退出迴圈開關
      /// </summary>
      public static CancellationTokenSource EncryptCancelToken = new();

      /// <summary>
      /// 解密佇列
      /// </summary>
      public static ConcurrentQueue<DecryptRequest> DecryptQueue = new();

      /// <summary>
      /// 加密佇列
      /// </summary>
      public static ConcurrentQueue<EncryptRequest> EncryptQueue = new();
  }

初始化使用者佇列處理程式

    /// <summary>
    /// 使用者佇列處理程式
    /// </summary>
    public class UserQueueHandler
    {
        private QueueHub _queueHub;

        /// <summary>
        /// 注入佇列匯流排
        /// </summary>
        /// <param name="queueHub"></param>
        public UserQueueHandler(QueueHub queueHub)
        {
            _queueHub = queueHub;
        }

        /// <summary>
        /// 啟動解密佇列
        /// </summary>
        /// <param name="services">註冊服務</param>
        /// <param name="cancellationToken">退出Token控制器</param>
        /// <returns></returns>
        public async Task DeProcessQueue(IServiceCollection services, CancellationToken cancellationToken)
        {
            try
            {
                var serviceProvider = services.BuildServiceProvider();
                // Rsa加解密服務
                var rsaService = serviceProvider.GetRequiredService<IRSAService>();
                await Console.Out.WriteLineAsync($"Decrypt ProcessQueue Start! ");

                while (true)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    if (cancellationToken.IsCancellationRequested)
                    {
                        break;
                    }

                    // 解密佇列出隊
                    if (GlobalUserQueue.DecryptQueue.TryDequeue(out DecryptRequest deRequest))
                    {
                        await Console.Out.WriteLineAsync($"DeProcessQueue UserIndexId -- {deRequest.UserIndex} -- {JsonConvert.SerializeObject(deRequest)} ");
                        try
                        {
                            // 解密
                            var deScryptRsp = rsaService.Decrypt(deRequest);
                            if (deScryptRsp != null)
                            {
                                var userInfo = GlobalUserInfo.UserInfos.First(x => x.Index == deScryptRsp.UserIndex);
                                userInfo.DecryptedPwd = deScryptRsp.DecryptedPwd;

                                // 推播解密資訊到前端
                                await _queueHub.SendDecryptDequeue(userInfo.UserId, userInfo.DecryptedPwd);
                                await Console.Out.WriteLineAsync($"DeProcessQueue Success! UserId--{userInfo.UserId} UserIndex--{deScryptRsp.UserIndex} ");
                            }
                            await Task.Delay(1000);
                        }
                        catch (Exception ex)
                        {
                            await Console.Out.WriteLineAsync($"DeProcessQueue Error --{JsonConvert.SerializeObject(ex)} ");
                        }
                    }
                    else
                    {
                        // 佇列中無數量 則休眠10秒
                        await Task.Delay(10000);
                    }
                }
            }
            catch (Exception ex)
            {
                await Console.Out.WriteLineAsync(ex.Message);
                throw;
            }
        }
}

這個方法就是觸發推播加解密的資訊,連線使用者端和伺服器端的核心。

  • ConcurrentQueue出隊使用的是 GlobalUserQueue.DecryptQueue.TryDequeue(out DecryptRequest deRequest)
  • 利用serviceProvider.GetRequiredService<IRSAService>(); 獲取RSA解密服務,然後再呼叫解密方法。
  • 拿到解密之後的資訊之後,使用queueHub的方法 await _queueHub.SendDecryptDequeue(userInfo.UserId, userInfo.DecryptedPwd); 推播解密資料
  • 推播資料主要是SignalR的方法 await GlobalUserInfo.Clients.All.SendAsync("ReceiveDecrypt", userId, message);

總結

稍微總結一下,

  • Demo整合了RSA加密、SignalR推播、記憶體版的佇列。
  • 講解了一下SignalR的用法以及注意事項
  • 記憶體版的佇列在Web應用中的優勢
  • 有更好的更快速的解決方案評論區留下資訊
    專案拉取下來,在解決方案設定中,同時啟動兩個專案即可。

    原始碼倉庫 https://github.com/OrzCoCo-Y/QueueDemo

參考資料

【微軟檔案】 https://learn.microsoft.com/zh-cn/aspnet/core/signalr/introduction?view=aspnetcore-6.0
【ChatGPT】