《魔獸世界》的老玩家都知道,密保卡曾經被用於登入驗證,以保證賬號安全。今天我用.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 Cells { get; set; }
public Card(int rows = 5, int cols = 5)
{
Id = Guid.NewGuid();
Rows = rows;
Cols = cols;
Cells = new List();
}
}
但是考慮到序列化資料時候不希望字串有太多冗餘資訊,因此加入CellData屬性用於簡化Cells的資料表示。將Cells中的資料拼成一個以逗號分隔的字串中。以便於持久化的時候和Card型別的屬性一起包在一個Json字串中,看起來不會太長。
[JsonIgnore]
public List 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 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 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
朋友會在“發現-看一看”看到你“在看”的內容