使用 ASP.NET 身分識別進行帳戶確認和密碼復原 (C#)
執行本教學課程之前,您應該先完成 使用登入、電子郵件確認和密碼重設建立安全的 ASP.NET MVC 5 Web 應用程式。 本教學課程包含更多詳細數據,並示範如何設定本機帳戶確認的電子郵件,並允許使用者在 ASP.NET 身分識別中重設忘記的密碼。
本地用戶帳戶需要使用者為該帳戶建立密碼,該密碼將安全地儲存在網頁應用程式中。 ASP.NET 身分識別也支援社交帳戶,而不需要使用者為應用程式建立密碼。 社交帳戶 使用第三方(例如Google、Twitter、Facebook或Microsoft)來驗證使用者。 本主題涵蓋下列內容:
- 建立 ASP.NET MVC 應用程式 並探索 ASP.NET 身分識別功能。
- 建置身分識別範例
- 設定電子郵件確認信
新用戶註冊其電子郵件別名,這會建立本機帳戶。
選取 [註冊] 按鈕會將包含驗證令牌的確認電子郵件傳送至其電子郵件位址。
使用者會收到一封電子郵件,其中包含其帳戶的確認令牌。
選取連結會確認帳戶。
密碼復原/重設
忘記密碼的本機使用者可以將安全性令牌傳送至其電子郵件帳戶,讓他們重設其密碼。
使用者很快就會收到一封電子郵件,其中包含可讓他們重設其密碼的連結。
選取連結會帶他們前往 [重設] 頁面。
選取 [重設] 按鈕,將會確認密碼已重設。
建立 ASP.NET Web 應用程式
從安裝並執行 Visual Studio 2017開始。
建立新的 ASP.NET Web 專案,然後選取 MVC 範本。 Web Forms 也支援 ASP.NET 身分識別,因此您可以遵循 Web 窗體應用程式中的類似步驟。
將驗證變更為 個別用戶帳戶。
執行應用程式,選取 註冊 連結並註冊使用者。 此時,電子郵件上唯一的驗證是使用 [EmailAddress] 屬性。
在 [伺服器總管] 中,瀏覽至 [資料連接\DefaultConnection\Tables\AspNetUsers],以滑鼠右鍵按一下並選取 [開啟資料表定義]。
下圖顯示
AspNetUsers
架構:以滑鼠右鍵按一下 AspNetUsers 資料表,然後選取 顯示資料表資料。
此時,尚未確認電子郵件。
ASP.NET 身分識別的預設數據存放區是 Entity Framework,但您可以將它設定為使用其他數據存放區,以及新增其他欄位。 請參閱本教學課程結尾 其他資源 一節。
當應用程式啟動並叫用 App_Start\Startup.Auth.cs中的 ConfigureAuth
方法時,會呼叫 OWIN 啟動類別 (Startup.cs),以設定 OWIN 管線並初始化 ASP.NET 身分識別。 檢查 ConfigureAuth
方法。 每個 CreatePerOwinContext
呼叫都會註冊回調函數(儲存在 OwinContext
中),以便每個要求都會呼叫一次,以建立指定類型的實例。 您可以在每個類型的建構函式和名為 Create
的方法中設置斷點(ApplicationDbContext, ApplicationUserManager
),並確認它們在每次請求中被調用。
ApplicationDbContext
和 ApplicationUserManager
的實例會儲存在 OWIN 內容中,可在整個應用程式中存取。 ASP.NET 身分識別會透過 Cookie 中間件連結到 OWIN 管線。 如需詳細資訊,請參閱 在 ASP.NET Identity中 UserManager 類別的請求存續期管理。
當您變更安全性配置檔時,會產生新的安全性戳記,並儲存在 AspNetUsers 資料表的 SecurityStamp
字段中。 請注意,[SecurityStamp
] 字段與安全性 Cookie 不同。 安全性 Cookie 不會儲存在 AspNetUsers
資料表中(或 Identity DB 中的任何其他位置)。 安全性 Cookie 令牌是使用 DPAPI 自我簽署,並使用 UserId, SecurityStamp
和到期時間資訊建立。
Cookie 中介軟體會在每次網路請求時檢查 Cookie。
Startup
類別中的 SecurityStampValidator
方法會按照 validateInterval
的規定,經常性地連接資料庫,並檢查安全戳記。 除非您變更安全性配置檔,否則這隻會每隔 30 分鐘發生一次(在我們的範例中)。 已選擇 30 分鐘的間隔,以將資料庫行程降到最低。 如需詳細資訊,請參閱 雙因素驗證教學課程。
根據程序代碼中的批注,UseCookieAuthentication
方法支援 Cookie 驗證。 [SecurityStamp
] 欄位和相關聯的程式代碼可為您的應用程式提供額外的安全性層級,當您變更密碼時,將會註銷您登入的瀏覽器。
SecurityStampValidator.OnValidateIdentity
方法可讓應用程式在使用者登入時驗證安全性令牌,當您變更密碼或使用外部登入時會用到此令牌。 這需要確保以舊密碼產生的任何令牌(Cookie)都會失效。 在範例專案中,如果您變更用戶密碼,則會為用戶產生新的令牌,任何先前的令牌都會失效,並更新 [SecurityStamp
] 字段。
身分識別系統允許您配置應用程式,確保當使用者的安全性配置變更時(例如,使用者變更密碼或相關聯的登入方式(例如從 Facebook、Google、Microsoft 帳戶等)),使用者將會從所有瀏覽器實例中登出。 例如,下圖顯示 單一登出範例 應用,讓用戶按下一個按鈕,即可在所有瀏覽器實例中登出(在此案例中為 IE、Firefox 和 Chrome)。 或者,此範例可讓您只登出特定瀏覽器視窗。
單一登出範例 應用程式展示了 ASP.NET 身分識別如何讓您重新生成安全性權杖。 這需要確保以舊密碼產生的任何令牌(Cookie)都會失效。 這項功能可為您的應用程式提供額外的安全層;當您變更密碼時,您將會登出所有已登入此應用程式的裝置。
App_Start\IdentityConfig.cs 檔案包含 ApplicationUserManager
、EmailService
和 SmsService
類別。 每個 EmailService
和 SmsService
類別都會實作 IIdentityMessageService
介面,因此您在每個類別中都有一般方法來設定電子郵件和簡訊。 雖然本教學課程只示範如何透過 sendGrid 新增電子郵件通知,但您可以使用 SMTP 和其他機制傳送電子郵件。
Startup
類別也包含用於新增社交登入的樣板程式碼(Facebook、Twitter 等)。如需詳細資訊,請參閱我的教學課程:MVC 5 應用程式與 Facebook、Twitter、LinkedIn 和 Google OAuth2 登入。
檢查 ApplicationUserManager
類別,其中包含使用者身分識別資訊,並設定下列功能:
- 密碼強度需求。
- 用户被鎖定(登入嘗試次數和時間)。
- 雙因素驗證(2FA)。 我將在另一個教學課程中討論 2FA 和 SMS。
- 連接電子郵件和簡訊服務。 (我將在另一個教學課程中涵蓋簡訊)。
ApplicationUserManager
類別衍生自泛型 UserManager<ApplicationUser>
類別。
ApplicationUser
衍生自 IdentityUser。
IdentityUser
衍生自泛型 IdentityUser
類別:
// Default EntityFramework IUser implementation
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
where TLogin : IdentityUserLogin<TKey>
where TRole : IdentityUserRole<TKey>
where TClaim : IdentityUserClaim<TKey>
{
public IdentityUser()
{
Claims = new List<TClaim>();
Roles = new List<TRole>();
Logins = new List<TLogin>();
}
/// User ID (Primary Key)
public virtual TKey Id { get; set; }
public virtual string Email { get; set; }
public virtual bool EmailConfirmed { get; set; }
public virtual string PasswordHash { get; set; }
/// A random value that should change whenever a users credentials have changed (password changed, login removed)
public virtual string SecurityStamp { get; set; }
public virtual string PhoneNumber { get; set; }
public virtual bool PhoneNumberConfirmed { get; set; }
public virtual bool TwoFactorEnabled { get; set; }
/// DateTime in UTC when lockout ends, any time in the past is considered not locked out.
public virtual DateTime? LockoutEndDateUtc { get; set; }
public virtual bool LockoutEnabled { get; set; }
/// Used to record failures for the purposes of lockout
public virtual int AccessFailedCount { get; set; }
/// Navigation property for user roles
public virtual ICollection<TRole> Roles { get; private set; }
/// Navigation property for user claims
public virtual ICollection<TClaim> Claims { get; private set; }
/// Navigation property for user logins
public virtual ICollection<TLogin> Logins { get; private set; }
public virtual string UserName { get; set; }
}
上述屬性與 AspNetUsers
數據表中的屬性一致,如上所示。
IUser
上的泛型自變數可讓您針對主鍵使用不同的類型來衍生類別。 請參閱 ChangePK 範例,其中示範如何將主鍵從字串變更為 int 或 GUID。
ApplicationUser
ApplicationUser
(public class ApplicationUserManager : UserManager<ApplicationUser>
) 定義於 Models\IdentityModels.cs 中,如下所示:
public class ApplicationUser : IdentityUser
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(
UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in
// CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this,
DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
}
上述已標示的程式代碼會生成 ClaimsIdentity。 ASP.NET 身分識別和 OWIN Cookie 驗證是以宣告為基礎,因此架構需要應用程式為用戶產生 ClaimsIdentity
。
ClaimsIdentity
具有使用者所有宣告的相關信息,例如使用者的名稱、年齡,以及用戶所屬的角色。 您也可以在此階段為使用者新增更多權限。
OWIN 的 AuthenticationManager.SignIn
方法會接受 ClaimsIdentity
並使使用者登入:
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
AuthenticationManager.SignIn(new AuthenticationProperties(){
IsPersistent = isPersistent },
await user.GenerateUserIdentityAsync(UserManager));
}
MVC 5 App 搭配 Facebook、Twitter、LinkedIn 和 Google OAuth2 登入, 示範如何將附加屬性新增至 ApplicationUser
類別。
電子郵件確認
確認新用戶註冊所使用的電子郵件,以確保他們不是在冒充他人(也就是說,他們沒有使用其他人的電子郵件進行註冊)是個好主意。 假設您有討論論壇,您應該防止 "bob@example.com"
註冊為 "joe@contoso.com"
。 如果沒有電子郵件確認,"joe@contoso.com"
可能會從您的應用程式收到不必要的電子郵件。 假設 Bob 不小心註冊為 "bib@example.com"
,但沒注意到,他將無法使用密碼復原,因為應用程式沒有正確的電子郵件。 電子郵件確認僅提供對於 Bot 的有限保護,並且無法抵禦有決心的垃圾郵件發送者,他們擁有許多運作中的電子郵件別名可以用來註冊。在下列範例中,使用者必須在帳戶確認之前(透過選取在註冊的電子郵件中收到的確認連結)才能修改其密碼。您可以將此工作流程應用於其他情境,例如傳送連結以確認和重設由系統管理員建立的新帳戶密碼,或在用戶更改個人資料時傳送電子郵件等等。 您通常想要防止新使用者在透過電子郵件、簡訊簡訊或其他機制確認之前,將任何數據張貼到您的網站。
建置更完整的範例
在本節中,您將使用 NuGet 來下載我們將使用的更完整範例。
建立新的 空白 ASP.NET Web 專案。
在套件管理員控制台中,輸入下列命令:
Install-Package SendGrid Install-Package -Prerelease Microsoft.AspNet.Identity.Samples
在本教學課程中,我們將使用 SendGrid 傳送電子郵件。
Identity.Samples
套件會安裝我們將使用的程序代碼。將 項目設定為使用 SSL。
要測試本機帳戶的建立,請執行應用程式,選取 註冊 連結,並提交註冊表單。
選取模擬電子郵件確認的示範電子郵件連結。
從範例中移除示範電子郵件連結確認碼(在帳戶控制器中的
ViewBag.Link
代碼。請參閱DisplayEmail
和ForgotPasswordConfirmation
動作方法及 Razor 視圖)。
警告
如果您變更此範例中的任何安全性設定,生產應用程式必須進行安全性稽核,以明確呼叫所做的變更。
檢查 App_Start\IdentityConfig.cs 中的程序代碼
此範例示範如何建立帳戶,並將其新增至 Admin 角色。 您應該將範例電子郵件地址替換為您將用於系統管理員帳戶的電子郵件地址。 現在建立系統管理員帳戶最簡單的方式,是在 Seed
方法中以程序設計方式建立。 我們希望未來有一個工具,可讓您建立和管理使用者和角色。 範例程式代碼可讓您建立和管理使用者和角色,但您必須先擁有系統管理員帳戶來執行角色和使用者管理員頁面。 在此範例中,系統會在植入資料庫時建立系統管理員帳戶。
變更密碼,並將名稱變更為您可以接收電子郵件通知的帳戶。
警告
安全性 - 永遠不要將敏感數據儲存在您的原始程式碼中。
如先前所述,在啟動類別中的 app.CreatePerOwinContext
呼叫會將回呼添加到應用程式資料庫內容、使用者管理器和角色管理器類別的 Create
方法。 OWIN 管線會針對每個要求在這些類別上呼叫 Create
方法,並儲存每個類別的內容。 帳戶控制器從 HTTP 上下文(其中包含 OWIN 上下文)公開用戶管理器:
public ApplicationUserManager UserManager
{
get
{
return _userManager ??
HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
當用戶註冊本機帳戶時,會呼叫 HTTP Post Register
方法:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
var callbackUrl = Url.Action(
"ConfirmEmail", "Account",
new { userId = user.Id, code = code },
protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(user.Id,
"Confirm your account",
"Please confirm your account by clicking this link: <a href=\""
+ callbackUrl + "\">link</a>");
// ViewBag.Link = callbackUrl; // Used only for initial demo.
return View("DisplayEmail");
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
上述程式代碼會使用模型數據,使用輸入的電子郵件和密碼來建立新的用戶帳戶。 如果電子郵件別名位於數據存放區中,帳戶建立會失敗,並再次顯示表單。
GenerateEmailConfirmationTokenAsync
方法會建立安全的確認令牌,並將它儲存在 ASP.NET 身分識別數據存放區中。
Url.Action 方法會建立包含 UserId
和確認令牌的連結。 然後,此連結會以電子郵件傳送給使用者,使用者可以選取其電子郵件應用程式中的連結以確認其帳戶。
設定電子郵件確認
移至 SendGrid 註冊頁面並註冊免費帳戶。 新增類似下列的程式代碼來設定 SendGrid:
public class EmailService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
return configSendGridasync(message);
}
private Task configSendGridasync(IdentityMessage message)
{
var myMessage = new SendGridMessage();
myMessage.AddTo(message.Destination);
myMessage.From = new System.Net.Mail.MailAddress(
"Joe@contoso.com", "Joe S.");
myMessage.Subject = message.Subject;
myMessage.Text = message.Body;
myMessage.Html = message.Body;
var credentials = new NetworkCredential(
ConfigurationManager.AppSettings["mailAccount"],
ConfigurationManager.AppSettings["mailPassword"]
);
// Create a Web transport for sending email.
var transportWeb = new Web(credentials);
// Send the email.
if (transportWeb != null)
{
return transportWeb.DeliverAsync(myMessage);
}
else
{
return Task.FromResult(0);
}
}
}
注意
電子郵件客戶程式經常只接受簡訊(沒有 HTML)。 您應該以文字和 HTML 提供訊息。 在上述 SendGrid 範例中,這會使用上面所示的 myMessage.Text
和 myMessage.Html
程式代碼來完成。
下列程式代碼示範如何使用 MailMessage 類別傳送電子郵件,其中 message.Body
只傳回連結。
void sendMail(Message message)
{
#region formatter
string text = string.Format("Please click on this link to {0}: {1}", message.Subject, message.Body);
string html = "Please confirm your account by clicking this link: <a href=\"" + message.Body + "\">link</a><br/>";
html += HttpUtility.HtmlEncode(@"Or click on the copy the following link on the browser:" + message.Body);
#endregion
MailMessage msg = new MailMessage();
msg.From = new MailAddress("joe@contoso.com");
msg.To.Add(new MailAddress(message.Destination));
msg.Subject = message.Subject;
msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(text, null, MediaTypeNames.Text.Plain));
msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(html, null, MediaTypeNames.Text.Html));
SmtpClient smtpClient = new SmtpClient("smtp.gmail.com", Convert.ToInt32(587));
System.Net.NetworkCredential credentials = new System.Net.NetworkCredential("joe@contoso.com", "XXXXXX");
smtpClient.Credentials = credentials;
smtpClient.EnableSsl = true;
smtpClient.Send(msg);
}
警告
安全性 - 永遠不要將敏感數據儲存在您的原始程式碼中。 帳戶和認證會儲存在 appSetting 中。 在 Azure 上,您可以在 Azure 入口網站的 [設定] 索引標籤上安全地儲存這些值。 請參閱 將密碼和其他敏感數據部署至 ASP.NET 和 Azure的最佳做法。
輸入您的 SendGrid 認證,執行應用程式,使用電子郵件別名註冊,然後在您的電子郵件中選取確認連結。 若要了解如何使用您的 Outlook.com 電子郵件帳戶執行這項操作,請參閱 John Atten 的 C# SMTP 設定以獲取 Outlook.Com SMTP 主機,還有他的ASP.NET 身分識別 2.0:設定帳戶驗證和 Two-Factor 授權 的文章。
一旦用戶選取 [註冊] 按鈕,就會將包含驗證令牌的確認電子郵件傳送至其電子郵件位址。
使用者會收到一封電子郵件,其中包含其帳戶的確認令牌。
收到電子郵件的影像
檢查程序代碼
下列程式碼顯示 POST ForgotPassword
方法。
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id)))
{
// Don't reveal that the user does not exist or is not confirmed
return View("ForgotPasswordConfirmation");
}
var code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
var callbackUrl = Url.Action("ResetPassword", "Account",
new { UserId = user.Id, code = code }, protocol: Request.Url.Scheme);
await UserManager.SendEmailAsync(user.Id, "Reset Password",
"Please reset your password by clicking here: <a href=\"" + callbackUrl + "\">link</a>");
return View("ForgotPasswordConfirmation");
}
// If we got this far, something failed, redisplay form
return View(model);
}
如果未確認使用者電子郵件,此方法會以無訊息方式失敗。 如果針對無效的電子郵件位址張貼錯誤,惡意使用者可能利用這些資訊來找出有效的用戶ID(電子郵件別名)進行攻擊。
下列程式代碼顯示帳戶控制器中的 ConfirmEmail
方法,當使用者點擊他們收到的電子郵件中之確認連結時會被呼叫:
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var result = await UserManager.ConfirmEmailAsync(userId, code);
if (result.Succeeded)
{
return View("ConfirmEmail");
}
AddErrors(result);
return View();
}
一旦使用忘記的密碼令牌,它就會失效。
Create
方法中的下列程式代碼變更(在 App_Start\IdentityConfig.cs 檔案中),會將令牌設定為在 3 小時內到期。
if (dataProtectionProvider != null)
{
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>
(dataProtectionProvider.Create("ASP.NET Identity"))
{
TokenLifespan = TimeSpan.FromHours(3)
};
}
使用上述程式代碼,忘記的密碼和電子郵件確認令牌將在 3 小時內到期。 預設 TokenLifespan
為一天。
下列程式代碼顯示電子郵件確認方法:
// GET: /Account/ConfirmEmail
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
IdentityResult result;
try
{
result = await UserManager.ConfirmEmailAsync(userId, code);
}
catch (InvalidOperationException ioe)
{
// ConfirmEmailAsync throws when the userId is not found.
ViewBag.errorMessage = ioe.Message;
return View("Error");
}
if (result.Succeeded)
{
return View();
}
// If we got this far, something failed.
AddErrors(result);
ViewBag.errorMessage = "ConfirmEmail failed";
return View("Error");
}
為了讓您的應用程式更安全,ASP.NET 身分識別支援 Two-Factor 驗證(2FA)。 請參閱 ASP.NET Identity 2.0:設定帳戶驗證和 Two-Factor 授權,作者 John Atten。 雖然您可以設定登入密碼嘗試失敗的帳戶鎖定,但這種方法可讓您的登入容易 DOS 鎖定。 我們建議只有在啟用雙重身份驗證 (2FA) 時,才使用帳戶鎖定。
其他資源
- ASP.NET 身分識別 的自定義記憶體提供者概觀
- MVC 5 應用程式搭配 Facebook、Twitter、LinkedIn 和 Google OAuth2 登入 也會示範如何將設定檔資訊新增至使用者表格。
- ASP.NET MVC 和身分識別 2.0:基礎知識入門,作者:John Atten。
- ASP.NET 身分識別簡介
- Pranav Rastogi 宣佈 ASP.NET Identity 2.0.0 RTM。