diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo.CmsKit.Public.Application.csproj b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo.CmsKit.Public.Application.csproj
index b092c3f511..c062ee451a 100644
--- a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo.CmsKit.Public.Application.csproj
+++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo.CmsKit.Public.Application.csproj
@@ -9,8 +9,6 @@
-
-
diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaException.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaException.cs
new file mode 100644
index 0000000000..7914e85b02
--- /dev/null
+++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaException.cs
@@ -0,0 +1,15 @@
+using System.Runtime.Serialization;
+using Volo.Abp;
+
+namespace Volo.CmsKit.Public.Web.Security.Captcha;
+
+public class CaptchaException : UserFriendlyException
+{
+ public CaptchaException(string message) : base(message)
+ {
+ }
+
+ public CaptchaException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context)
+ {
+ }
+}
\ No newline at end of file
diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaOptions.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaOptions.cs
new file mode 100644
index 0000000000..3db5aa06b6
--- /dev/null
+++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaOptions.cs
@@ -0,0 +1,66 @@
+using SixLabors.Fonts;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats;
+
+namespace Volo.CmsKit.Public.Web.Security.Captcha;
+
+public class CaptchaOptions
+{
+ ///
+ /// Default fonts are "Arial", "Verdana", "Times New Roman" in Windows. These fonts must exist in the target OS.
+ ///
+ public string[] FontFamilies { get; set; } = new string[] { "Arial", "Verdana", "Times New Roman" };
+
+ public Color[] TextColor { get; set; } = new Color[]
+ {
+ Color.Blue, Color.Black, Color.Black, Color.Brown, Color.Gray, Color.Green
+ };
+ public Color[] DrawLinesColor { get; set; } = new Color[]
+ {
+ Color.Blue, Color.Black, Color.Black, Color.Brown, Color.Gray, Color.Green
+ };
+
+ public float MinLineThickness { get; set; } = 0.7f;
+
+ public float MaxLineThickness { get; set; } = 2.0f;
+
+ public ushort Width { get; set; } = 180;
+
+ public ushort Height { get; set; } = 70;
+
+ public ushort NoiseRate { get; set; } = 500;
+
+ public Color[] NoiseRateColor { get; set; } = new Color[] { Color.Gray };
+
+ public byte FontSize { get; set; } = 32;
+
+ public FontStyle FontStyle { get; set; } = FontStyle.Regular;
+
+ public EncoderTypes EncoderType { get; set; } = EncoderTypes.Png;
+
+ public IImageEncoder Encoder => RandomTextGenerator.GetEncoder(EncoderType);
+
+ public byte DrawLines { get; set; } = 2;
+
+ public byte MaxRotationDegrees { get; set; } = 4;
+
+ public int Number1MinValue { get; set; } = 1;
+
+ public int Number1MaxValue { get; set; } = 99;
+
+ public int Number2MinValue { get; set; } = 1;
+
+ public int Number2MaxValue { get; set; } = 99;
+
+ public CaptchaOptions()
+ {
+
+ }
+ public CaptchaOptions(int number1MinValue, int number1MaxValue, int number2MinValue, int number2MaxValue)
+ {
+ Number1MinValue = number1MinValue;
+ Number1MaxValue = number1MaxValue;
+ Number2MinValue = number2MinValue;
+ Number1MaxValue = number2MaxValue;
+ }
+}
\ No newline at end of file
diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaOutput.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaOutput.cs
new file mode 100644
index 0000000000..3a483aa46a
--- /dev/null
+++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/CaptchaOutput.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace Volo.CmsKit.Public.Web.Security.Captcha;
+
+public class CaptchaOutput
+{
+ public Guid Id { get; set; }
+ public string Text { get; set; }
+ public byte[] ImageBytes { get; set; }
+ public int Result { get; set; }
+}
+
+public class CaptchaInput
+{
+ public int Number1 { get; set; }
+ public int Number2 { get; set; }
+}
+
+public class CaptchaRequest
+{
+ public CaptchaInput Input { get; set; }
+ public CaptchaOutput Output { get; set; }
+
+ public CaptchaRequest()
+ {
+ Input = new CaptchaInput();
+ Output = new CaptchaOutput
+ {
+ Id = Guid.NewGuid()
+ };
+ }
+}
\ No newline at end of file
diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/EncoderTypes.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/EncoderTypes.cs
new file mode 100644
index 0000000000..60308946f0
--- /dev/null
+++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/EncoderTypes.cs
@@ -0,0 +1,7 @@
+namespace Volo.CmsKit.Public.Web.Security.Captcha;
+
+public enum EncoderTypes
+{
+ Jpeg,
+ Png,
+}
\ No newline at end of file
diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/RandomTextGenerator.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/RandomTextGenerator.cs
new file mode 100644
index 0000000000..df5818f88c
--- /dev/null
+++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/RandomTextGenerator.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Security.Cryptography;
+using System.Text;
+using SixLabors.ImageSharp.Formats;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Formats.Png;
+
+namespace Volo.CmsKit.Public.Web.Security.Captcha;
+public static class RandomTextGenerator
+{
+ private static readonly char[] AllowedChars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVXYZW23456789".ToCharArray();
+
+ public static IImageEncoder GetEncoder(EncoderTypes encoderType)
+ {
+ IImageEncoder encoder = encoderType switch
+ {
+ EncoderTypes.Png => new PngEncoder(),
+ EncoderTypes.Jpeg => new JpegEncoder(),
+ _ => throw new ArgumentException($"Encoder '{encoderType}' not found!")
+ };
+
+ return encoder;
+ }
+
+ public static string GetRandomText(int size)
+ {
+ var data = new byte[4 * size];
+ using (var crypto = new RNGCryptoServiceProvider())
+ {
+ crypto.GetBytes(data);
+ }
+
+ var result = new StringBuilder(size);
+ for (var i = 0; i < size; i++)
+ {
+ var rnd = BitConverter.ToUInt32(data, i * 4);
+ var idx = rnd % AllowedChars.Length;
+ result.Append(AllowedChars[idx]);
+ }
+
+ return result.ToString();
+ }
+
+ public static string GetUniqueKey(int size, char[] chars)
+ {
+ var data = new byte[4 * size];
+ using (var crypto = new RNGCryptoServiceProvider())
+ {
+ crypto.GetBytes(data);
+ }
+
+ var result = new StringBuilder(size);
+
+ for (var i = 0; i < size; i++)
+ {
+ var rnd = BitConverter.ToUInt32(data, i * 4);
+ var idx = rnd % chars.Length;
+ result.Append(chars[idx]);
+ }
+
+ return result.ToString();
+ }
+
+ public static float GenerateNextFloat(double min = -3.40282347E+38, double max = 3.40282347E+38)
+ {
+ var random = new Random();
+ var range = max - min;
+ var sample = random.NextDouble();
+ var scaled = sample * range + min;
+ var result = (float)scaled;
+ return result;
+ }
+}
diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/SimpleMathsCaptchaGenerator.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/SimpleMathsCaptchaGenerator.cs
new file mode 100644
index 0000000000..b9268a7f6a
--- /dev/null
+++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Security/Captcha/SimpleMathsCaptchaGenerator.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Numerics;
+using System.Threading.Tasks;
+using SixLabors.Fonts;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Drawing.Processing;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using Volo.Abp.DependencyInjection;
+using Color = SixLabors.ImageSharp.Color;
+using PointF = SixLabors.ImageSharp.PointF;
+
+namespace Volo.CmsKit.Public.Web.Security.Captcha;
+
+public class SimpleMathsCaptchaGenerator : ISingletonDependency
+{
+ private static Dictionary Session { get; set; } = new Dictionary();
+
+ public CaptchaOutput Generate()
+ {
+ return Generate(options: null, number1: null, number2: null);
+ }
+
+ public CaptchaOutput Generate(CaptchaOptions options)
+ {
+ return Generate(options, number1: null, number2: null);
+ }
+
+ ///
+ /// Creates a simple captcha code.
+ ///
+ /// Options for captcha generation
+ /// First number for maths operation
+ /// Second number for maths operation
+ ///
+ public CaptchaOutput Generate(CaptchaOptions options, int? number1, int? number2)
+ {
+ var random = new Random();
+ options ??= new CaptchaOptions();
+
+ number1 ??= random.Next(options.Number1MinValue, options.Number1MaxValue);
+ number2 ??= random.Next(options.Number2MinValue, options.Number2MaxValue);
+
+ var text = number1 + "+" + number2;
+ var request = new CaptchaRequest
+ {
+ Input =
+ {
+ Number1 = number1.Value,
+ Number2 = number2.Value
+ },
+ Output =
+ {
+ Text = text,
+ Result = Calculate(number1.Value, number2.Value),
+ ImageBytes = GenerateInternal(text, options)
+ }
+ };
+
+ Session[request.Output.Id] = request;
+ return request.Output;
+ }
+
+ private static int Calculate(int number1, int number2)
+ {
+ return number1 + number2;
+ }
+
+ public void Validate(Guid requestId, int value)
+ {
+ var request = Session[requestId];
+ if (request.Output.Result != value)
+ {
+ throw new CaptchaException("The captcha code doesn't match text on the picture! Please try again.");
+ }
+ }
+
+ public void Validate(Guid requestId, string value)
+ {
+ if (int.TryParse(value, out var captchaInput))
+ {
+ Validate(requestId, captchaInput);
+ }
+ else
+ {
+ throw new CaptchaException("The captcha code is missing!");
+ }
+ }
+
+ private byte[] GenerateInternal(string stringText, CaptchaOptions options)
+ {
+ byte[] result;
+
+ using (var image = new Image(options.Width, options.Height))
+ {
+ float position = 0;
+ var random = new Random();
+ var startWith = (byte)random.Next(5, 10);
+ image.Mutate(ctx => ctx.BackgroundColor(Color.Transparent));
+ var fontName = options.FontFamilies[random.Next(0, options.FontFamilies.Length)];
+ var font = SystemFonts.CreateFont(fontName, options.FontSize, options.FontStyle);
+
+ foreach (var character in stringText)
+ {
+ var text = character.ToString();
+ var color = options.TextColor[random.Next(0, options.TextColor.Length)];
+ var location = new PointF(startWith + position, random.Next(6, 13));
+ image.Mutate(ctx => ctx.DrawText(text, font, color, location));
+ position += TextMeasurer.Measure(character.ToString(), new RendererOptions(font, location)).Width;
+ }
+
+ //add rotation
+ var rotation = GetRotation(options);
+ image.Mutate(ctx => ctx.Transform(rotation));
+
+ // add the dynamic image to original image
+ var size = (ushort)TextMeasurer.Measure(stringText, new RendererOptions(font)).Width;
+ var img = new Image(size + 15, options.Height);
+ img.Mutate(ctx => ctx.BackgroundColor(Color.White));
+
+ Parallel.For(0, options.DrawLines, i =>
+ {
+ var x0 = random.Next(0, random.Next(0, 30));
+ var y0 = random.Next(10, img.Height);
+
+ var x1 = random.Next(30, img.Width);
+ var y1 = random.Next(0, img.Height);
+
+ img.Mutate(ctx =>
+ ctx.DrawLines(options.TextColor[random.Next(0, options.TextColor.Length)],
+ RandomTextGenerator.GenerateNextFloat(options.MinLineThickness, options.MaxLineThickness),
+ new PointF[] { new PointF(x0, y0), new PointF(x1, y1) })
+ );
+ });
+
+ img.Mutate(ctx => ctx.DrawImage(image, 0.80f));
+
+ Parallel.For(0, options.NoiseRate, i =>
+ {
+ var x0 = random.Next(0, img.Width);
+ var y0 = random.Next(0, img.Height);
+ img.Mutate(
+ ctx => ctx
+ .DrawLines(options.NoiseRateColor[random.Next(0, options.NoiseRateColor.Length)],
+ RandomTextGenerator.GenerateNextFloat(0.5, 1.5), new PointF[] { new Vector2(x0, y0), new Vector2(x0, y0) })
+ );
+ });
+
+ img.Mutate(x =>
+ {
+ x.Resize(options.Width, options.Height);
+ });
+
+ using (var ms = new MemoryStream())
+ {
+ img.Save(ms, options.Encoder);
+ result = ms.ToArray();
+ }
+ }
+
+ return result;
+ }
+
+ private static AffineTransformBuilder GetRotation(CaptchaOptions options)
+ {
+ var random = new Random();
+ var width = random.Next(10, options.Width);
+ var height = random.Next(10, options.Height);
+ var pointF = new PointF(width, height);
+ var rotationDegrees = random.Next(0, options.MaxRotationDegrees);
+ return new AffineTransformBuilder().PrependRotationDegrees(rotationDegrees, pointF);
+ }
+}
\ No newline at end of file
diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj
index ce31515b0e..7aaa2eda5e 100644
--- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj
+++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj
@@ -15,10 +15,11 @@
-
+
+