Triển Khai Hệ Thống Mã Xác Thực Hình Ảnh CAPTCHA Tùy Chỉnh Trong .NET

Trong quá trình phát triển các module xác thực người dùng, việc ngăn chặn các truy cập tự động từ bot là một yêu cầu bảo mật quan trọng. Sử dụng mã xác thực hình ảnh (CAPTCHA) là giải pháp phổ biến để giải quyết vấn đề này. Bài viết sẽ trình bày cách xây dựng một bộ sinh CAPTCHA tùy chỉnh sử dụng thư viện đồ họa GDI+ trong môi trường .NET.

Quy trình kỹ thuật bao gồm các bước chính: tạo chuỗi ký tự ngẫu nhiên, vẽ nội dung lên bề mặt bitmap, thêm các yếu tố nhiễu ngẫu nhiên, áp dụng hiệu ứng biến dạng sóng và cuối cùng là xuất dữ liệu ảnh dưới dạng luồng byte hoặc Base64.

1. Xây dựng lớp xử lý hình ảnh

Lớp CaptchaImageBuilder sẽ đảm nhiệm toàn bộ logic tạo mã và vẽ hình. Để tăng tính rõ ràng cho người dùng, chúng ta sẽ loại bỏ các ký tự dễ gây nhầm lẫn như số 0 và chữ O, số 1 và chữ I.

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Text;

namespace Core.Security.Captcha
{
    public class CaptchaImageBuilder
    {
        private const string AllowedChars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
        private readonly Random _randomizer;

        public CaptchaImageBuilder()
        {
            _randomizer = new Random();
        }

        /// <summary>
        /// Tạo chuỗi ký tự ngẫu nhiên làm mã xác thực
        /// </summary>
        public string BuildRandomToken(int size)
        {
            var tokenBuilder = new StringBuilder();
            for (int i = 0; i < size; i++)
            {
                int index = _randomizer.Next(AllowedChars.Length);
                tokenBuilder.Append(AllowedChars[index]);
            }
            return tokenBuilder.ToString();
        }

        /// <summary>
        /// Vẽ mã xác thực lên bitmap và áp dụng hiệu ứng nhiễu
        /// </summary>
        public Bitmap RenderCaptchaBitmap(string token)
        {
            int fontSize = 28;
            int padding = 4;
            int charWidth = fontSize + padding;
            
            // Bảng màu ngẫu nhiên cho ký tự
            Color[] palette = new[]
            {
                Color.DarkBlue, Color.DarkGreen, Color.DarkRed, 
                Color.DarkCyan, Color.Purple, Color.OrangeRed
            };
            
            string[] fontNames = new[] { "Verdana", "Tahoma", "Arial" };

            int bmpWidth = (token.Length * charWidth) + 10;
            int bmpHeight = fontSize * 2 + padding;

            Bitmap bitmap = new Bitmap(bmpWidth, bmpHeight);
            using (Graphics g = Graphics.FromImage(bitmap))
            {
                g.Clear(Color.WhiteSmoke);

                // Vẽ nhiễu nền bằng các đường thẳng ngẫu nhiên
                Pen noisePen = new Pen(Color.LightGray, 1);
                int noiseCount = token.Length * 20;

                for (int i = 0; i < noiseCount; i++)
                {
                    int x1 = _randomizer.Next(bitmap.Width);
                    int y1 = _randomizer.Next(bitmap.Height);
                    int x2 = _randomizer.Next(bitmap.Width);
                    int y2 = _randomizer.Next(bitmap.Height);
                    g.DrawLine(noisePen, x1, y1, x2, y2);
                }

                int baselineOffset = (bmpHeight - fontSize) / 2;
                
                // Vẽ từng ký tự với màu sắc và font chữ khác nhau
                for (int i = 0; i < token.Length; i++)
                {
                    int colorIndex = _randomizer.Next(palette.Length);
                    int fontIndex = _randomizer.Next(fontNames.Length);
                    
                    using (Font font = new Font(fontNames[fontIndex], fontSize, FontStyle.Bold))
                    using (SolidBrush brush = new SolidBrush(palette[colorIndex]))
                    {
                        int leftPos = i * charWidth + 2;
                        int topPos = baselineOffset + _randomizer.Next(0, 5);
                        g.DrawString(token[i].ToString(), font, brush, leftPos, topPos);
                    }
                }

                // Vẽ khung viền
                g.DrawRectangle(new Pen(Color.Silver, 1), 0, 0, bitmap.Width - 1, bitmap.Height - 1);
            }

            // Áp dụng hiệu ứng sóng sine để làm biến dạng ảnh
            bitmap = ApplySineWaveDistortion(bitmap, true, 4.0, 1.5);
            return bitmap;
        }

        /// <summary>
        /// Biến dạng ảnh theo hàm sin để chống OCR
        /// </summary>
        private Bitmap ApplySineWaveDistortion(Bitmap sourceImage, bool horizontal, double intensity, double phase)
        {
            const double PI2 = 6.283185307179586476925286766559;
            Bitmap targetImage = new Bitmap(sourceImage.Width, sourceImage.Height);

            using (Graphics g = Graphics.FromImage(targetImage))
            {
                g.Clear(Color.White);
            }

            double axisLength = horizontal ? (double)targetImage.Height : (double)targetImage.Width;

            for (int x = 0; x < targetImage.Width; x++)
            {
                for (int y = 0; y < targetImage.Height; y++)
                {
                    double offset = 0;
                    offset = horizontal ? (PI2 * (double)y) / axisLength : (PI2 * (double)x) / axisLength;
                    offset += phase;
                    
                    double sinValue = Math.Sin(offset);
                    int oldX = x;
                    int oldY = y;

                    if (horizontal)
                    {
                        oldX = x + (int)(sinValue * intensity);
                    }
                    else
                    {
                        oldY = y + (int)(sinValue * intensity);
                    }

                    if (oldX >= 0 && oldX < targetImage.Width && oldY >= 0 && oldY < targetImage.Height)
                    {
                        targetImage.SetPixel(oldX, oldY, sourceImage.GetPixel(x, y));
                    }
                }
            }
            return targetImage;
        }

        /// <summary>
        /// Chuyển đổi bitmap sang mảng byte định dạng JPEG
        /// </summary>
        public byte[] ConvertToJpegBytes(Bitmap image)
        {
            using (MemoryStream stream = new MemoryStream())
            {
                image.Save(stream, ImageFormat.Jpeg);
                return stream.ToArray();
            }
        }
    }

    public class CaptchaTokenInfo
    {
        public string Token { get; set; }
        public int MaxRetries { get; set; } = 3;
    }
}

2. Tích hợp vào Controller

Tại lớp điều khiển, chúng ta khởi tạo bộ sinh mã, tạo chuỗi xác thực và chuyển đổi hình ảnh thành chuỗi Base64 để nhúng trực tiếp vào view mà không cần lưu file tạm.

public ActionResult GetCaptchaImage()
{
    var builder = new CaptchaImageBuilder();
    string code = builder.BuildRandomToken(5);

    // Lưu mã vào Session để xác thực sau này
    Session["CaptchaCode"] = code;

    using (Bitmap img = builder.RenderCaptchaBitmap(code))
    {
        byte[] imageData = builder.ConvertToJpegBytes(img);
        string base64String = "data:image/jpeg;base64," + Convert.ToBase64String(imageData);
        ViewBag.CaptchaImageSource = base64String;
    }

    return View();
}

Thẻ: csharp aspnet-mvc gdi-plus CAPTCHA web-security

Đăng vào ngày 22 tháng 5 lúc 17:09