From 564ffecfed6feddff6df55d415f1922998be32a7 Mon Sep 17 00:00:00 2001 From: biss Date: Sat, 12 Sep 2026 08:17:14 +0800 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=BB=BA=E7=99=BB=E5=BD=95=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- src/Jiaowu.Api/Controllers/AuthController.cs | 19 ++++---- src/Jiaowu.Api/Controllers/SsoController.cs | 6 +-- src/Jiaowu.Api/Jiaowu.Api.csproj | 5 ++- src/Jiaowu.Api/Program.cs | 9 +++- tests/Jiaowu.Api.Tests/AuthControllerTests.cs | 6 +-- .../CaptchaKitIntegrationTests.cs | 43 +++++++++++++++++++ .../LazyCaptchaIntegrationTests.cs | 26 ----------- tests/Jiaowu.Api.Tests/SsoControllerTests.cs | 6 +-- 9 files changed, 75 insertions(+), 49 deletions(-) create mode 100644 tests/Jiaowu.Api.Tests/CaptchaKitIntegrationTests.cs delete mode 100644 tests/Jiaowu.Api.Tests/LazyCaptchaIntegrationTests.cs diff --git a/.gitignore b/.gitignore index 3211410..2738a85 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ **/node_modules/ **/dist/ .artifacts/ +.codex-build/ +packages/CaptchaKit/artifacts/ .vs/ .vscode/ *.user @@ -17,4 +19,4 @@ src/Jiaowu.Api/wwwroot/ !.env.example !.env.docker.example certs/ -publish/ \ No newline at end of file +publish/ diff --git a/src/Jiaowu.Api/Controllers/AuthController.cs b/src/Jiaowu.Api/Controllers/AuthController.cs index 18bcc05..dddcceb 100644 --- a/src/Jiaowu.Api/Controllers/AuthController.cs +++ b/src/Jiaowu.Api/Controllers/AuthController.cs @@ -4,7 +4,7 @@ using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Caching; -using Lazy.Captcha.Core; +using CaptchaKit.AspNetCore; using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -21,7 +21,7 @@ public sealed class AuthController( UserManager userManager, IAuthSessionService authSessionService, ITwoFactorLoginTicketService twoFactorTickets, - ICaptcha captcha, + ICaptchaKitService captcha, IAppCache cache) : ControllerBase { [AllowAnonymous] @@ -140,13 +140,12 @@ public sealed class AuthController( [AllowAnonymous] [EnableRateLimiting("public-auth")] [HttpPost("login/captcha")] - public ActionResult CreateLoginCaptcha() + public async Task> CreateLoginCaptcha(CancellationToken cancellationToken) { - var captchaId = Guid.NewGuid().ToString("N"); - var generated = captcha.Generate(captchaId, expirySeconds: 120); - return Ok(new LazyLoginCaptchaResponse( - captchaId, - $"data:image/gif;base64,{Convert.ToBase64String(generated.Bytes)}")); + var generated = await captcha.CreateAsync(cancellationToken); + return Ok(new LoginCaptchaResponse( + generated.Id, + $"data:{generated.ContentType};base64,{Convert.ToBase64String(generated.ImageBytes)}")); } [AllowAnonymous] @@ -156,7 +155,7 @@ public sealed class AuthController( LoginRequest request, CancellationToken cancellationToken) { - if (!captcha.Validate(request.CaptchaId, request.CaptchaCode)) + if (!await captcha.VerifyAsync(request.CaptchaId, request.CaptchaCode, cancellationToken)) return Unauthorized(LoginCaptchaProblem()); var user = await userManager.FindByNameAsync(request.UserName); if (user is null || !user.IsEnabled) @@ -334,7 +333,7 @@ public sealed record LoginRequest( [Required, MinLength(3), MaxLength(16)] string CaptchaCode, bool IsNativeApp = false); -public sealed record LazyLoginCaptchaResponse(string CaptchaId, string ImageData); +public sealed record LoginCaptchaResponse(string CaptchaId, string ImageData); public sealed record TotpLoginRequest( [Required, MinLength(20), MaxLength(2048)] string TwoFactorTicket, diff --git a/src/Jiaowu.Api/Controllers/SsoController.cs b/src/Jiaowu.Api/Controllers/SsoController.cs index c0f2bd0..aa6779e 100644 --- a/src/Jiaowu.Api/Controllers/SsoController.cs +++ b/src/Jiaowu.Api/Controllers/SsoController.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Options; -using Lazy.Captcha.Core; +using CaptchaKit.AspNetCore; using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace Jiaowu.Api.Controllers; @@ -23,7 +23,7 @@ public sealed class SsoController( UserManager userManager, IAuthSessionService authSessionService, IDistributedCache cache, - ICaptcha captcha, + ICaptchaKitService captcha, IOptions options, ILogger logger) : ControllerBase { @@ -59,7 +59,7 @@ public sealed class SsoController( if (string.IsNullOrWhiteSpace(bindingIntent) && (string.IsNullOrWhiteSpace(captchaId) || string.IsNullOrWhiteSpace(captchaCode) || - !captcha.Validate(captchaId, captchaCode))) + !await captcha.VerifyAsync(captchaId, captchaCode, cancellationToken))) { return Unauthorized(AuthController.LoginCaptchaProblem()); } diff --git a/src/Jiaowu.Api/Jiaowu.Api.csproj b/src/Jiaowu.Api/Jiaowu.Api.csproj index 6692e71..a77d830 100644 --- a/src/Jiaowu.Api/Jiaowu.Api.csproj +++ b/src/Jiaowu.Api/Jiaowu.Api.csproj @@ -28,7 +28,6 @@ - @@ -48,6 +47,10 @@ + + + + diff --git a/src/Jiaowu.Api/Program.cs b/src/Jiaowu.Api/Program.cs index 1a67ab7..bc3d6e7 100644 --- a/src/Jiaowu.Api/Program.cs +++ b/src/Jiaowu.Api/Program.cs @@ -19,7 +19,7 @@ using Jiaowu.Api.Infrastructure.Operations; using Jiaowu.Api.Infrastructure.Persistence; using Jiaowu.Api.Infrastructure.Scheduling; using Jiaowu.Api.Infrastructure.Timetables; -using Lazy.Captcha.Core; +using CaptchaKit.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.Cookies; @@ -393,7 +393,12 @@ builder.Services.AddHybridCache(options => options.MaximumKeyLength = 512; options.MaximumPayloadBytes = cacheOptions.MaximumPayloadKilobytes * 1024; }); -builder.Services.AddCaptcha(builder.Configuration); +builder.Services.AddCaptchaKit(options => +{ + options.CodeLength = 5; + options.Lifetime = TimeSpan.FromMinutes(2); + options.CacheKeyPrefix = "jiaowu:login-captcha:"; +}); builder.Services.AddSingleton(); builder.Services diff --git a/tests/Jiaowu.Api.Tests/AuthControllerTests.cs b/tests/Jiaowu.Api.Tests/AuthControllerTests.cs index ffa8c36..3767181 100644 --- a/tests/Jiaowu.Api.Tests/AuthControllerTests.cs +++ b/tests/Jiaowu.Api.Tests/AuthControllerTests.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Configuration; -using Lazy.Captcha.Core; +using CaptchaKit.AspNetCore; namespace Jiaowu.Api.Tests; @@ -50,7 +50,7 @@ public sealed class AuthControllerTests var services = new ServiceCollection(); services.AddLogging(); services.AddDistributedMemoryCache(); - services.AddCaptcha(new ConfigurationBuilder().Build()); + services.AddCaptchaKit(); services.AddDbContext(options => options .UseSqlite(connection) .ReplaceService()); @@ -113,7 +113,7 @@ public sealed class AuthControllerTests userManager, new StubAuthSessionService(), new TwoFactorLoginTicketService(new Microsoft.AspNetCore.DataProtection.EphemeralDataProtectionProvider()), - scope.ServiceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService(), NoOpAppCache.Instance); var request = new StudentActivationRequest( student.Name, diff --git a/tests/Jiaowu.Api.Tests/CaptchaKitIntegrationTests.cs b/tests/Jiaowu.Api.Tests/CaptchaKitIntegrationTests.cs new file mode 100644 index 0000000..6a328bd --- /dev/null +++ b/tests/Jiaowu.Api.Tests/CaptchaKitIntegrationTests.cs @@ -0,0 +1,43 @@ +using CaptchaKit.AspNetCore; +using CaptchaKit; +using Microsoft.Extensions.DependencyInjection; + +namespace Jiaowu.Api.Tests; + +public sealed class CaptchaKitIntegrationTests +{ + [Fact] + public async Task Challenge_is_valid_once_and_the_default_generator_returns_a_png() + { + var services = new ServiceCollection(); + services.AddDistributedMemoryCache(); + services.AddCaptchaKit(); + services.AddSingleton(); + using var provider = services.BuildServiceProvider(); + var captcha = provider.GetRequiredService(); + + var challenge = await captcha.CreateAsync(); + + Assert.Equal("image/png", challenge.ContentType); + Assert.True(challenge.ImageBytes.AsSpan().StartsWith(new byte[] { 137, 80, 78, 71 })); + Assert.True(await captcha.VerifyAsync(challenge.Id, "a1b2")); + Assert.False(await captcha.VerifyAsync(challenge.Id, "a1b2")); + } + + [Fact] + public void Default_generator_returns_png_bytes() + { + var challenge = new SkiaTextCaptchaGenerator().Generate( + new CaptchaGenerationRequest("test", DateTimeOffset.UtcNow.AddMinutes(1), 5)); + + Assert.Equal("image/png", challenge.Challenge.ContentType); + Assert.True(challenge.Challenge.ImageBytes.AsSpan().StartsWith(new byte[] { 137, 80, 78, 71 })); + } + + private sealed class KnownGenerator : ICaptchaGenerator + { + public GeneratedCaptcha Generate(CaptchaGenerationRequest request) => new( + new CaptchaChallenge(request.Id, "image/png", new byte[] { 137, 80, 78, 71 }, request.ExpiresAt), + "A1B2"); + } +} diff --git a/tests/Jiaowu.Api.Tests/LazyCaptchaIntegrationTests.cs b/tests/Jiaowu.Api.Tests/LazyCaptchaIntegrationTests.cs deleted file mode 100644 index 2b029ec..0000000 --- a/tests/Jiaowu.Api.Tests/LazyCaptchaIntegrationTests.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Lazy.Captcha.Core; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Jiaowu.Api.Tests; - -public sealed class LazyCaptchaIntegrationTests -{ - [Fact] - public void Generated_code_is_valid_once_and_returns_image_bytes() - { - var services = new ServiceCollection(); - services.AddDistributedMemoryCache(); - services.AddCaptcha(new ConfigurationBuilder().Build()); - using var provider = services.BuildServiceProvider(); - var captcha = provider.GetRequiredService(); - var id = Guid.NewGuid().ToString("N"); - - var generated = captcha.Generate(id, expirySeconds: 120); - - Assert.NotEmpty(generated.Bytes); - Assert.True(captcha.Validate(id, generated.Code)); - Assert.False(captcha.Validate(id, generated.Code)); - } -} diff --git a/tests/Jiaowu.Api.Tests/SsoControllerTests.cs b/tests/Jiaowu.Api.Tests/SsoControllerTests.cs index bc521af..d906214 100644 --- a/tests/Jiaowu.Api.Tests/SsoControllerTests.cs +++ b/tests/Jiaowu.Api.Tests/SsoControllerTests.cs @@ -14,7 +14,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Extensions.Configuration; -using Lazy.Captcha.Core; +using CaptchaKit.AspNetCore; namespace Jiaowu.Api.Tests; @@ -165,7 +165,7 @@ public sealed class SsoControllerTests var services = new ServiceCollection(); services.AddLogging(); services.AddDistributedMemoryCache(); - services.AddCaptcha(new ConfigurationBuilder().Build()); + services.AddCaptchaKit(); services.AddDbContext(options => options.UseSqlite(connection)); services .AddIdentityCore() @@ -190,7 +190,7 @@ public sealed class SsoControllerTests userManager, new StubAuthSessionService(), cache, - provider.GetRequiredService(), + provider.GetRequiredService(), Options.Create(new SsoOptions { Enabled = true,