This commit is contained in:
biss committed 2026-09-12 10:46:36 +08:00
1 parent 9cc95215ff
commit 69059b2e64
5 files changed
+48 -27

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
CaptchaKit is an ASP.NET Core CAPTCHA foundation with server-side, one-time validation and pluggable generators and storage. 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. The package does not claim that image CAPTCHA alone defeats determined automation. Deploy it with rate limits, account lockout, and appropriate risk controls.
@@ -5,7 +5,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild> <GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<PackageId>CaptchaKit.AspNetCore</PackageId> <PackageId>CaptchaKit.AspNetCore</PackageId>
<Version>0.1.0-preview.2</Version> <Version>0.1.0-preview.3</Version>
<Authors>CaptchaKit Contributors</Authors> <Authors>CaptchaKit Contributors</Authors>
<Description>ASP.NET Core and distributed-cache integration for CaptchaKit.</Description> <Description>ASP.NET Core and distributed-cache integration for CaptchaKit.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
+44 -24
View File
@@ -13,7 +13,7 @@ public sealed class CaptchaKitOptions
public TimeSpan Lifetime { get; set; } = TimeSpan.FromMinutes(2); public TimeSpan Lifetime { get; set; } = TimeSpan.FromMinutes(2);
public string CacheKeyPrefix { get; set; } = "captchakit:"; public string CacheKeyPrefix { get; set; } = "captchakit:";
public bool RequireBrowserProof { get; set; } public bool RequireBrowserProof { get; set; }
public bool EnableOneClickChallenge { get; set; } = true; public bool EnableMathChallenge { get; set; } = true;
public bool EnableSelectionChallenge { get; set; } = true; public bool EnableSelectionChallenge { get; set; } = true;
} }
@@ -85,33 +85,41 @@ public sealed class DistributedCaptchaChallengeStore(
public sealed class RandomCaptchaGenerator(CaptchaKitOptions options) : ICaptchaGenerator public sealed class RandomCaptchaGenerator(CaptchaKitOptions options) : ICaptchaGenerator
{ {
private readonly SkiaTextCaptchaGenerator _text = new(); private readonly SkiaTextCaptchaGenerator _text = new();
private const string SelectionAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
public GeneratedCaptcha Generate(CaptchaGenerationRequest request) public GeneratedCaptcha Generate(CaptchaGenerationRequest request)
{ {
var roll = RandomNumberGenerator.GetInt32(100); var roll = RandomNumberGenerator.GetInt32(100);
if (options.EnableOneClickChallenge && roll < 15) if (options.EnableMathChallenge && roll < 25)
{ {
return new GeneratedCaptcha( var left = RandomNumberGenerator.GetInt32(2, 10);
new CaptchaChallenge(request.Id, CaptchaChallengeType.OneClick, request.ExpiresAt, var right = RandomNumberGenerator.GetInt32(1, 10);
"请点击确认按钮继续登录"), var isAddition = RandomNumberGenerator.GetInt32(2) == 0;
"CONFIRM"); 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 target = SelectionAlphabet[RandomNumberGenerator.GetInt32(SelectionAlphabet.Length)].ToString();
var candidates = Enumerable.Range(1, 9) var candidates = SelectionAlphabet
.Where(x => x.ToString() != correct) .Select(x => x.ToString())
.Where(x => x != target)
.OrderBy(_ => RandomNumberGenerator.GetInt32(int.MaxValue)) .OrderBy(_ => RandomNumberGenerator.GetInt32(int.MaxValue))
.Take(3) .Take(3)
.Append(int.Parse(correct)) .Append(target)
.OrderBy(_ => RandomNumberGenerator.GetInt32(int.MaxValue)) .OrderBy(_ => RandomNumberGenerator.GetInt32(int.MaxValue))
.Select(x => new CaptchaChoice($"choice-{x}", x.ToString())) .Select(x => new CaptchaChoice($"choice-{x}", x))
.ToList(); .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( return new GeneratedCaptcha(
new CaptchaChallenge(request.Id, CaptchaChallengeType.Selection, request.ExpiresAt, visual.Challenge with { Choices = candidates },
$"请选择数字 {correct}", candidates), visual.Answer);
answer);
} }
return _text.Generate(request); return _text.Generate(request);
} }
@@ -125,27 +133,39 @@ public sealed class SkiaTextCaptchaGenerator : ICaptchaGenerator
{ {
var answer = string.Concat(Enumerable.Range(0, request.CodeLength) var answer = string.Concat(Enumerable.Range(0, request.CodeLength)
.Select(_ => Alphabet[RandomNumberGenerator.GetInt32(Alphabet.Length)])); .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); using var canvas = new SKCanvas(bitmap);
canvas.Clear(new SKColor(244, 248, 252)); 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 paint = new SKPaint { IsAntialias = true };
using var font = new SKFont(SKTypeface.FromFamilyName("Arial", SKFontStyle.Bold), 30); 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); var y = 38 + RandomNumberGenerator.GetInt32(-4, 5);
paint.Color = RandomColor(35, 145); paint.Color = RandomColor(35, 145);
canvas.Save(); canvas.Save();
canvas.RotateDegrees(RandomNumberGenerator.GetInt32(-20, 21), x, y); 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(); canvas.Restore();
} }
DrawNoise(canvas, 160, 54, 8); DrawNoise(canvas, width, 54, 10);
using var image = SKImage.FromBitmap(bitmap); using var image = SKImage.FromBitmap(bitmap);
using var data = image.Encode(SKEncodedImageFormat.Png, 100); using var data = image.Encode(SKEncodedImageFormat.Png, 100);
var challenge = new CaptchaChallenge(request.Id, CaptchaChallengeType.TextImage, request.ExpiresAt, var challenge = new CaptchaChallenge(request.Id, type, request.ExpiresAt,
"请输入图中的字符", null, "image/png", data.ToArray()); prompt, null, "image/png", data.ToArray());
return new GeneratedCaptcha(challenge, answer); return new GeneratedCaptcha(challenge, answer);
} }
Loaded 3 of 5 files, more files were not shown because too many files have changed in this diff. Show more