歡迎光臨
每天分享高質量文章

.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,它擁有行標列標三個屬性。我分別用RowIndexColIndexValue來表示。為了方便顯示,我加入了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中已存在的資訊進行對比。

對於每一個需要驗證的單元格:

  1. 在Cells中查詢具有同樣行列的單元格。

  2. 對比這兩者的值是否相等,一旦遇到不相等直接傳回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

已同步到看一看
贊(0)

分享創造快樂