.NET Core 仿魔兽世界密保卡实现
《魔獸世界》的老玩家都知道,密保卡曾經被用于登錄驗證,以保證賬號安全。今天我用.NET Core模擬了一把密保卡(也叫矩陣卡)的實現,分享給大家。
密保卡的原理
這是一張典型的魔獸世界密保卡。序列號用于綁定游戲賬號,而下面表格中的數字用于登錄驗證。
(圖片來源于網絡)
假設黑客已經知道了你的賬號和密碼,但是由于你綁定了一張密保卡。因此在登錄游戲時,游戲會隨機挑選其中一定數量(一般是3)個格子,要求輸入對應的數字,如A1=928,C8=985,B10=640。而因為黑客沒有拿到你的密保卡,因此他不知道矩陣中的數字,無法登錄你的賬號。即使抓取了幾次你的輸入,但由于每次登錄賬號被隨機選中的單元格組合都不同,因此對于一張7X12的密保卡,黑客需要抓(對不起我數學40分這個算不出來)次,才能完全掌握你的密保卡信息。然而賬號主人可以隨時更換密保卡,讓黑客前功盡棄。
.NET Core 實現
關注我博客的朋友可能知道,8年前我寫過這個話題,兩篇文章分別是:《C#仿魔獸世界密保卡簡單實現》與《C#仿魔獸世界密保卡OOP重構版》。
但是時代變了,獸人永不為奴,而.NET必將為王。8年了,當年文章里用的ASP.NET WebForm和巫妖王一起死在了冰封王座,.NET踏上了跨平臺的遠征,C# 的語法也突飛猛進的發展。榮耀屬于.NET Core,因此我把這盤冷飯拿出來炒一下,用現代化的手段重寫當年的老代碼,刷刷聲望。
最終效果如下,實現生成、序列號數據、重新加載數據以及驗證輸入:
源代碼傳送門:https://go.edi.wang/fw/5d12778d
Cell 類
Cell用于描述矩陣卡中的單元格。對于一個Cell,它擁有行標、列標和值三個屬性。我分別用RowIndex,ColIndex,Value來表示。為了方便顯示,我加入了ColumnName屬性,用于把列標顯示為英文字母(此處稍微和官方密保卡設計不一樣)。
為了約束Cell類型的使用,以上屬性設計為只讀,并只能從構造函數賦值。
public class Cell
{
? ? public int RowIndex { get; }
? ? public int ColIndex { get; }
? ? public ColumnCode ColumnName => (ColumnCode)ColIndex;
? ? public int Value { get; set; }
? ? public Cell(int rowIndex, int colIndex, int val = 0)
? ? {
? ? ? ? RowIndex = rowIndex;
? ? ? ? ColIndex = colIndex;
? ? ? ? Value = val;
? ? }
}
public enum ColumnCode
{
? ? A = 0,
? ? B = 1,
? ? C = 2,
? ? D = 3,
? ? E = 4
}
ColumnCode 可以根據自己需要拓展,目前我只寫了5個值。
Card 類
Card用于描述一張密保卡。因此除了包含一堆Cell以外,還得有卡號(Id),以及行數、列數等信息。起初的Card類型長這樣:
public class Card
{
? ? public Guid Id { get; set; }
? ? public int Rows { get; set; }
? ? public int Cols { get; set; }
? ? public List<Cell> Cells { get; set; }
? ? public Card(int rows = 5, int cols = 5)
? ? {
? ? ? ? Id = Guid.NewGuid();
? ? ? ? Rows = rows;
? ? ? ? Cols = cols;
? ? ? ? Cells = new List<Cell>();
? ? }
}
但是考慮到序列化數據時候不希望字符串有太多冗余信息,因此加入CellData屬性用于簡化Cells的數據表示。將Cells中的數據拼成一個以逗號分隔的字符串中。以便于持久化的時候和Card類型的屬性一起包在一個Json字符串中,看起來不會太長。
[JsonIgnore]
public List<Cell> Cells { get; set; }
public string CellData
{
? ? get
? ? {
? ? ? ? var vals = Cells.Select(c => c.Value);
? ? ? ? return string.Join(',', vals);
? ? }
}
生成密保卡數據
首先,根據行、列數量,生成一個二位數組,使用0-100的隨機值填充。值范圍可以根據自己需要改。
private static int[,] GenerateRandomMatrix(int rows, int cols)
{
? ? var r = new Random();
? ? var arr = new int[rows, cols];
? ? for (var row = 0; row < rows; row++)
? ? {
? ? ? ? for (var col = 0; col < cols; col++)
? ? ? ? {
? ? ? ? ? ? arr[row, col] = r.Next(0, 100);
? ? ? ? }
? ? }
? ? return arr;
}
然后將生成的值按行、列分配給Cells屬性
private void FillCellData(int[,] array)
{
? ? for (var row = 0; row < Rows; row++)
? ? {
? ? ? ? for (var col = 0; col < Cols; col++)
? ? ? ? {
? ? ? ? ? ? var c = new Cell(row, col, array[row, col]);
? ? ? ? ? ? Cells.Add(c);
? ? ? ? }
? ? }
}
在Console上打印密保卡信息也很簡單,用兩個循環分別控制行、列的輸出即可。(當然,這只是demo意圖,真實使用場景用不著console)
private static void PrintCard(Card card)
{
? ? Console.WriteLine("? |\tA\tB\tC\tD\tE\t");
? ? Console.WriteLine("----------------------------------------------");
? ? var i = 0;
? ? for (var k = 0; k < card.Rows; k++)
? ? {
? ? ? ? Console.Write(k + " |\t");
? ? ? ? for (var l = 0; l < card.Cols; l++)
? ? ? ? {
? ? ? ? ? ? Console.Write(card.Cells[i].Value + "\t");
? ? ? ? ? ? i++;
? ? ? ? }
? ? ? ? Console.WriteLine();
? ? }
}
加載Cells數據
除了生成數據,我們還要支持加載既有數據到Cells中。
因為之前被簡化過的Cells數據是個以逗號分割的string字符串,因此我們需要把它拆成數組,并轉換類型回int,然后利用之前寫的FillCellData()方法填充到Cells屬性里。
public Card LoadCellData(string strMatrix)
{
? ? var tempArrStr = strMatrix.Split(',');
? ? if (tempArrStr.Length != Rows * Cols)
? ? {
? ? ? ? throw new ArgumentException(
? ? ? ? ? ? "The number of elements in the matrix does not match the current card cell numbers.", nameof(strMatrix));
? ? }
? ? var arr = new int[Rows, Cols];
? ? var index = 0;
? ? for (var row = 0; row < Rows; row++)
? ? {
? ? ? ? for (var col = 0; col < Cols; col++)
? ? ? ? {
? ? ? ? ? ? arr[row, col] = int.Parse(tempArrStr[index]);
? ? ? ? ? ? index++;
? ? ? ? }
? ? }
? ? FillCellData(arr);
? ? return this;
}
隨機選擇與驗證
同樣使用Random類型,在給定的行列范圍內隨機選擇給定數量的單元格,但不從Cells中取,因為我們無需返回單元格的值。在服務器/客戶端場景下,驗證始終應該放在服務器上做,不要在客戶端驗證值,因此不要返回值。
public IEnumerable<Cell> PickRandomCells(int howMany)
{
? ? var r = new Random();
? ? for (var i = 0; i < howMany; i++)
? ? {
? ? ? ? var randomCol = r.Next(0, Cols);
? ? ? ? var randomRow = r.Next(0, Rows);
? ? ? ? var c = new Cell(randomRow, randomCol);
? ? ? ? yield return c;
? ? }
}
由于返回的Cell信息包含了行、列,因此當用戶輸入值之后,我們可以與Cells中已存在的信息進行對比。
對于每一個需要驗證的單元格:
在Cells中查找具有同樣行列的單元格。
對比這兩者的值是否相等,一旦遇到不相等直接返回false,無需再驗證下一個單元格。
通常這樣的操作某些語言就得寫好幾個循環,不僅麻煩,還容易下標搞錯數組越界然后996。好在C#的LINQ一行就寫完了:(換行只是代碼格式)
public bool Validate(IEnumerable<Cell> cellsToValidate)
{
? ? return (
? ? ? ? from cell in cellsToValidate
? ? ? ? let thisCell = Cells.Find(p => p.ColIndex == cell.ColIndex
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?&& p.RowIndex == cell.RowIndex)
? ? ? ? select thisCell.Value == cell.Value)
? ? ? ? .All(matches => matches);
}
完整代碼傳送門:https://go.edi.wang/fw/5d12778d
總結
以上是生活随笔為你收集整理的.NET Core 仿魔兽世界密保卡实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用Jenkins部署.Net Core
- 下一篇: 浅谈C#泛型