首頁

目前文章總數:157 篇

  

最後更新:2024年 12月 07日

0044. .Net Core Website 實現圖形驗證碼驗證登入

日期:2023年 07月 22日

標籤: C# Asp.net Core Web MVC Web CAPTCHA

摘要:C# 學習筆記


應用所需:1. Visual Studio 2022
     2. .net core Web專案 (Website MVC示範)
範例檔案:連結
解決問題:如何在網站的登入系統中增加圖形驗證碼驗證
應用層面:很常見的一種驗證機制,隨著時代演進,現在主要是防止機器人太快重複呼叫某些網站功能
基本介紹:本篇分為3大部分。
第一部分:範例專案說明
第二部分:Demo錯誤展示(Session key重複)
第三部分:Demo正確展示結果






第一部分:範例專案說明

Step 1:範例專案架構


1. 檢視頁面 這邊用兩個登入頁,管理者、一般用戶,為了第二部分說明Session重複的問題
2. 控制器 頁面檢視的控制器,說明如何使用圖形驗證碼、並建立Session驗證
3. 共用函式邏輯 CaptchaUtil 是產生圖片驗證碼的函式 ; SessionUtil 是紀錄圖片驗證的Session Key
4. 客製化屬性 CaptchaBindingAttribute 實現圖形驗證碼Action對Action的關係,避免第二部分的問題發生




Step 2:檢視頁面

以下是用戶登入的畫面,需要輸入帳號、密碼、驗證碼


@model WebSiteCaptchaLoginExample.Models.LoginViewModel
@{
    ViewData["Title"] = "用戶-登入";
}
<h1>@ViewData["Title"]</h1>

<p>用戶登入頁面</p>
<form method="post" asp-controller="Home" asp-action="UserLoginVerify">
    <table>
        <tr>
            <td>帳號:</td>
            <td><input type="text" asp-for="SubmitData.Account" /></td>
            <td></td>
        </tr>
        <tr>
            <td>密碼:</td>
            <td><input type="text" asp-for="SubmitData.Password" /></td>
            <td></td>
        </tr>
        <tr>
            <td>驗證碼:</td>
            <td><input type="text" asp-for="SubmitData.InputCaptcha" /></td>
            <td></td>
        </tr>
        <tr>
            <td></td>
            <td><img style='width:180px;height:60px;border:1px solid #ccc;' id='captchaImage' class='cursor-pointer absolute r-0 t-0' src='@Model.Chapcha' alt="验证码"></td>
            <td></td>
        </tr>
        <tr>
            <td></td>
            <td> <button type="submit">用戶登入</button></td>
            <td></td>
        </tr>
    </table>
</form>


以下是管理員的登入,與用戶登入相同,但 asp-action=”AdminLoginVerify”


@model WebSiteCaptchaLoginExample.Models.LoginViewModel
@{
    ViewData["Title"] = "管理員-登入";
}
<h1>@ViewData["Title"]</h1>

<p>管理員登入頁面</p>
<form method="post" asp-controller="Home" asp-action="AdminLoginVerify">
    <table>
        <tr>
            <td>帳號:</td>
            <td><input type="text" asp-for="SubmitData.Account" /></td>
            <td></td>
        </tr>
        <tr>
            <td>密碼:</td>
            <td><input type="text" asp-for="SubmitData.Password" /></td>
            <td></td>
        </tr>
        <tr>
            <td>驗證碼:</td>
            <td><input type="text" asp-for="SubmitData.InputCaptcha" /></td>
            <td></td>
        </tr>
        <tr>
            <td></td>
            <td><img style='width:180px;height:60px;border:1px solid #ccc;' id='captchaImage' class='cursor-pointer absolute r-0 t-0' src='@Model.Chapcha' alt="验证码"></td>
            <td></td>
        </tr>
        <tr>
            <td></td>
            <td> <button type="submit">管理者登入</button></td>
            <td></td>
        </tr>
    </table>
</form>



Step 3:控制器

實現了一共4個
1. 用戶登入

/// <summary>
/// 用戶登入
/// </summary> 
CaptchaBinding(CaptchaBindingName = nameof(UserLogin), Generate = true)]
public IActionResult UserLogin()
{            
    //使用 CaptchaUtil.GetCapChatImg 產生Base64的驗證碼圖形
    var result = new LoginViewModel()
    {
        Chapcha = $@"data:image/jpeg;base64,{Convert.ToBase64String(CaptchaUtil.GetCapChatImg(SessionUtil.CaptCha))}",
    };
    return View(result);
}


2. 用戶登入按鈕-進行登入

