分享给需要帮助的人:记一次 IdentityAPI 中注册的源码解读:设置用户账户为未验证状态,以及除此之外更安全的做法: 延迟用户创建。包含了对优缺点的说明,以及适用场景。
在ASP.NET 8 Identity 中注册API的源码如下:
routeGroup.MapPost("/register", async Task<Results<Ok, ValidationProblem>>
([FromBody] RegisterRequest registration, HttpContext context, [FromServices] IServiceProvider sp) =>
{
var userManager = sp.GetRequiredService<UserManager<TUser>>();
if (!userManager.SupportsUserEmail)
{
throw new NotSupportedException($"{nameof(MapIdentityApi)} requires a user store with email support.");
}
var userStore = sp.GetRequiredService<IUserStore<TUser>>();
var emailStore = (IUserEmailStore<TUser>)userStore;
var email = registration.Email;
if (string.IsNullOrEmpty(email) || !_emailAddressAttribute.IsValid(email))
{
return CreateValidationProblem(IdentityResult.Failed(userManager.ErrorDescriber.InvalidEmail(email)));
}
var user = new TUser { EmailConfirmed = false }; // 标记为未验证
await userStore.SetUserNameAsync(user, email, CancellationToken.None);
await emailStore.SetEmailAsync(user, email, CancellationToken.None);
var result = await userManager.CreateAsync(user, registration.Password);
if (!result.Succeeded)
{
return CreateValidationProblem(result);
}
await SendConfirmationEmailAsync(user, userManager, context, email);
return TypedResults.Ok();
});
routeGroup.MapGet("/confirm-email", async Task<IResult>
([FromQuery] string userId, [FromQuery] string token, [FromServices] UserManager<TUser> userManager) =>
{
var user = await userManager.FindByIdAsync(userId);
if (user == null)
{
return TypedResults.BadRequest("Invalid user.");
}
var result = await userManager.ConfirmEmailAsync(user, token);
if (!result.Succeeded)
{
return TypedResults.BadRequest("Email confirmation failed.");
}
user.EmailConfirmed = true; // 更新为已验证
await userManager.UpdateAsync(user);
return TypedResults.Ok("Email confirmed successfully.");
});
会发现它在注册的时候使用邮箱作为用户名,配置了邮箱和密码。但是它在发送邮箱验证码之前,就已经通过CreateAsync创建好了账号。这种方式叫做设置用户账户为未验证状态,将 EmailConfirmed 设置为 false,邮箱验证确认后设置为true。
这种方式的缺点很明显:
优点如下:
更安全的方式是延迟用户创建,代码如下:
routeGroup.MapPost("/register", async Task<IResult>
([FromBody] RegisterRequest registration, HttpContext context, [FromServices] IServiceProvider sp) =>
{
var userManager = sp.GetRequiredService<UserManager<TUser>>();
if (!userManager.SupportsUserEmail)
{
throw new NotSupportedException($"{nameof(MapIdentityApi)} requires a user store with email support.");
}
var userStore = sp.GetRequiredService<IUserStore<TUser>>();
var emailStore = (IUserEmailStore<TUser>)userStore;
var email = registration.Email;
if (string.IsNullOrEmpty(email) || !_emailAddressAttribute.IsValid(email))
{
return CreateValidationProblem(IdentityResult.Failed(userManager.ErrorDescriber.InvalidEmail(email)));
}
// 生成验证令牌并发送确认邮件
var verificationToken = GenerateVerificationToken();
await SendVerificationEmailAsync(email, verificationToken, context);
// 临时保存注册信息
SaveTemporaryRegistrationInfo(registration, verificationToken);
return TypedResults.Ok("Please confirm your email.");
});
routeGroup.MapGet("/confirm-email", async Task<IResult>
([FromQuery] string token, [FromServices] IServiceProvider sp) =>
{
var registration = GetTemporaryRegistrationInfoByToken(token);
if (registration == null)
{
return TypedResults.BadRequest("Invalid or expired token.");
}
var userManager = sp.GetRequiredService<UserManager<TUser>>();
var user = new TUser();
await userStore.SetUserNameAsync(user, registration.Email, CancellationToken.None);
await emailStore.SetEmailAsync(user, registration.Email, CancellationToken.None);
var result = await userManager.CreateAsync(user, registration.Password);
if (!result.Succeeded)
{
return CreateValidationProblem(result);
}
return TypedResults.Ok("Email confirmed and user created.");
});
会发现它与第一个例子是相反的,它是用户注册后把数据保存在了临时的内存中,再向邮箱发送验证码。通过配置邮箱的时候,用验证码得到用户数据,并以此创建新的账号。
此做法的缺点也很明显:
优点如下:
它们的适用场景如下: