mirror of https://github.com/abpframework/abp.git
8 changed files with 370 additions and 3 deletions
@ -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) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
using SixLabors.Fonts; |
||||
|
using SixLabors.ImageSharp; |
||||
|
using SixLabors.ImageSharp.Formats; |
||||
|
|
||||
|
namespace Volo.CmsKit.Public.Web.Security.Captcha; |
||||
|
|
||||
|
public class CaptchaOptions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Default fonts are "Arial", "Verdana", "Times New Roman" in Windows. These fonts must exist in the target OS.
|
||||
|
/// </summary>
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
@ -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() |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace Volo.CmsKit.Public.Web.Security.Captcha; |
||||
|
|
||||
|
public enum EncoderTypes |
||||
|
{ |
||||
|
Jpeg, |
||||
|
Png, |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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<Guid, CaptchaRequest> Session { get; set; } = new Dictionary<Guid, CaptchaRequest>(); |
||||
|
|
||||
|
public CaptchaOutput Generate() |
||||
|
{ |
||||
|
return Generate(options: null, number1: null, number2: null); |
||||
|
} |
||||
|
|
||||
|
public CaptchaOutput Generate(CaptchaOptions options) |
||||
|
{ |
||||
|
return Generate(options, number1: null, number2: null); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a simple captcha code.
|
||||
|
/// </summary>
|
||||
|
/// <param name="options">Options for captcha generation</param>
|
||||
|
/// <param name="number1">First number for maths operation</param>
|
||||
|
/// <param name="number2">Second number for maths operation</param>
|
||||
|
/// <returns></returns>
|
||||
|
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<Rgba32>(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<Rgba32>(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); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue