diff --git a/README.md b/README.md
index 2eb4ebe..f5ab709 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
CaptchaKit is an ASP.NET Core CAPTCHA foundation with server-side, one-time validation and pluggable generators and storage.
-`CaptchaKit.Core` has provider-neutral contracts. `CaptchaKit.AspNetCore` supplies randomized text-image, selection, and one-click challenges with `IDistributedCache` storage.
+`CaptchaKit.Core` has provider-neutral contracts. `CaptchaKit.AspNetCore` supplies randomized distorted-text, image-math, and image-target-selection challenges with `IDistributedCache` storage.
The package does not claim that image CAPTCHA alone defeats determined automation. Deploy it with rate limits, account lockout, and appropriate risk controls.
diff --git a/src/CaptchaKit.AspNetCore/CaptchaKit.AspNetCore.csproj b/src/CaptchaKit.AspNetCore/CaptchaKit.AspNetCore.csproj
index 04dac06..22b6c13 100644
--- a/src/CaptchaKit.AspNetCore/CaptchaKit.AspNetCore.csproj
+++ b/src/CaptchaKit.AspNetCore/CaptchaKit.AspNetCore.csproj
@@ -5,7 +5,7 @@
enable
false
CaptchaKit.AspNetCore
- 0.1.0-preview.2
+ 0.1.0-preview.3
CaptchaKit Contributors
ASP.NET Core and distributed-cache integration for CaptchaKit.
MIT
diff --git a/src/CaptchaKit.AspNetCore/CaptchaKitService.cs b/src/CaptchaKit.AspNetCore/CaptchaKitService.cs
index 8451ebd..dead738 100644
--- a/src/CaptchaKit.AspNetCore/CaptchaKitService.cs
+++ b/src/CaptchaKit.AspNetCore/CaptchaKitService.cs
@@ -13,7 +13,7 @@ public sealed class CaptchaKitOptions
public TimeSpan Lifetime { get; set; } = TimeSpan.FromMinutes(2);
public string CacheKeyPrefix { get; set; } = "captchakit:";
public bool RequireBrowserProof { get; set; }
- public bool EnableOneClickChallenge { get; set; } = true;
+ public bool EnableMathChallenge { get; set; } = true;
public bool EnableSelectionChallenge { get; set; } = true;
}
@@ -85,33 +85,41 @@ public sealed class DistributedCaptchaChallengeStore(
public sealed class RandomCaptchaGenerator(CaptchaKitOptions options) : ICaptchaGenerator
{
private readonly SkiaTextCaptchaGenerator _text = new();
+ private const string SelectionAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
public GeneratedCaptcha Generate(CaptchaGenerationRequest request)
{
var roll = RandomNumberGenerator.GetInt32(100);
- if (options.EnableOneClickChallenge && roll < 15)
+ if (options.EnableMathChallenge && roll < 25)
{
- return new GeneratedCaptcha(
- new CaptchaChallenge(request.Id, CaptchaChallengeType.OneClick, request.ExpiresAt,
- "请点击确认按钮继续登录"),
- "CONFIRM");
+ var left = RandomNumberGenerator.GetInt32(2, 10);
+ var right = RandomNumberGenerator.GetInt32(1, 10);
+ var isAddition = RandomNumberGenerator.GetInt32(2) == 0;
+ if (!isAddition && right > left)
+ (left, right) = (right, left);
+ var expression = $"{left} {(isAddition ? '+' : '-')} {right} = ?";
+ var answer = (isAddition ? left + right : left - right).ToString();
+ return _text.CreateVisualChallenge(
+ request, expression, answer, CaptchaChallengeType.MathImage, "请计算图片中的算式");
}
- if (options.EnableSelectionChallenge && roll < 45)
+ if (options.EnableSelectionChallenge && roll < 55)
{
- var correct = RandomNumberGenerator.GetInt32(2, 10).ToString();
- var candidates = Enumerable.Range(1, 9)
- .Where(x => x.ToString() != correct)
+ var target = SelectionAlphabet[RandomNumberGenerator.GetInt32(SelectionAlphabet.Length)].ToString();
+ var candidates = SelectionAlphabet
+ .Select(x => x.ToString())
+ .Where(x => x != target)
.OrderBy(_ => RandomNumberGenerator.GetInt32(int.MaxValue))
.Take(3)
- .Append(int.Parse(correct))
+ .Append(target)
.OrderBy(_ => RandomNumberGenerator.GetInt32(int.MaxValue))
- .Select(x => new CaptchaChoice($"choice-{x}", x.ToString()))
+ .Select(x => new CaptchaChoice($"choice-{x}", x))
.ToList();
- var answer = candidates.Single(x => x.Label == correct).Id;
+ var answer = candidates.Single(x => x.Label == target).Id;
+ var visual = _text.CreateVisualChallenge(
+ request, target, answer, CaptchaChallengeType.Selection, "请选择与图片中字符相同的一项");
return new GeneratedCaptcha(
- new CaptchaChallenge(request.Id, CaptchaChallengeType.Selection, request.ExpiresAt,
- $"请选择数字 {correct}", candidates),
- answer);
+ visual.Challenge with { Choices = candidates },
+ visual.Answer);
}
return _text.Generate(request);
}
@@ -125,27 +133,39 @@ public sealed class SkiaTextCaptchaGenerator : ICaptchaGenerator
{
var answer = string.Concat(Enumerable.Range(0, request.CodeLength)
.Select(_ => Alphabet[RandomNumberGenerator.GetInt32(Alphabet.Length)]));
- using var bitmap = new SKBitmap(160, 54);
+ return CreateVisualChallenge(request, answer, answer, CaptchaChallengeType.TextImage, "请输入图中的字符");
+ }
+
+ public GeneratedCaptcha CreateVisualChallenge(
+ CaptchaGenerationRequest request,
+ string displayText,
+ string answer,
+ CaptchaChallengeType type,
+ string prompt)
+ {
+ var width = Math.Max(160, 20 + displayText.Length * 27);
+ using var bitmap = new SKBitmap(width, 54);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(new SKColor(244, 248, 252));
- DrawNoise(canvas, 160, 54, 13);
+ DrawNoise(canvas, width, 54, 16);
using var paint = new SKPaint { IsAntialias = true };
using var font = new SKFont(SKTypeface.FromFamilyName("Arial", SKFontStyle.Bold), 30);
- for (var index = 0; index < answer.Length; index++)
+ var spacing = Math.Max(22, (width - 24) / displayText.Length);
+ for (var index = 0; index < displayText.Length; index++)
{
- var x = 14 + index * 29 + RandomNumberGenerator.GetInt32(-2, 3);
+ var x = 12 + index * spacing + RandomNumberGenerator.GetInt32(-2, 3);
var y = 38 + RandomNumberGenerator.GetInt32(-4, 5);
paint.Color = RandomColor(35, 145);
canvas.Save();
canvas.RotateDegrees(RandomNumberGenerator.GetInt32(-20, 21), x, y);
- canvas.DrawText(answer[index].ToString(), x, y, SKTextAlign.Left, font, paint);
+ canvas.DrawText(displayText[index].ToString(), x, y, SKTextAlign.Left, font, paint);
canvas.Restore();
}
- DrawNoise(canvas, 160, 54, 8);
+ DrawNoise(canvas, width, 54, 10);
using var image = SKImage.FromBitmap(bitmap);
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
- var challenge = new CaptchaChallenge(request.Id, CaptchaChallengeType.TextImage, request.ExpiresAt,
- "请输入图中的字符", null, "image/png", data.ToArray());
+ var challenge = new CaptchaChallenge(request.Id, type, request.ExpiresAt,
+ prompt, null, "image/png", data.ToArray());
return new GeneratedCaptcha(challenge, answer);
}
diff --git a/src/CaptchaKit.Core/CaptchaContracts.cs b/src/CaptchaKit.Core/CaptchaContracts.cs
index f63f342..af02c6f 100644
--- a/src/CaptchaKit.Core/CaptchaContracts.cs
+++ b/src/CaptchaKit.Core/CaptchaContracts.cs
@@ -3,6 +3,7 @@ namespace CaptchaKit;
public enum CaptchaChallengeType
{
TextImage,
+ MathImage,
Selection,
OneClick
}
diff --git a/src/CaptchaKit.Core/CaptchaKit.Core.csproj b/src/CaptchaKit.Core/CaptchaKit.Core.csproj
index 4d71f40..ab90cd1 100644
--- a/src/CaptchaKit.Core/CaptchaKit.Core.csproj
+++ b/src/CaptchaKit.Core/CaptchaKit.Core.csproj
@@ -5,7 +5,7 @@
enable
false
CaptchaKit.Core
- 0.1.0-preview.2
+ 0.1.0-preview.3
CaptchaKit Contributors
Provider-neutral, server-validated CAPTCHA challenge contracts.
MIT