自主注册、登录、退出、当前用户与密码修改
兼容现有 PBKDF2 密码和 hz_session Cookie TOTP 绑定、二步登录、防重放、恢复码与 AES-GCM 密钥 与 Node 完全一致的 Redis 键格式,可跨运行时共享会话 生产环境开启原生认证时强制要求 Redis 默认保持兼容代理;设置 AUTH_NATIVE_ENABLED=true 即可切换
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
using Eis.Application.Authentication;
|
||||
using Eis.Infrastructure.Authentication;
|
||||
|
||||
namespace Eis.Web.Authentication;
|
||||
|
||||
public static class NativeAuthenticationEndpoints
|
||||
{
|
||||
private const string SessionCookieName = "hz_session";
|
||||
|
||||
public static IEndpointRouteBuilder MapNativeAuthenticationEndpoints(
|
||||
this IEndpointRouteBuilder endpoints,
|
||||
AuthenticationOptions options)
|
||||
{
|
||||
if (!options.NativeEnabled)
|
||||
{
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
endpoints.MapPost("/api/auth/register", async (
|
||||
HttpContext context,
|
||||
RegisterRequest request,
|
||||
IAuthenticationService service,
|
||||
CancellationToken cancellationToken) => ToResult(
|
||||
context,
|
||||
await service.RegisterAsync(
|
||||
request.Name ?? string.Empty,
|
||||
request.Gender ?? string.Empty,
|
||||
request.Password ?? string.Empty,
|
||||
request.SchoolId ?? string.Empty,
|
||||
request.ClassId ?? string.Empty,
|
||||
cancellationToken)));
|
||||
|
||||
endpoints.MapGet("/api/auth/me", async (
|
||||
HttpContext context,
|
||||
IAuthenticationService service,
|
||||
CancellationToken cancellationToken) => ToResult(
|
||||
context,
|
||||
await service.GetCurrentUserAsync(SessionToken(context), cancellationToken)));
|
||||
|
||||
endpoints.MapPost("/api/auth/login", async (
|
||||
HttpContext context,
|
||||
LoginRequest request,
|
||||
IAuthenticationService service,
|
||||
CancellationToken cancellationToken) => ToResult(
|
||||
context,
|
||||
await service.LoginAsync(request.Username ?? string.Empty, request.Password ?? string.Empty, cancellationToken)));
|
||||
|
||||
endpoints.MapPost("/api/auth/login/totp", async (
|
||||
HttpContext context,
|
||||
TotpLoginRequest request,
|
||||
IAuthenticationService service,
|
||||
CancellationToken cancellationToken) => ToResult(
|
||||
context,
|
||||
await service.CompleteTotpLoginAsync(
|
||||
request.Challenge ?? string.Empty,
|
||||
request.Code ?? string.Empty,
|
||||
cancellationToken)));
|
||||
|
||||
endpoints.MapPost("/api/auth/change-password", async (
|
||||
HttpContext context,
|
||||
ChangePasswordRequest request,
|
||||
IAuthenticationService service,
|
||||
CancellationToken cancellationToken) => ToResult(
|
||||
context,
|
||||
await service.ChangePasswordAsync(
|
||||
SessionToken(context),
|
||||
request.CurrentPassword ?? string.Empty,
|
||||
request.NewPassword ?? string.Empty,
|
||||
cancellationToken)));
|
||||
|
||||
endpoints.MapGet("/api/auth/totp", async (
|
||||
HttpContext context,
|
||||
IAuthenticationService service,
|
||||
CancellationToken cancellationToken) => ToResult(
|
||||
context,
|
||||
await service.GetTotpStatusAsync(SessionToken(context), cancellationToken)));
|
||||
|
||||
endpoints.MapPost("/api/auth/totp/setup", async (
|
||||
HttpContext context,
|
||||
PasswordRequest request,
|
||||
IAuthenticationService service,
|
||||
CancellationToken cancellationToken) => ToResult(
|
||||
context,
|
||||
await service.BeginTotpSetupAsync(
|
||||
SessionToken(context),
|
||||
request.CurrentPassword ?? string.Empty,
|
||||
cancellationToken)));
|
||||
|
||||
endpoints.MapPost("/api/auth/totp/enable", async (
|
||||
HttpContext context,
|
||||
CodeRequest request,
|
||||
IAuthenticationService service,
|
||||
CancellationToken cancellationToken) => ToResult(
|
||||
context,
|
||||
await service.EnableTotpAsync(
|
||||
SessionToken(context),
|
||||
request.Code ?? string.Empty,
|
||||
cancellationToken)));
|
||||
|
||||
endpoints.MapPost("/api/auth/totp/recovery-codes", async (
|
||||
HttpContext context,
|
||||
PasswordAndCodeRequest request,
|
||||
IAuthenticationService service,
|
||||
CancellationToken cancellationToken) => ToResult(
|
||||
context,
|
||||
await service.RegenerateRecoveryCodesAsync(
|
||||
SessionToken(context),
|
||||
request.CurrentPassword ?? string.Empty,
|
||||
request.Code ?? string.Empty,
|
||||
cancellationToken)));
|
||||
|
||||
endpoints.MapPost("/api/auth/totp/disable", async (
|
||||
HttpContext context,
|
||||
PasswordAndCodeRequest request,
|
||||
IAuthenticationService service,
|
||||
CancellationToken cancellationToken) => ToResult(
|
||||
context,
|
||||
await service.DisableTotpAsync(
|
||||
SessionToken(context),
|
||||
request.CurrentPassword ?? string.Empty,
|
||||
request.Code ?? string.Empty,
|
||||
cancellationToken)));
|
||||
|
||||
endpoints.MapPost("/api/auth/logout", async (
|
||||
HttpContext context,
|
||||
IAuthenticationService service,
|
||||
CancellationToken cancellationToken) => ToResult(
|
||||
context,
|
||||
await service.LogoutAsync(SessionToken(context), cancellationToken)));
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static string SessionToken(HttpContext context) =>
|
||||
context.Request.Cookies.TryGetValue(SessionCookieName, out var token) ? token : string.Empty;
|
||||
|
||||
private static IResult ToResult(HttpContext context, AuthenticationEndpointResult result)
|
||||
{
|
||||
context.Response.Headers.CacheControl = "no-store";
|
||||
context.Response.Headers["X-EIS-Implementation"] = "aspnet-core";
|
||||
if (result.SetCookie is not null)
|
||||
{
|
||||
context.Response.Headers.Append("Set-Cookie", result.SetCookie);
|
||||
}
|
||||
|
||||
return Results.Json(result.Body, statusCode: result.StatusCode);
|
||||
}
|
||||
|
||||
private sealed class LoginRequest
|
||||
{
|
||||
public string? Username { get; init; }
|
||||
|
||||
public string? Password { get; init; }
|
||||
}
|
||||
|
||||
private sealed class RegisterRequest
|
||||
{
|
||||
public string? Name { get; init; }
|
||||
|
||||
public string? Gender { get; init; }
|
||||
|
||||
public string? Password { get; init; }
|
||||
|
||||
public string? SchoolId { get; init; }
|
||||
|
||||
public string? ClassId { get; init; }
|
||||
}
|
||||
|
||||
private sealed class TotpLoginRequest
|
||||
{
|
||||
public string? Challenge { get; init; }
|
||||
|
||||
public string? Code { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ChangePasswordRequest
|
||||
{
|
||||
public string? CurrentPassword { get; init; }
|
||||
|
||||
public string? NewPassword { get; init; }
|
||||
}
|
||||
|
||||
private sealed class PasswordRequest
|
||||
{
|
||||
public string? CurrentPassword { get; init; }
|
||||
}
|
||||
|
||||
private sealed class CodeRequest
|
||||
{
|
||||
public string? Code { get; init; }
|
||||
}
|
||||
|
||||
private sealed class PasswordAndCodeRequest
|
||||
{
|
||||
public string? CurrentPassword { get; init; }
|
||||
|
||||
public string? Code { get; init; }
|
||||
}
|
||||
}
|
||||
+16
-2
@@ -1,10 +1,12 @@
|
||||
using System.Net;
|
||||
using Eis.Infrastructure.Authentication;
|
||||
using Eis.Application.Public;
|
||||
using Eis.Infrastructure;
|
||||
using Eis.Infrastructure.Data;
|
||||
using Eis.Infrastructure.Migration;
|
||||
using Eis.Infrastructure.Security;
|
||||
using Eis.Web.Configuration;
|
||||
using Eis.Web.Authentication;
|
||||
using Eis.Web.Frontend;
|
||||
using Eis.Web.Legacy;
|
||||
using Eis.Web.Public;
|
||||
@@ -29,11 +31,16 @@ builder.Services.AddHttpClient<LegacyApiProxy>((services, client) =>
|
||||
});
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddSingleton<IPublicSiteConfiguration, PublicSiteConfiguration>();
|
||||
var authenticationOptions = AuthenticationOptions.FromEnvironment(
|
||||
builder.Environment.IsProduction(),
|
||||
builder.Configuration.GetValue<bool>("AuthenticationMigration:NativeEnabled"));
|
||||
builder.Services.AddEisInfrastructure(
|
||||
DatabaseOptions.FromEnvironment(applicationRoot, builder.Environment.IsProduction()),
|
||||
DocumentVerificationOptions.FromEnvironment(builder.Environment.IsProduction()));
|
||||
DocumentVerificationOptions.FromEnvironment(builder.Environment.IsProduction()),
|
||||
authenticationOptions);
|
||||
|
||||
var app = builder.Build();
|
||||
app.Services.EnsureNativeAuthenticationReady(authenticationOptions);
|
||||
|
||||
app.UseExceptionHandler();
|
||||
app.Use(async (context, next) =>
|
||||
@@ -59,11 +66,18 @@ app.MapGet("/health/migration", async (LegacyApiProxy proxy, CancellationToken c
|
||||
{
|
||||
status = legacyAvailable ? "healthy" : "degraded",
|
||||
legacyApiAvailable = legacyAvailable,
|
||||
features = MigrationFeatureCatalog.Current
|
||||
authentication = new
|
||||
{
|
||||
nativeEnabled = authenticationOptions.NativeEnabled,
|
||||
stateBackend = authenticationOptions.UsesRedis ? "redis" : "memory",
|
||||
sharesLegacySessions = authenticationOptions.SharesLegacySessions
|
||||
},
|
||||
features = MigrationFeatureCatalog.Current(authenticationOptions.NativeEnabled)
|
||||
}, statusCode: statusCode);
|
||||
});
|
||||
|
||||
app.MapNativePublicEndpoints();
|
||||
app.MapNativeAuthenticationEndpoints(authenticationOptions);
|
||||
|
||||
string[] methods =
|
||||
[
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"Enabled": true,
|
||||
"BaseUrl": "http://127.0.0.1:4174"
|
||||
},
|
||||
"AuthenticationMigration": {
|
||||
"NativeEnabled": false
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
||||
Reference in New Issue
Block a user