From 69059b2e64d61a84c42b198d6a3adab05611dfeb Mon Sep 17 00:00:00 2001 From: biss Date: Sat, 12 Sep 2026 10:46:36 +0800 Subject: [PATCH] update --- README.md | 2 +- .../CaptchaKit.AspNetCore.csproj | 2 +- .../CaptchaKitService.cs | 68 ++++++++++++------- src/CaptchaKit.Core/CaptchaContracts.cs | 1 + src/CaptchaKit.Core/CaptchaKit.Core.csproj | 2 +- 5 files changed, 48 insertions(+), 27 deletions(-) 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