UPDATE 8/29/2023 TLDR: For those who don't want to learn all the details but just want to use captcha right away, the code in this post has been updated to support latest .NET 6.0, 7.0 as well as Linux. Please use my captcha library https://github.com/EdiWang/Edi.Captcha.AspNetCore now you can skip the entire blog post.
If you want to use captcha code to protect your website from spam messages, there are a few options such as Google ReCaptcha and captcha.com. Both of them can be integrated into ASP.NET Core applications. However, you may still want to generate the captcha code yourself for some reason, such as your website may be used in mainland China... This post will show you how to generate and use captcha code in the latest version of ASP.NET Core.
The method I use is from Microsoft official code sample here https://code.msdn.microsoft.com/How-to-make-and-use-d0d1752a but with a few modifications so that it can work with .NET Core 2.x as well as some improvements.
How does captcha work
A simple captcha is generating a random code (numbers or alphabets), save the code to the session, and also make an image to show on the webpage. When a user posts the content back to the server, the server compares the user input captcha with the value stored in session to check if the user has entered a correct captcha code. The workflow is explained in the figure below:
This is a sample in the next version of my blog system:
Implement captcha code in ASP.NET Core 2.1
After understand how captcha code works, let's dig into the implementation.
1. Preparation
First, you need to set your project to allow unsafe code for both Debug and Release configuration.
We need to use classes in System.Drawing.Imaging namespace, so we also need to install a NuGet package:
Install-Package System.Drawing.Common -Version 4.5.1
Because captcha relies on Session storage, so we also have to enable Session support in ASP.NET Core in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// other code...
// add session support
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(20);
options.Cookie.HttpOnly = true;
});
// other code...
}
And also here
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// other code...
// add session support
app.UseSession();
// other code...
}
Note: Session relies on Cookies, so make sure you accept the GDPR cookie policy first, which is added into ASP.NET Core 2.1 default template.
2. Generate Captcha Image
Create a CaptchaResult class to represent the captcha information:
public class CaptchaResult
{
public string CaptchaCode { get; set; }
public byte[] CaptchaByteData { get; set; }
public string CaptchBase64Data => Convert.ToBase64String(CaptchaByteData);
public DateTime Timestamp { get; set; }
}
And a Captcha class for generating and validating the captcha
public static class Captcha
{
const string Letters = "2346789ABCDEFGHJKLMNPRTUVWXYZ";
public static string GenerateCaptchaCode()
{
Random rand = new Random();
int maxRand = Letters.Length - 1;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 4; i++)
{
int index = rand.Next(maxRand);
sb.Append(Letters[index]);
}
return sb.ToString();
}
public static bool ValidateCaptchaCode(string userInputCaptcha, HttpContext context)
{
var isValid = userInputCaptcha == context.Session.GetString("CaptchaCode");
context.Session.Remove("CaptchaCode");
return isValid;
}
public static CaptchaResult GenerateCaptchaImage(int width, int height, string captchaCode)
{
using (Bitmap baseMap = new Bitmap(width, height))
using (Graphics graph = Graphics.FromImage(baseMap))
{
Random rand = new Random();
graph.Clear(GetRandomLightColor());
DrawCaptchaCode();
DrawDisorderLine();
AdjustRippleEffect();
MemoryStream ms = new MemoryStream();
baseMap.Save(ms, ImageFormat.Png);
return new CaptchaResult { CaptchaCode = captchaCode, CaptchaByteData = ms.ToArray(), Timestamp = DateTime.Now };
int GetFontSize(int imageWidth, int captchCodeCount)
{
var averageSize = imageWidth / captchCodeCount;
return Convert.ToInt32(averageSize);
}
Color GetRandomDeepColor()
{
int redlow = 160, greenLow = 100, blueLow = 160;
return Color.FromArgb(rand.Next(redlow), rand.Next(greenLow), rand.Next(blueLow));
}
Color GetRandomLightColor()
{
int low = 180, high = 255;
int nRend = rand.Next(high) % (high - low) + low;
int nGreen = rand.Next(high) % (high - low) + low;
int nBlue = rand.Next(high) % (high - low) + low;
return Color.FromArgb(nRend, nGreen, nBlue);
}
void DrawCaptchaCode()
{
SolidBrush fontBrush = new SolidBrush(Color.Black);
int fontSize = GetFontSize(width, captchaCode.Length);
Font font = new Font(FontFamily.GenericSerif, fontSize, FontStyle.Bold, GraphicsUnit.Pixel);
for (int i = 0; i < captchaCode.Length; i++)
{
fontBrush.Color = GetRandomDeepColor();
int shiftPx = fontSize / 6;
float x = i * fontSize + rand.Next(-shiftPx, shiftPx) + rand.Next(-shiftPx, shiftPx);
int maxY = height - fontSize;
if (maxY < 0) maxY = 0;
float y = rand.Next(0, maxY);
graph.DrawString(captchaCode[i].ToString(), font, fontBrush, x, y);
}
}
void DrawDisorderLine()
{
Pen linePen = new Pen(new SolidBrush(Color.Black), 3);
for (int i = 0; i < rand.Next(3, 5); i++)
{
linePen.Color = GetRandomDeepColor();
Point startPoint = new Point(rand.Next(0, width), rand.Next(0, height));
Point endPoint = new Point(rand.Next(0, width), rand.Next(0, height));
graph.DrawLine(linePen, startPoint, endPoint);
//Point bezierPoint1 = new Point(rand.Next(0, width), rand.Next(0, height));
//Point bezierPoint2 = new Point(rand.Next(0, width), rand.Next(0, height));
//graph.DrawBezier(linePen, startPoint, bezierPoint1, bezierPoint2, endPoint);
}
}
void AdjustRippleEffect()
{
short nWave = 6;
int nWidth = baseMap.Width;
int nHeight = baseMap.Height;
Point[,] pt = new Point[nWidth, nHeight];
for (int x = 0; x < nWidth; ++x)
{
for (int y = 0; y < nHeight; ++y)
{
var xo = nWave * Math.Sin(2.0 * 3.1415 * y / 128.0);
var yo = nWave * Math.Cos(2.0 * 3.1415 * x / 128.0);
var newX = x + xo;
var newY = y + yo;
if (newX > 0 && newX < nWidth)
{
pt[x, y].X = (int)newX;
}
else
{
pt[x, y].X = 0;
}
if (newY > 0 && newY < nHeight)
{
pt[x, y].Y = (int)newY;
}
else
{
pt[x, y].Y = 0;
}
}
}
Bitmap bSrc = (Bitmap)baseMap.Clone();
BitmapData bitmapData = baseMap.LockBits(new Rectangle(0, 0, baseMap.Width, baseMap.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
BitmapData bmSrc = bSrc.LockBits(new Rectangle(0, 0, bSrc.Width, bSrc.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
int scanline = bitmapData.Stride;
IntPtr scan0 = bitmapData.Scan0;
IntPtr srcScan0 = bmSrc.Scan0;
unsafe
{
byte* p = (byte*)(void*)scan0;
byte* pSrc = (byte*)(void*)srcScan0;
int nOffset = bitmapData.Stride - baseMap.Width * 3;
for (int y = 0; y < nHeight; ++y)
{
for (int x = 0; x < nWidth; ++x)
{
var xOffset = pt[x, y].X;
var yOffset = pt[x, y].Y;
if (yOffset >= 0 && yOffset < nHeight && xOffset >= 0 && xOffset < nWidth)
{
if (pSrc != null)
{
p[0] = pSrc[yOffset * scanline + xOffset * 3];
p[1] = pSrc[yOffset * scanline + xOffset * 3 + 1];
p[2] = pSrc[yOffset * scanline + xOffset * 3 + 2];
}
}
p += 3;
}
p += nOffset;
}
}
baseMap.UnlockBits(bitmapData);
bSrc.UnlockBits(bmSrc);
bSrc.Dispose();
}
}
}
}
There are a few things to point out.
1. The letters do not contain all numbers and English alphabets, because some characters are difficult to distinguish, for example:
- Number 0 and English O
- Number 5 and English S
- Number 1 and English I
2. I comment out Bezier code in function DrawDisorderLine() because when the image is very small, it would be very hard to see the characters if there are Bezier lines on it.
Now, in your MVC controller, create an Action for return the captcha image
[Route("get-captcha-image")]
public IActionResult GetCaptchaImage()
{
int width = 100;
int height = 36;
var captchaCode = Captcha.GenerateCaptchaCode();
var result = Captcha.GenerateCaptchaImage(width, height, captchaCode);
HttpContext.Session.SetString("CaptchaCode", result.CaptchaCode);
Stream s = new MemoryStream(result.CaptchaByteData);
return new FileStreamResult(s, "image/png");
}
Now, try to access this Action, you should get a captcha image like this:
3. Use Captcha Image
Add a new property called CaptchaCode to your model that is used for posting content to the server.
[Required]
[StringLength(4)]
public string CaptchaCode { get; set; }
Add a new input field for the CaptchaCode and an image which invokes the "GetCaptchaImage" Action in above section in your view.
<div class="input-group">
<div class="input-group-prepend">
<img id="img-captcha" src="~/get-captcha-image" />
</div>
<input type="text" class="form-control" placeholder="Captcha Code" asp-for="CaptchaCode" maxlength="4" />
<span asp-validation-for="CaptchaCode" class="text-danger"></span>
</div>
In your Action that deal with user post content, add a logic to check captcha code:
if (ModelState.IsValid)
{
// Validate Captcha Code
if (!Captcha.ValidateCaptchaCode(model.CaptchaCode, HttpContext))
{
// return error
}
// continue business logic
}
4. Additional Enhancements
You can let the user click and load a new captcha image via jQuery.
$("#img-captcha").click(function () {
resetCaptchaImage();
});
function resetCaptchaImage() {
d = new Date();
$("#img-captcha").attr("src", "/get-captcha-image?" + d.getTime());
}
Thomas Hetzer
Looks like a promising approach. Thanks for the code, but it is not working on Linux/Docker.
Exception thrown: 'System.TypeInitializationException' in System.Drawing.Common.dll: 'The type initializer for 'Gdip' threw an exception.'
smhKashanchi
Hi edi thank you for your complete education. this very useful for me. if you allow me I translate your article and write on our website with backlink this page.
with best regards
heri
nice
Gerry
Hi! Thank you for your article.
Im currently working with Razor Pages. How can i display the captcha image from the FileStreamResult on my razor page? <img id="img-captcha" src="~/get-captcha-image" /> this approach with [Route("get-captcha-image")] my handlermethode from my PageModel seems not to work.
Thanks and best regards!
Keith Myers
https://codeshorts.com/ASP-NET-CORE-Razor-Pages-Return-a-image-from-a-byte-array?TopicLangId=3
This is how it's done in Razor Pages...
Prakash
What is use of unsafe code, i removed following unsafe code. still i loading captcha without unsafe code. unsafe{ }
Ventura Macute
Very good article. Well done!
VoDanh
nice, thanks you pro share <3
kaveh
works great. thank you
shayan
Do you know a better way to save the key in the client-side instead of a cookie when you use .net core 3.1?
Anderson
Good morning, I don't speak English very well, but come on, I have the following error: ISession does not cotain a definition for 'GetString' and no acessible extension metohd 'GetString' accpeting a first argument of type 'ISession' could be found. Would you help me?
Miguel
hola
zoheir
hello tank you artical
This Captchawa* *ecuted at first, but after a while it does not read the Route, but it i **ecuted in a new project
What do you think, please help?
Ehsan
Thanks a loooooooooot :)
nikhil
Thanks it works well.
amit baghel
Hi i want to use it my website. I didn't understand about where to give the credit for the owner to use it ?
Miles
You save my life. Thank you a lot.
Zain Iqbal
ValidateCaptchaCode Always return false how can I fix this
Oguzhan
Tahnk you a lot, it helped me. Your coding style is intuitive also, i learnt new things. Have a nice day.
Rakesh
incomplete passing filestraem in model please explain
xpote
can i get the sample download source
david
Hola, cuando lo despliego en el servidor de IIS y le doy click a la imagen para recargarla el control retorna not found al refrescar la pagina vuelve y lo muestra bien, en mi maquina de desarrollo funciona correctamente pero en el servidor se presenta ese error estoy desarrollando en aspnet core 3.1
Hima
Thanks a lot for providing such a useful article and its working. Can you please provide the code for audio CAPTCHA as well.
CS
Thanks, Working :)
chenzuo
清真~ 直接ctr/c/v就赚了今天的工资
Rene Goris
Thanks a lot, works like charm en with Razor pages. <img src="@Url.Page("/Helpers/CaptchaImage","LoadImageFile",new {name="captcha.png" })" />
Braz
Thanks, Working :)
Anonymous
It helps a lot. Thank You
Kontext
Hi Edi, just to let you know that I've created a cross-platform SVG captcha code mechanism without using GDI+ for your reference: <a href="https://kontext.tech/article/970/load-fonts-as-glyph-in-net">https://kontext.tech/article/970/load-fonts-as-glyph-in-net</a>
Jeab
ValidateCaptchaCode Always return false how can I fix this
rajesh
ok
Vahid
Thank You, I removed the unsafe part and it still works . Does this run on linux?
Vahid
How to use Captcha in .net 5 and does it work on Linux-x64 and arm?
Suldip
this captcha not showing on IIS
LAYA
Exception thrown: 'System.TypeInitializationException' in System.Drawing.Common.dll: 'The type initializer for 'Gdip' threw an exception.'
Rahim
excellent