验证码优化

This commit is contained in:
biss committed 2026-09-12 07:57:27 +08:00
1 parent f489a3553c
commit 4a3bb84aa4
17 files changed
+156 -430

No files matched your search

@@ -63,7 +63,7 @@ public sealed class ArchivesController(AppDbContext db, ICurrentUserDataScope sc
} }
public sealed record ArchiveBatchRequest( public sealed record ArchiveBatchRequest(
[property: Required, MinLength(1), MaxLength(100)] Guid[] Ids, [param: Required, MinLength(1), MaxLength(100)] Guid[] Ids,
bool Archived, bool Archived,
[property: MaxLength(500)] string? Reason = null); [param: MaxLength(500)] string? Reason = null);
+12 -41
View File
@@ -4,6 +4,7 @@ using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Caching; using Jiaowu.Api.Infrastructure.Caching;
using Lazy.Captcha.Core;
using Jiaowu.Api.Infrastructure.Persistence; using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
@@ -20,7 +21,7 @@ public sealed class AuthController(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
IAuthSessionService authSessionService, IAuthSessionService authSessionService,
ITwoFactorLoginTicketService twoFactorTickets, ITwoFactorLoginTicketService twoFactorTickets,
ILoginCaptchaService loginCaptcha, ICaptcha captcha,
IAppCache cache) : ControllerBase IAppCache cache) : ControllerBase
{ {
[AllowAnonymous] [AllowAnonymous]
@@ -139,30 +140,13 @@ public sealed class AuthController(
[AllowAnonymous] [AllowAnonymous]
[EnableRateLimiting("public-auth")] [EnableRateLimiting("public-auth")]
[HttpPost("login/captcha")] [HttpPost("login/captcha")]
public async Task<ActionResult<LoginCaptchaChallenge>> CreateLoginCaptcha( public ActionResult<LazyLoginCaptchaResponse> CreateLoginCaptcha()
LoginCaptchaCreateRequest request,
CancellationToken cancellationToken) =>
Ok(await loginCaptcha.CreateAsync(request.DeviceId, HttpContext, cancellationToken));
[AllowAnonymous]
[EnableRateLimiting("public-auth")]
[HttpPost("login/captcha/verify")]
public async Task<ActionResult<LoginCaptchaProofResponse>> VerifyLoginCaptcha(
LoginCaptchaVerifyRequest request,
CancellationToken cancellationToken)
{ {
var proof = await loginCaptcha.VerifyAsync( var captchaId = Guid.NewGuid().ToString("N");
new LoginCaptchaVerification( var generated = captcha.Generate(captchaId, expirySeconds: 120);
request.ChallengeId, return Ok(new LazyLoginCaptchaResponse(
request.DeviceId, captchaId,
request.Text, $"data:image/gif;base64,{Convert.ToBase64String(generated.Bytes)}"));
request.SliderX,
request.Clicks),
HttpContext,
cancellationToken);
return proof is null
? Unauthorized(LoginCaptchaProblem())
: Ok(new LoginCaptchaProofResponse(proof));
} }
[AllowAnonymous] [AllowAnonymous]
@@ -172,11 +156,7 @@ public sealed class AuthController(
LoginRequest request, LoginRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (!await loginCaptcha.ConsumeProofAsync( if (!captcha.Validate(request.CaptchaId, request.CaptchaCode))
request.CaptchaTicket,
request.DeviceId,
HttpContext,
cancellationToken))
return Unauthorized(LoginCaptchaProblem()); return Unauthorized(LoginCaptchaProblem());
var user = await userManager.FindByNameAsync(request.UserName); var user = await userManager.FindByNameAsync(request.UserName);
if (user is null || !user.IsEnabled) if (user is null || !user.IsEnabled)
@@ -350,20 +330,11 @@ public sealed class AuthController(
public sealed record LoginRequest( public sealed record LoginRequest(
[Required, MaxLength(100)] string UserName, [Required, MaxLength(100)] string UserName,
[Required, MaxLength(100)] string Password, [Required, MaxLength(100)] string Password,
[Required, MinLength(32), MaxLength(128)] string CaptchaTicket, [Required, MinLength(16), MaxLength(64)] string CaptchaId,
[MaxLength(128)] string? DeviceId = null, [Required, MinLength(3), MaxLength(16)] string CaptchaCode,
bool IsNativeApp = false); bool IsNativeApp = false);
public sealed record LoginCaptchaCreateRequest([MaxLength(128)] string? DeviceId = null); public sealed record LazyLoginCaptchaResponse(string CaptchaId, string ImageData);
public sealed record LoginCaptchaVerifyRequest(
[Required, MinLength(32), MaxLength(64)] string ChallengeId,
[MaxLength(128)] string? DeviceId,
[MaxLength(16)] string? Text,
[Range(0, 240)] int? SliderX,
[MaxLength(2)] IReadOnlyList<CaptchaPoint>? Clicks);
public sealed record LoginCaptchaProofResponse(string CaptchaTicket);
public sealed record TotpLoginRequest( public sealed record TotpLoginRequest(
[Required, MinLength(20), MaxLength(2048)] string TwoFactorTicket, [Required, MinLength(20), MaxLength(2048)] string TwoFactorTicket,
+9 -7
View File
@@ -12,6 +12,7 @@ using Microsoft.AspNetCore.RateLimiting;
using Microsoft.AspNetCore.WebUtilities; using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Lazy.Captcha.Core;
using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Jiaowu.Api.Controllers; namespace Jiaowu.Api.Controllers;
@@ -22,7 +23,7 @@ public sealed class SsoController(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
IAuthSessionService authSessionService, IAuthSessionService authSessionService,
IDistributedCache cache, IDistributedCache cache,
ILoginCaptchaService loginCaptcha, ICaptcha captcha,
IOptions<SsoOptions> options, IOptions<SsoOptions> options,
ILogger<SsoController> logger) : ControllerBase ILogger<SsoController> logger) : ControllerBase
{ {
@@ -47,8 +48,8 @@ public sealed class SsoController(
[FromQuery] string? bindingIntent = null, [FromQuery] string? bindingIntent = null,
[FromQuery] bool nativeApp = false, [FromQuery] bool nativeApp = false,
[FromQuery] string? nativeState = null, [FromQuery] string? nativeState = null,
[FromQuery] string? captchaTicket = null, [FromQuery] string? captchaId = null,
[FromQuery] string? deviceId = null, [FromQuery] string? captchaCode = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
if (!_options.Enabled) if (!_options.Enabled)
@@ -56,8 +57,9 @@ public sealed class SsoController(
var safeReturnUrl = NormalizeReturnUrl(returnUrl); var safeReturnUrl = NormalizeReturnUrl(returnUrl);
if (string.IsNullOrWhiteSpace(bindingIntent) && if (string.IsNullOrWhiteSpace(bindingIntent) &&
(string.IsNullOrWhiteSpace(captchaTicket) || (string.IsNullOrWhiteSpace(captchaId) ||
!await loginCaptcha.ConsumeProofAsync(captchaTicket, deviceId, HttpContext, cancellationToken))) string.IsNullOrWhiteSpace(captchaCode) ||
!captcha.Validate(captchaId, captchaCode)))
{ {
return Unauthorized(AuthController.LoginCaptchaProblem()); return Unauthorized(AuthController.LoginCaptchaProblem());
} }
@@ -417,8 +419,8 @@ public sealed class SsoController(
bindingIntent = intentCode, bindingIntent = intentCode,
nativeApp, nativeApp,
nativeState, nativeState,
captchaTicket = (string?)null, captchaId = (string?)null,
deviceId = (string?)null captchaCode = (string?)null
})!; })!;
return new SsoBindingStartResponse(loginUrl); return new SsoBindingStartResponse(loginUrl);
} }
Loaded 3 of 17 files, more files were not shown because too many files have changed in this diff. Show more