/// <summary>
/// 用戶登入按鈕-進行登入
/// </summary>        
[HttpPost]
[CaptchaBinding(CaptchaBindingName = nameof(UserLogin))]
public IActionResult UserLoginVerify(LoginViewModel inputData)
{
    //進行登入時會依照綁定的"設置名稱" 取得對應頁面的-圖形驗證碼,就不會造成A頁面卻吃到B頁面驗證碼的錯誤
    if (inputData.SubmitData.InputCaptcha == SessionUtil.CaptCha)
        return Ok("驗證成功");
    else
        return Ok("登入失敗");
}


3. 管理者登入

/// <summary>
/// 管理者登入
/// </summary>        
[CaptchaBinding(CaptchaBindingName = nameof(AdminLogin), Generate = true)]        
public IActionResult AdminLogin()
{
    var result = new LoginViewModel()
    {
        Chapcha = $@"data:image/jpeg;base64,{Convert.ToBase64String(CaptchaUtil.GetCapChatImg(SessionUtil.CaptCha))}",
    };
    return View(result);
}


4. 管理者登入按鈕-進行登入

[HttpPost]
[CaptchaBinding(CaptchaBindingName = nameof(AdminLogin))]
public IActionResult AdminLoginVerify(LoginViewModel inputData)
{
    if (inputData.SubmitData.InputCaptcha == SessionUtil.CaptCha)
        return Ok("驗證成功");
    else
        return Ok("登入失敗");
}



Step 4:共用函式邏輯-產生圖形驗證碼

CaptchaUtil.cs 實現了產生圖形驗證碼,只要將指定的字串放進即可生成 byte[] 資料流
以下是生成的圖片樣式:

/// <summary>
/// 產生驗證碼圖片
/// </summary>
/// <param name="captCha">驗證碼文字</param>
/// <param name="bmpWidth">產生圖片寬</param>
/// <param name="bmpHeight">產生圖片高</param>
/// <param name="noisePotCount">雜點數量</param>
/// <param name="noiseLineCount">雜訊線條數量</param>
/// <returns></returns>
public static byte[] GetCapChatImg(string captCha,
    int bmpWidth = 200,
    int bmpHeight = 80,
    int noisePotCount = 60,
    int noiseLineCount = 90)
{
    using (Bitmap bmp = new Bitmap(bmpWidth, bmpHeight))
    {
        int xAxis1;
        int yAxis1;
        int xAxis2;
        int yAxis2;
        var random = new Random();
        Graphics graphItem = Graphics.FromImage(bmp);
        var fontStyle = Enum.GetValues(typeof(FontStyle)).Cast<FontStyle>().ToArray();
        var randomFontStyle = fontStyle[random.Next(0, fontStyle.Length)];
        Font font = new Font("Courier New", random.Next(36, 46), randomFontStyle);
        //設定圖片背景
        graphItem.Clear(Color.White);
        //產生雜點
        var noiseWidth = bmpWidth / 4;
        var noiseHeight = bmpHeight / 4;
        for (int noisePots = 0; noisePots < noisePotCount; noisePots++)
        {
            xAxis1 = random.Next(0, bmp.Width);
            yAxis1 = random.Next(0, bmp.Height);
            bmp.SetPixel(xAxis1, yAxis1, Color.Brown);
        }
        //產生擾亂弧線
        for (int noiseLines = 0; noiseLines < noiseLineCount; noiseLines++)
        {
            xAxis1 = random.Next(bmp.Width - noiseWidth);
            yAxis1 = random.Next(bmp.Height - noiseHeight);
            xAxis2 = random.Next(1, noiseWidth);
            yAxis2 = random.Next(1, noiseHeight);
            var startAngle = random.Next(0, 90);
            var sweepAngle = random.Next(-270, 270);
            graphItem.DrawArc(new Pen(Brushes.Gray), xAxis1, yAxis1, xAxis2, yAxis2, startAngle, sweepAngle);
        }
        var randomDrawX = random.Next(3, 30);
        var randomDrawY = random.Next(3, 12);
        graphItem.DrawString(captCha, font, GetRandomBrushes(), randomDrawX, randomDrawY);
        using (var memoryStream = new MemoryStream())
        {
            bmp.Save(memoryStream, ImageFormat.Gif);
            memoryStream.Close();
            return memoryStream.GetBuffer();
        }
    }
    //筆刷顏色,為了用戶體驗與安全性,RGB隨機性在96~160
    Brush GetRandomBrushes(byte startRGB = 96, byte endRGB = 160)
    {
        Random r = new Random();
        int red = r.Next(startRGB, endRGB + 1);
        int green = r.Next(startRGB, endRGB + 1);
        int blue = r.Next(startRGB, endRGB + 1);
        return new SolidBrush(Color.FromArgb(red, green, blue));
    }
}


