-
Notifications
You must be signed in to change notification settings - Fork 0
[Netcore] 用户注册启用Email认证时需要注意的问题
Asp.Net core启用Email认证时,如果没有特殊处理此过程,则会有安全问题,其他人可以无需密码就可以以该Email登录系统
此步骤可以参考微软官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/accconfirm
Asp.Net core默认生成的带账号认证的Register代码模板,默认是没有启用Email验证功能,代码如下:
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
// For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=532713
// Send an email with this link
//var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
//var callbackUrl = Url.Action(nameof(ConfirmEmail), "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
//await _emailSender.SendEmailAsync(model.Email, "Confirm your account",
// $"Please confirm your account by clicking this link: <a href='{callbackUrl}'>link</a>");
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation(3, "User created a new account with password.");
return RedirectToLocal(returnUrl);
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
想要启用Email认证,要做如下几件事情:
- 调整Register方法的注册逻辑,将登陆替换为发送验证Email
- 在startup类中声明,Email没有经过验证的账号不能登陆
- 使用自己的邮件服务或者注册在线的邮件服务,发送Email
if (result.Succeeded)
{
// For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=532713
// Send an email with this link
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Action(nameof(ConfirmEmail), "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
await _emailSender.SendEmailAsync(model.Email, "Confirm your account",
// $"Please confirm your account by clicking this link: <a href='{callbackUrl}'>link</a>");
//await _signInManager.SignInAsync(user, isPersistent: false);
//_logger.LogInformation(3, "User created a new account with password.");
//return RedirectToLocal(returnUrl);
ViewData["RegisterMessage"] = "请登录您的邮箱验证账号!";
return View(model);
}
public void ConfigureServices(IServiceCollection services)
{
services.Configure<IdentityOptions>(options =>
{
options.SignIn.RequireConfirmedEmail = true;
});
可以参照微软官方文档:
https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/accconfirm
先看看默认验证Email code的代码:
public async Task<IActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return View("Error");
}
var result = await _userManager.ConfirmEmailAsync(user, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
这里并没有判断 code 是否已经被验证过,只是单纯的验证 code 是否和 _userManager.GenerateEmailConfirmationTokenAsync(user) 生成的 code 对应起来。
这会有什么问题?
如果有人能够拿到这个code,无需密码即可以该账号的身份登录系统。
有如下两种方法:
- 在 ConfirmEmail 方法中,判断 user 的email是否已经处于验证通过状态,如果是则返回错误。
- 在 ConfirmEmail 方法中,user email 首次验证通过之后,生成新的 code,让之前验证的 code作废。
public async Task<IActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return View("Error");
}
// add for blocking old email validation code.
if (user.EmailConfirmed)
{
ViewData["ErrorMessage"] = "验证码已经过期!";
return View();
}
var result = await _userManager.ConfirmEmailAsync(user, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
如果仅仅是调用 _userManager.GenerateEmailConfirmationTokenAsync(user) 方法,会发现无论调用多少次,返回的code均不会变化。
查看ASP.Net core的源码可以看到,此方法生成的code依赖于user的 SecurityStamp 属性:
public virtual async Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var ms = new MemoryStream();
var userId = await manager.GetUserIdAsync(user);
using (var writer = ms.CreateWriter())
{
writer.Write(DateTimeOffset.UtcNow);
writer.Write(userId);
writer.Write(purpose ?? "");
string stamp = null;
if (manager.SupportsUserSecurityStamp)
{
stamp = await manager.GetSecurityStampAsync(user);
}
writer.Write(stamp ?? "");
}
var protectedBytes = Protector.Protect(ms.ToArray());
return Convert.ToBase64String(protectedBytes);
}
SecurityStamp不会变化,除非 a users credentials change (password changed, login removed)
可以呼叫 _userManager.UpdateSecurityStampAsync(user); 方法来改变 user 的 SecurityStamp。
通常情况下,站点会提供“重新发送验证Email”功能,利用上面生成新的 code 的方法,可以保证每次发送的验证Email code不同,并且让旧的code失效,保证安全性。