ایجاد کپچایی (captcha) سریع و ساده در ASP.NET MVC 5
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: پنج دقیقه

در این مثال به کمک MVC5، یک کپچای ساده و قابل فهم را تولید و استفاده خواهیم کرد. این نوشته بر اساس این مقاله  ایجاد شده و جزئیات زیادی برای درک افراد مبتدی به آن افزوده شده است که امیدوارم راهنمای مفیدی برای علاقمندان باشد.

با کلیک راست بر روی پوشه کنترلر، یک کنترلر به منظور ایجاد کپچا بسازید و اکشن متد زیر را در آن کنترلر ایجاد کنید: 

public class CaptchaController : Controller
    {
        public ActionResult CaptchaImage(string prefix, bool noisy = true)
        {
            var rand = new Random((int)DateTime.Now.Ticks);
            //generate new question
            int a = rand.Next(10, 99);
            int b = rand.Next(0, 9);
            var captcha = string.Format("{0} + {1} = ?", a, b);

            //store answer
            Session["Captcha" + prefix] = a + b;

            //image stream
            FileContentResult img = null;

            using (var mem = new MemoryStream())
            using (var bmp = new Bitmap(130, 30))
            using (var gfx = Graphics.FromImage((Image)bmp))
            {
                gfx.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
                gfx.SmoothingMode = SmoothingMode.AntiAlias;
                gfx.FillRectangle(Brushes.White, new Rectangle(0, 0, bmp.Width, bmp.Height));

                //add noise
                if (noisy)
                {
                    int i, r, x, y;
                    var pen = new Pen(Color.Yellow);
                    for (i = 1; i < 10; i++)
                    {
                        pen.Color = Color.FromArgb(
                        (rand.Next(0, 255)),
                        (rand.Next(0, 255)),
                        (rand.Next(0, 255)));

                        r = rand.Next(0, (130 / 3));
                        x = rand.Next(0, 130);
                        y = rand.Next(0, 30);

                        gfx.DrawEllipse(pen, x - r, y - r, r, r);
                    }
                }

                //add question
                gfx.DrawString(captcha, new Font("Tahoma", 15), Brushes.Gray, 2, 3);

                //render as Jpeg
                bmp.Save(mem, System.Drawing.Imaging.ImageFormat.Jpeg);
                img = this.File(mem.GetBuffer(), "image/Jpeg");
            }

            return img;
        }

همانطور که از کد فوق پیداست، دو مقدار a و b، به شکل اتفاقی ایجاد می‌شوند و حاصل جمع آنها در یک Session نگهداری خواهد شد. سپس تصویری بر اساس تصویر a+b ایجاد می‌شود (مثل 3+4). این تصویر خروجی این اکشن متد است. به سادگی می‌توانید این اکشن را بر اساس خواسته خود اصلاح کنید؛ مثلا به جای حاصل جمع دو عدد، از کاربرد چند حرف یا عدد که بصورت اتفاقی تولید کرده‌اید، استفاده نمائید.

فرض کنید می‌خواهیم کپچا را هنگام ثبت نام استفاده کنیم.

در فایل AccountViewModels.cs در پوشه مدل‌ها در کلاس RegisterViewModel  خاصیت زیر را اضافه کنید:

[Required(ErrorMessage = "لطفا {0} را وارد کنید")]
         [Display(Name = "حاصل جمع")]
         public string Captcha { get; set; }

حالا در پوشه View/Account به فایل Register.Cshtml خاصیت فوق را اضافه کنید:

<div class="form-group">
                        <input type="button" value="" id="refresh" />

                        @Html.LabelFor(model => model.Captcha)

                        <img alt="Captcha" id="imgcpatcha" src="@Url.Action("CaptchaImage","Captcha")" style="" />
                    </div>

وظیفه این بخش، نمایش کپچاست. تگ img دارای آدرسی است که توسط اکشن متدی که در ابتدای این مقاله ایجاد نموده‌ایم تولید می‌شود. این آدرس تصویر کپچاست.  یک دکمه هم با شناسه refresh برای به روز رسانی مجدد تصویر در نظر گرفته‌ایم. 

حالا کد ایجکسی برای آپدیت کپچا توسط دکمه refresh را  به شکل زیر بنویسید (من در پایین ویوی Register، اسکریپت زیر را قرار دادم): 

<script type="text/javascript">
    $(function () {
        $('#refresh').click(function () {


            $.ajax({
                url: '@Url.Action("CaptchaImage","Captcha")',
                type: "GET",
                data: null
            })
            .done(function (functionResult) {
                $("#imgcpatcha").attr("src", "/Captcha/CaptchaImage?" + functionResult);
            });

        });
    });
</script>

آنچه در url نوشته شده است، شاید اصولی‌ترین شکل فراخوانی یک اکشن متد باشد. این اکشن در ابتدای مقاله تحت کنترلری به نام Captcha معرفی شده بود و خروجی آن آدرس یک فایل تصویری است. نوع ارتباط، Get است و هیچ اطلاعاتی به اکشن متد فرستاده نمیشود، اما اکشن متد ما آدرسی را به ما برمی‌گرداند که تحت نام FunctionResult آن را دریافت کرده و به کمک کد جی کوئری، مقدارش را در ویژگی src تصویر موجود در صفحه جاری جایگزین می‌کنیم. دقت کنید که برای دسترسی به تصویر، لازم است جایگزینی آدرس، در ویژگی src به شکل فوق صورت پذیرد.*

تنها کار باقیمانده اضافه کردن کد زیر به ابتدای اکشن متد Register درون کنترلر Account است. 

if (Session["Captcha"] == null || Session["Captcha"].ToString() != model.Captcha)
            {
                ModelState.AddModelError("Captcha", "مجموع اشتباه است");
            }

واضح است که اینکار پیش از شرط if(ModelState.IsValidate) صورت میگیرد و وظیفه شرط فوق، بررسی ِ برابریِ مقدار Session تولید شده در اکشن CaptchaImage  (ابتدای این مقاله) با مقدار ورودی کاربر است. (مقداری که از طریق خاصیت تولیدی خودمان  به آن دسترسی داریم) . بدیهی‌است اگر این دو مقدار نابرابر باشند، یک خطا به ModelState اضافه می‌شود و شرط ModelState.IsValid که در اولین خط بعد از کد فوق وجود دارد، برقرار نخواهد بود و پیغام خطا در صفحه ثبت نام نمایش داده خواهد شد.

تصویر زیر نمونه‌ی نتیجه‌ای است که حاصل خواهد شد  :


* اصلاح : دقت کنید بدون استفاده از ایجکس هم میتوانید تصویر فوق را آپدیت کنید:

  $('#refresh').click(function () {
         
            var d = new Date();
            $("#imgcpatcha").attr("src", "Captcha/CaptchaImage?" + d.getTime());

        });

رویداد کلیک را با کد فوق جایگزین کنید؛ دو نکته در اینجا وجود دارد :

یک. استفاده از زمان در انتهای آدرس به خاطر مشکلاتیست که فایرفاکس یا IE با اینگونه آپدیت‌های تصویری دارند. این دو مرورگر (بر خلاف کروم) تصاویر را نگهداری میکنند و آپدیت به روش فوق به مشکل برخورد میکند مگر آنکه آدرس را به کمک اضافه کردن زمان آپدیت کنید تا مرورگر متوجه داستان شود

دو. همانطور که میبینید آدرس تصویر در حقیقت خروجی یک اکشن است. پس نیازی نیست هر بار این اکشن را به کمک ایجکس صدا بزنیم و روش فوق در مرورگرهای مختلف جواب خواهد داد.

  • #
    ‫۱۰ سال و ۳ ماه قبل، دوشنبه ۲۶ خرداد ۱۳۹۳، ساعت ۱۴:۰۳

    ممنون از شما. فقط یک نکته‌ی کوچک در مورد memory stream هست که بهتره درنظر گرفته بشه. در این شیء متدهای ToArray و GetBuffer یکی نیستند. متد GetBuffer حجمی نزدیک به 2 برابر آرایه اصلی رو عموما داره و انتهاش یک سری بایت‌های اضافی هم شاید باشند. اما ToArray اصل دیتا رو بر می‌گردونه.

    Note that the buffer contains allocated bytes which might be unused. For example, if the string "test" is written into the MemoryStream object, the length of the buffer returned from GetBuffer is 256, not 4, with 252 bytes unused. To obtain only the data in the buffer, use the ToArray method; however, ToArray creates a copy of the data in memory.

  • #
    ‫۹ سال و ۷ ماه قبل، پنجشنبه ۲۳ بهمن ۱۳۹۳، ساعت ۰۳:۳۸
    با سلام و با تشکر؛ با اجازه بنده کد فوق رو کامل‌تر کردم و یک سری کد جدید بهش اضافه کردم و برخی بخش‌ها رو هم تغییر داده ام:
    1- به جای سوال ، بنده یک عبارت رو نمایش میدم
    2- ارسال دیتا از طریق کوئری استرینگ که باعث میشه سشن دیگه نیاز نباشه و از مصرف حافظه رو تا حد زیادی کاسته بشه.
    البته این مورد برای سایت‌های پربازدید خیلی قابل لمس است و ممکنه روی سایت‌های معمولی تفاوت زیادی احساس نشه.
    3- ارسال داده بصورت هش شده ، که این رو بنده خودم با یک کلاس دست ساز معمولی به روش TripleDes انجام داده ام که دوستان به هر روشی می‌تونن داده هاشون رو هش کنن.
    4- یکم حروف رو چرخوندم و فاصله بین حروف رو هم طوری تنظیم کردم که در عرض تصویر پخش بشن (از کل عرض تصویر استفاده بشه)
    * شایان ذکر است که به نظر من روش فوق در ایجاد نویز‌های دایره ای بسیار زیبا بود، چون همیشه همه جا با یک سری خط ساده نویز ایجاد می‌کنن ولی روش فوق واقعا خلاقانه و قشنگ بود :)
    ساختار کنترلر ریکپچای من :
    public class CaptchaController : Controller
        {
            private static readonly Brush ForeColor = Brushes.Black;
            private const string FontName = "tahoma";
            private const int FontSize = 14;
            private const int Width = 130;
            private const int Height = 35;
    
            [HttpGet]
            public ActionResult Image(string cc)
            {
                if (string.IsNullOrEmpty(cc) || string.IsNullOrWhiteSpace(cc))
                    return null;
    
                var captchaData = CustomHashing.DecryptTpl(cc);
    
                var rand = new Random((int)DateTime.Now.Ticks);
    
                // image stream
                FileContentResult img = null;
    
                using (var mem = new MemoryStream())
                using (var bmp = new Bitmap(Width, Height))
                using (var mtrx = new Matrix())
                using (var gfx = Graphics.FromImage((Image)bmp))
                {
                    gfx.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
                    gfx.SmoothingMode = SmoothingMode.AntiAlias;
                    gfx.FillRectangle(Brushes.White, new Rectangle(0, 0, bmp.Width, bmp.Height));
    
                    //add noise
                    int rn, xn, yn;
                    var pen = new Pen(Color.Yellow);
    
                    for (int i = 1; i < 10; i++)
                    {
                        pen.Color = Color.FromArgb((rand.Next(0, 255)), (rand.Next(0, 255)), (rand.Next(0, 255)));
    
                        rn = rand.Next(0, (130 / 3));
                        xn = rand.Next(0, 130);
                        yn = rand.Next(0, 30);
    
                        gfx.DrawEllipse(pen, xn - rn, yn - rn, rn, rn);
                    }
    
                    //add chars
                    #region draw pic
    
                    float x = 1, y = 1;
                    int degree = 10;
    
                    for (int i = 0; i < captchaData.Length; i++)
                    {
                        mtrx.Reset();
    
                        x = (float)(Width * (0.19 * i));
    
                        y = (float)(Height * 0.19);
    
                        degree = rand.Next(-25, 25);
    
                        if (i == 0 && degree > 20)
                        {
                            x += (FontSize + 5);
                            y -= 15;
                        }
    
                        mtrx.RotateAt(degree, new PointF(x, y));
    
                        gfx.Transform = mtrx;
    
                        gfx.DrawString(captchaData[i].ToString(), new Font(FontName, FontSize), ForeColor, x, y);
    
                        gfx.ResetTransform();
                    }
                    #endregion
    
                    //render as Jpeg
                    bmp.Save(mem, System.Drawing.Imaging.ImageFormat.Jpeg);
                    img = this.File(mem.GetBuffer(), "image/Jpeg");
                }
    
                return img;
            }
    برای استفاده هم داریم :
    @{
        var r = new Web.Tools.CustomRandom();
        string hash = Web.Tools.CustomHashing.EncryptTpl(r.CraeteCapchaNumericData(4));
    } 
    
    <!DOCTYPE html>
    
    <html>
    <head>
        <meta name="viewport" content="width=device-width" />
        <title>test Index</title>
    </head>
    <body>
    <div>
    
        <img src="@Url.Action("Image", "Captcha", new { cc = hash })" />
    
    </div>
    </body>
    </html>
    محتوای کلاس CustomRandom :
    این کلاس به تعداد مورد نیاز کاراکتر عددی/عددی-حروفی می‌سازه و به شما تحویل میده
    public class CustomRandom
     {
            /// <summary>
            /// ساخت یک عبارت عددی رندوم
            /// </summary>
            public string CraeteCapchaNumericData(int length)
            {
                var rnd = new Random((int) DateTime.Now.Ticks);
                var temp = new StringBuilder();
    
                for (var i = 0; i < length; i++)
                    temp.Append(Convert.ToChar(rnd.Next(49, 58)));
    
                return temp.ToString();
            }
    
            /// <summary>
            /// ساخت یک عبارت رندوم
            /// </summary>
            public string CreateRandomName(int length)
            {
                var rnd = new Random((int) DateTime.Now.Ticks);
                var temp = new StringBuilder();
                var flag = 1;
    
                for (var i = 0; i < length; i++)
                {
                    flag = rnd.Next(0, 15);
    
                    if (flag < 5)
                        temp.Append(Convert.ToChar(rnd.Next(97, 123))); // lower
                    else if (flag >= 5 && flag < 10)
                        temp.Append(Convert.ToChar(rnd.Next(49, 58))); // numeric
                    else
                        temp.Append(Convert.ToChar(rnd.Next(65, 91))); // biger
                }
    
                return temp.ToString();
            } 
    }
    همانطور که گفتم پیاده سازی متد های DecryptTpl   و EncryptTpl  کلاس CustomHashing   رو به خود دوستان واگذار می‌کنم تا با هر الگوریتمی که دوست دارن این کار رو انجام بدن. (^)
    امیدوارم کد بنده به دوستان کمک کنه.
    موفق باشید
  • #
    ‫۹ سال و ۲ ماه قبل، سه‌شنبه ۱۳ مرداد ۱۳۹۴، ساعت ۱۸:۳۲
    با تشکر از پست خوبتون
    فقط مساله ای که برای من اتفاق افتاد این بود که زمانی که دکمه مربوط به بازسازی کپچا رو می‌زدم تصویر از کش خونده می‌شد و تصویر جدید نمایش داده نمی‌شد که برای حل این موضوع کافیست کد ایجکس برای آپدیت کپچا توسط دکمه refresh را به صورت زیر بنویسید:
    jQuery('#refresh').on({
                    'click': function () {
                        var random = new Date();
                        jQuery.ajax({
                            url: '@Url.Action("CaptchaImage","Captcha")',
                            type: "GET",
                            data: null,
                            success: function (result) {
                                jQuery("#imgcpatcha").attr("src", "/Captcha/CaptchaImage?" + random + result);
                            }
                        });
                    }
                    });
  • #
    ‫۸ سال و ۳ ماه قبل، دوشنبه ۲۴ خرداد ۱۳۹۵، ساعت ۱۴:۰۷
    ممنون از مطلب مفیدتون
    چیزی که بنظرم مغفول مونده، پاک کردن مقدار سشن  بعد از بررسی اون می‌باشد. اگر این کار انجام نشه، هکر می‌تونه یکبار کپچا را درست وارد کند و دفعات بعد مانع ارسال درخواست Captcha/CaptchaImage بشه. به این صورت با همون مقدار کپچای اول، می‌تونه به کارش ادامه بده.
    لذا مقدار سشن کپچا بعد از هر استفاده حتما باید پاک شده یا تغییر یابد.