另外也提供自動生成隨機字串的函式


private const string baseNumberWord = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

/// <summary>
/// 取得指定長度的字串
/// </summary>        
public static string GetRandomCaptcha(int length = 5)
{
    var random = new Random();
    var strBuilder = new StringBuilder(32);
    for (var index = 0; index < length; index++)
    {
        strBuilder.Append(baseNumberWord.Substring(random.Next(baseNumberWord.Length), 1));
    }
    return strBuilder.ToString();
}



Step 5:共用函式邏輯-紀錄Session

SessionUtil.cs 目的是紀錄產生後的圖形驗證碼,以便於在用戶提交後可以比對

1. CaptChaBindName 圖形驗證碼綁定名稱 - 使驗證碼與某些Action具有關聯性
2. CaptCha 圖形驗證碼


        
/// <summary>
/// 圖形驗證碼綁定名稱 - 使驗證碼與某些Action具有關聯性
/// </summary>
public static string CaptChaBindName
{
    get
    {
        return _httpContextAccessor?.HttpContext?.Session.GetString("CaptChaBindName") ?? string.Empty;
    }
    set
    {
        _httpContextAccessor?.HttpContext?.Session.SetString("CaptChaBindName", value);
    }
}
/// <summary>
/// 圖形驗證碼
/// </summary>
public static string CaptCha
{
    get
    {
        return _httpContextAccessor?.HttpContext?.Session.GetString($@"{CaptChaBindName}_CaptCha") ?? string.Empty;
    }
    set
    {
        _httpContextAccessor?.HttpContext?.Session.SetString($@"{CaptChaBindName}_CaptCha", value);
    }
}



Step 6:客製化屬性

宣告中 [AttributeUsage(AttributeTargets.Method)] 表示先過濾Http Method(Get、Post、Put …)
並且支援攜帶參數 CaptchaBindingName 與 Generate 實現Action間,確認是共用同一組驗證碼


[AttributeUsage(AttributeTargets.Method)]
public class CaptchaBindingAttribute : Attribute, IActionFilter
{
    /// <summary>
    /// 圖形驗證碼綁定名稱
    /// </summary>
    public string CaptchaBindingName { get; set; } = string.Empty;
    public bool Generate { get; set; } = false;
    //1. 有綁定 [CaptchaBinding] 的Action才會觸發
    public void OnActionExecuting(ActionExecutingContext context)
    {
        try
        {
            //2. 配置當前"設定名稱"
            var attribute = context.ActionDescriptor.EndpointMetadata.OfType<CaptchaBindingAttribute>().FirstOrDefault();
            if (attribute != null)
            {
                SessionUtil.CaptChaBindName = attribute.CaptchaBindingName;
                //3. 配置時為產生 Generate = true 才會生成字符串
                if (Generate)
                {
                    SessionUtil.CaptCha = CaptchaUtil.GetRandomCaptcha(5);
                }
            }
        }
        catch
        {
            //直接拋棄,不影響正式流程
        }
    }
    public void OnActionExecuted(ActionExecutedContext context)
    {
    }
}





第二部分:Demo錯誤展示(Session重複)

Step 1:執行程式

範例檔案:範例檔案,下載後,啟動程式後
開啟兩個頁籤,先開用戶登入(不要關閉) -> 然後再開啟管理員登入(不要關閉)


Step 2:用戶登入

這時進行用戶登入,帳號、密碼隨意填寫,但是圖形驗證碼填入正確的


Step 3:用戶登入-失敗

可以發現我們會登入失敗,因為SessionUtil.Captchr 因為讀取頁面順序的關係
實際上圖形驗證碼SessionUtil.Captchr已經變成管理用登入的圖片





第三部分:Demo正確展示結果

Step 1:調整方法

範例檔案裡面已經提供加入 CaptchaBindingAttribute 的Attribute,以下是實現順序:
a. 參考:第一部分-Step 5:共用函式邏輯-紀錄Session 擴增一個 CaptChaBindName 可以很好的解決此問題
b. 參考:第一部分-Step 6:客製化屬性,新增一個Class 讓Action 可以掛載
c. 參考:第一部分-Step 3:控制器 讓Action 可以掛載


掛載在控制器上


[HttpPost]
[CaptchaBinding(CaptchaBindingName = nameof(UserLogin))]
public IActionResult UserLoginVerify(LoginViewModel inputData)
{


}



Step 2:正確結果

再次登入後即使開不同頁面,也只會吃自己頁面的圖形驗證碼