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());
}
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.'
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
nice
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!
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...
What is use of unsafe code, i removed following unsafe code. still i loading captcha without unsafe code. unsafe{ }
Very good article. Well done!
nice, thanks you pro share <3
works great. thank you
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?
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?
hola
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?
Thanks a loooooooooot :)
Thanks it works well.
Hi i want to use it my website. I didn't understand about where to give the credit for the owner to use it ?
You save my life. Thank you a lot.
ValidateCaptchaCode Always return false how can I fix this
Tahnk you a lot, it helped me. Your coding style is intuitive also, i learnt new things. Have a nice day.
incomplete passing filestraem in model please explain
can i get the sample download source
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
Thanks a lot for providing such a useful article and its working. Can you please provide the code for audio CAPTCHA as well.
Thanks, Working :)
清真~ 直接ctr/c/v就赚了今天的工资
Thanks a lot, works like charm en with Razor pages. <img src="@Url.Page("/Helpers/CaptchaImage","LoadImageFile",new {name="captcha.png" })" />
Thanks, Working :)
It helps a lot. Thank You
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>
ValidateCaptchaCode Always return false how can I fix this
ok
Thank You, I removed the unsafe part and it still works . Does this run on linux?
How to use Captcha in .net 5 and does it work on Linux-x64 and arm?