65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET Identity 2.0:设置账户验证和双因素授权

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (28投票s)

2014年4月21日

CPOL

12分钟阅读

viewsIcon

152163

随着 2014 年 3 月 Identity 2.0 框架的发布,Identity 团队为之前简单但略显基础的 ASP.NET Identity 系统增加了一套重要的全新功能。新版本中引入的最引人注目且需求最多的功能之一是帐户验证。

 
 
 

4097092685_cd75ff7679_z

随着 Identity 2.0 框架在 2014 年 3 月发布,Identity 团队为之前简单但略显基础的 ASP.NET Identity 系统增加了一套重要的全新功能。新版本中引入的最引人注目且需求最多的功能之一是帐户验证和双因素身份验证。

Identity 团队创建了一个出色的示例项目/模板,几乎可以有效地作为任何您希望使用 Identity 2.0 功能构建的 ASP.NET MVC 项目的起点。在这篇文章中,我们将探讨如何在帐户创建过程中实现电子邮件帐户验证以及双因素身份验证。

图片Lucas 拍摄 | 部分权利保留

之前,我们对 Identity 2.0 中一些新功能进行了非常高层次的概述,以及关键区域与之前的 1.0 版本有何不同。在本文中,我们将仔细研究实现电子邮件帐户验证和双因素身份验证。

入门:通过 Nuget 创建示例项目

要开始并跟进,请在 Visual Studio 中创建一个空的 ASP.NET 项目(不是 MVC 项目,在创建新项目时使用“空项目”模板)。然后,打开程序包管理器控制台并输入

从 Nuget 安装示例项目
PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre

注意:少数读者报告了 Nuget 下载和安装过程中出现的奇怪行为。如果您在此过程中遇到任何异常情况,请尝试在评论部分注明发生了什么、您正在使用哪个 VS 版本以及您认为可能值得注意的其他配置项。请确保使用一个完全空的 ASP.NET 项目作为您的起点!

一旦 Nuget 完成其工作,您应该会在解决方案资源管理器中看到熟悉的 ASP.NET MVC 项目结构

解决方案资源管理器中的 ASP.NET Identity 2.0 示例项目
installed-project-in-solution-explorer

如果您之前使用过标准的 ASP.NET MVC 项目,那么此结构应该大部分都很熟悉。但是,仔细观察会发现其中有一些新项目。对于我们本文的目的,我们主要关注位于 App_Start 文件夹中的 IdentityConfig.cs 文件。

如果我们打开 IdentityConfig.cs 文件并滚动浏览,我们会发现定义了两个服务类:EmailServiceSmsService

电子邮件服务和短信服务类
public class EmailService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Plug in your email service here to send an email.
        return Task.FromResult(0);
    }
}
  

public class SmsService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Plug in your sms service here to send a text message.
        return Task.FromResult(0);
    }
}

请注意,EmailServiceSmsService 都实现了通用的接口 IIdentityMessageService。我们可以使用 IIdentityMessageService 来创建任意数量的邮件、短信或其他消息传递实现。我们很快就会看到这是如何工作的。

对我们来说,只需了解电子邮件帐户验证和双因素身份验证功能都依赖于设置 EmailServiceSmsService 并使其在应用程序中可用即可。 

设置默认管理员帐户

在继续深入之前,我们需要设置一个默认的管理员帐户,该帐户将在站点运行时部署。示例项目设置如下:在项目运行期间以及每次 Code-First 模型架构更改时,数据库都会初始化。我们需要一个默认的管理员用户,以便一旦站点运行,您就可以登录并执行管理员类型的操作,而如果没有注册的管理员帐户,您就无法执行此操作。

与以前版本的 Identity 一样,在线注册期间创建的默认用户帐户没有管理员特权。与以前的版本不同,站点运行时创建的第一个用户不是管理员用户。我们需要在初始化期间填充数据库。

注意:CP 用户 Excheque 指出了 Identity 2.0 示例项目代码中可能存在的 bug(这确实是一个 alpha 版本(现在是 beta))。在创建初始管理员用户时,从未设置 EmailConfirmed 属性,这会影响该用户执行某些功能的能力。我已在下面的代码中更新了该属性。 

查看 ApplicationDbInitializer 类,该类也定义在 IdentityConfig.cs 中。

Application Db Initializer 类
public class ApplicationDbInitializer 
    : DropCreateDatabaseIfModelChanges<ApplicationDbContext> 
{
    protected override void Seed(ApplicationDbContext context) {
        InitializeIdentityForEF(context);
        base.Seed(context);
    }
  

    //Create User=Admin@Admin.com with password=Admin@123456 in the Admin role        
    public static void InitializeIdentityForEF(ApplicationDbContext db) 
    {
        var userManager = 
            HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
 
        var roleManager = 
            HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>();

        const string name = "admin@admin.com";
        const string password = "Admin@123456";
        const string roleName = "Admin";

        //Create Role Admin if it does not exist
        var role = roleManager.FindByName(roleName);

        if (role == null) 
        {
            role = new IdentityRole(roleName);
            var roleresult = roleManager.Create(role);
        }

        var user = userManager.FindByName(name);

        if (user == null) 
        {
            user = new ApplicationUser { UserName = name, Email = name };
            var result = userManager.Create(user, password);
            
            // Set Email confirmation property (see note above):
            user.EmailConfirmed = true;
            result = userManager.SetLockoutEnabled(user.Id, false);
        }
  
        // Add user admin to Role Admin if not already added
        var rolesForUser = userManager.GetRoles(user.Id);

        if (!rolesForUser.Contains(role.Name)) 
        {
            var result = userManager.AddToRole(user.Id, role.Name);
        }
    }
}

roleManager 初始化之后声明的常量定义了应用程序首次运行时将创建的默认管理员用户。在继续之前,请根据您的需求更改这些常量,最好包括一个真实有效的电子邮件地址。

帐户验证:工作原理

您无疑熟悉基于电子邮件的帐户验证。您在某个网站上创建一个帐户,然后会向您的电子邮件地址发送一封确认电子邮件,其中包含一个您需要点击的链接,以便确认您的电子邮件地址并验证您的帐户。

如果我们查看示例项目中的 AccountController,我们会找到 Register() 方法。

Account Controller 上的 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;
            return View("DisplayEmail");
        }

        AddErrors(result);
    }
    // If we got this far, something failed, redisplay form
    return View(model);
}

仔细查看上面的代码,我们会看到对 UserManager.SendEmailAsync 的调用,我们在其中传递了一些参数。在 AccountController 中,UserManager 是一个属性,它返回 ApplicationUserManager 类型的实例。如果我们查看 App_Start 文件夹中的 IdentityConfig.cs 文件,我们会找到 ApplicationUserManager 的定义。在该类中,静态 Create() 方法会初始化并返回 ApplicationUserManager 的新实例,这就是我们的消息服务配置所在的地方。

ApplicationUserManager 类上定义的 Create() 方法
public static ApplicationUserManager Create(
    IdentityFactoryOptions<ApplicationUserManager> options, 
    IOwinContext context)
{
    var manager = new ApplicationUserManager(
        new UserStore<ApplicationUser>(
            context.Get<ApplicationDbContext>()));

    // Configure validation logic for usernames
    manager.UserValidator = new UserValidator<ApplicationUser>(manager)
    {
        AllowOnlyAlphanumericUserNames = false,
        RequireUniqueEmail = true
    };

    // Configure validation logic for passwords
    manager.PasswordValidator = new PasswordValidator
    {
        RequiredLength = 6, 
        RequireNonLetterOrDigit = true,
        RequireDigit = true,
        RequireLowercase = true,
        RequireUppercase = true,
    };

    // Configure user lockout defaults
    manager.UserLockoutEnabledByDefault = true;
    manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
    manager.MaxFailedAccessAttemptsBeforeLockout = 5;

    // Register two factor authentication providers. This application 
    // uses Phone and Emails as a step of receiving a code for verifying the user
    // You can write your own provider and plug in here.
    manager.RegisterTwoFactorProvider(
        "PhoneCode", 
        new PhoneNumberTokenProvider<ApplicationUser>
    {
        MessageFormat = "Your security code is: {0}"
    });

    manager.RegisterTwoFactorProvider(
        "EmailCode", 
        new EmailTokenProvider<ApplicationUser>
    {
        Subject = "SecurityCode",
        BodyFormat = "Your security code is {0}"
    });

    manager.EmailService = new EmailService();
    manager.SmsService = new SmsService();
    var dataProtectionProvider = options.DataProtectionProvider;

    if (dataProtectionProvider != null)
    {
        manager.UserTokenProvider = 
            new DataProtectorTokenProvider<ApplicationUser>(
                dataProtectionProvider.Create("ASP.NET Identity"));
    }
    return manager;
}

在该方法的末尾附近,我们注册了双因素提供程序,并在 ApplicationUserManager 实例上设置了电子邮件和短信服务。

从上面可以看出,AccountControllerRegister() 方法调用 ApplicationUserManagerSendEmailAsync 方法,该方法在创建时已配置了电子邮件和短信服务。

新用户帐户的电子邮件验证、通过电子邮件进行双因素身份验证以及通过短信进行双因素身份验证都依赖于 EmailServiceSmsService 类的有效实现。

使用您自己的邮件帐户实现电子邮件服务

为应用程序设置电子邮件服务是一项相对简单的任务。您可以使用自己的电子邮件帐户,或使用 Sendgrid 等邮件服务来创建和发送帐户验证或双因素登录电子邮件。例如,如果我想 使用 Outlook.com 电子邮件帐户发送确认电子邮件,我可能会像这样配置我的电子邮件服务。

配置为使用 Outlook.com 主机的邮件服务
public class EmailService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Credentials:
        var credentialUserName = "yourAccount@outlook.com";
        var sentFrom = "yourAccount@outlook.com";
        var pwd = "yourApssword";
 
        // Configure the client:
        System.Net.Mail.SmtpClient client = 
            new System.Net.Mail.SmtpClient("smtp-mail.outlook.com");

        client.Port = 587;
        client.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network;
        client.UseDefaultCredentials = false;

        // Create the credentials:
        System.Net.NetworkCredential credentials = 
            new System.Net.NetworkCredential(credentialUserName, pwd);

        client.EnableSsl = true;
        client.Credentials = credentials;

        // Create the message:
        var mail = 
            new System.Net.Mail.MailMessage(sentFrom, message.Destination);

        mail.Subject = message.Subject;
        mail.Body = message.Body;

        // Send:
        return client.SendMailAsync(mail);
    }
}

您的 SMTP 主机的具体详细信息可能有所不同,但您应该可以找到文档。不过,总的来说,上面的配置是一个不错的起点。

使用 Sendgrid 实现电子邮件服务

有许多电子邮件服务可用,但 Sendgrid 是 .NET 社区中一个流行的选择。Sendgrid 为多种语言提供 API 支持,以及一个基于 HTTP 的 Web API。此外,Sendgrid 还提供与 Windows Azure 的直接集成。

如果您有一个标准的 Sendgrid 帐户(您可以在 Sendgrid 网站上 设置一个免费开发者帐户),设置电子邮件服务的配置方式与上一个示例略有不同。首先,您的网络凭据将仅使用您的 Sendgrid 用户名和密码。请注意,在这种情况下,您的用户名与您发送邮件的电子邮件地址不同。实际上,您可以将任何地址用作您的发件人地址。

配置为使用 Sendgrid 的 EmailService
public class EmailService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Credentials:
        var sendGridUserName = "yourSendGridUserName";
        var sentFrom = "whateverEmailAdressYouWant";
        var sendGridPassword = "YourSendGridPassword";

        // Configure the client:
        var client = 
            new System.Net.Mail.SmtpClient("smtp.sendgrid.net", Convert.ToInt32(587));

        client.Port = 587;
        client.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network;
        client.UseDefaultCredentials = false;

        // Creatte the credentials:
        System.Net.NetworkCredential credentials = 
            new System.Net.NetworkCredential(sendGridUserName, pwd);

        client.EnableSsl = true;
        client.Credentials = credentials;

        // Create the message:
        var mail = 
            new System.Net.Mail.MailMessage(sentFrom, message.Destination);

        mail.Subject = message.Subject;
        mail.Body = message.Body;

        // Send:
        return client.SendMailAsync(mail);
    }
}

在上面的代码中,我们看到唯一真正需要更改的是我们的凭据和 SMTP 主机字符串。

完成此操作后,我们可以快速试用我们的电子邮件服务。

使用电子邮件服务测试帐户确认

首先,让我们运行应用程序,并尝试注册一个新用户。请使用一个您有权访问的有效电子邮件地址。如果一切顺利,您将被重定向到一个类似以下的视图:

重定向到“确认已发送”视图

redirect-to-confirmation-view

如您所见,有一些文字表明您可以点击视图中的链接来确认帐户创建(用于演示目的)。但是,应该已经向您在创建帐户时指定的地址发送了一封实际的确认电子邮件,并且点击确认链接实际上会确认帐户并允许您登录。

一旦我们的站点正常工作,我们将显然希望删除演示链接,并更改此视图上的文本。我们稍后会介绍。

实现短信服务

要使用短信/文本消息进行双因素身份验证,您需要一个短信主机提供商,例如 Twilio。与 Sendgrid 一样,Twilio 在 .NET 社区中非常受欢迎,并提供了一个全面的 C# API。您可以在 Twilio 网站上 注册一个免费帐户

当您创建一个 Twilio 帐户时,您将获得一个短信电话号码、一个帐户 SID 和一个 Auth Token。您可以通过登录您的帐户并导航到“Numbers”选项卡来查找您的 Twilio 短信电话号码。

查找 Twilio 短信号码

twilio-sms-number

同样,您可以在 Dashboard 选项卡上找到您的 SID 和 Auth Token。

查找 Twilio SID 和 Auth Token

twilio-sid-and-auth-token

在上面的截图中,您看到了 Auth Token 旁边的那个小挂锁图标吗?如果点击它,您的令牌就会显示出来,并且可以复制并粘贴到您的代码中。

一旦您创建了帐户,为了在您的应用程序中使用 Twilio,您需要获取 Twilio Nuget 包。

使用程序包管理器控制台添加 Twilio Nuget 包
Install-Package Twilio

完成此操作后,我们可以将 Twilio 添加到 IdentityConfig.cs 文件顶部的 using 语句中,然后按以下方式实现 SmsService

使用 Twilio 的短信服务
public class SmsService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        string AccountSid = "YourTwilioAccountSID";
        string AuthToken = "YourTwilioAuthToken";
        string twilioPhoneNumber = "YourTwilioPhoneNumber";

        var twilio = new TwilioRestClient(AccountSid, AuthToken);
        twilio.SendSmsMessage(twilioPhoneNumber, message.Destination, message.Body); 

        // Twilio does not return an async Task, so we need this:
        return Task.FromResult(0);
    }
}

现在我们已经配置了电子邮件服务和 SmsService,我们可以看看我们的双因素身份验证是否按预期工作。

测试双因素身份验证

示例项目设置为双因素身份验证是每个用户的“选择加入”功能。要查看一切是否正常工作,请使用您的默认管理员帐户或您之前在测试电子邮件验证功能时创建的帐户登录。登录后,通过点击浏览器视图右上角显示的用户名来导航到用户帐户区域。您应该会看到类似以下的内容:

管理用户帐户视图

enable-two-factor-auth

在上面的窗口中,您需要启用双因素身份验证,并添加您的电话号码。当您添加电话号码时,您将收到一条短信进行确认。如果我们到目前为止一切都做对了,这应该只需要几秒钟(有时可能需要更长的时间,但总的来说,如果过了 30 秒或更长时间,可能就有什么问题了...)。

启用双因素身份验证后,您可以注销并尝试再次登录。您将看到一个下拉菜单,您可以选择通过电子邮件或短信接收您的双因素代码。

选择双因素身份验证方法

two-factor-choose-medium

在上面做出选择后,双因素代码将被发送到您选择的提供商,然后您就可以完成登录过程。

删除演示快捷方式

如前所述,示例项目包含一些内置的快捷方式,以便在开发和测试期间,无需实际从电子邮件或短信帐户发送或检索双因素代码(或从实际电子邮件中遵循帐户验证链接)。

一旦我们准备好部署,我们将需要从相应的视图中删除这些快捷方式,以及传递链接/代码到 ViewBag 的代码。例如,如果我们再次查看 AccountController 上的 Register() 方法,我们可以看到该方法中间有以下代码:

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>");

    // This should not be deployed in production:
    ViewBag.Link = callbackUrl;
    return View("DisplayEmail");
}

AddErrors(result);

在生产环境中,应注释掉或删除突出显示的代码行。

同样,由 Register() 方法返回的视图 DisplayEmail.cshtml 也需要进行一些调整。

DisplayEmail 视图
@{
    ViewBag.Title = "DEMO purpose Email Link";
}
<h2>@ViewBag.Title.</h2>
<p class="text-info">
    Please check your email and confirm your email address.
</p>
<p class="text-danger">
    For DEMO only: You can click this link to confirm the email: <a href="@ViewBag.Link">link</a>
    Please change this code to register an email service in IdentityConfig to send an email.
</p>

类似地,控制器方法 VerifyCode() 也将双因素代码推送到视图中,以便在开发期间轻松使用,但在生产环境中我们绝对不希望出现这种行为。

AccountController 上的 Verify Code 方法
[AllowAnonymous]
public async Task<ActionResult> VerifyCode(string provider, string returnUrl)
{
    // Require that the user has already logged in via username/password or external login
    if (!await SignInHelper.HasBeenVerified())
    {
        return View("Error");
    }

    var user = 
        await UserManager.FindByIdAsync(await SignInHelper.GetVerifiedUserIdAsync());

    if (user != null)
    {
        ViewBag.Status = 
            "For DEMO purposes the current " 
            + provider 
            + " code is: " 
            + await UserManager.GenerateTwoFactorTokenAsync(user.Id, provider);
    }
    return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl });
}

同样,就像 Register() 方法一样,我们需要对 VerifyCode.cshtml 视图进行适当的调整。

VerifyCode.cshtml 视图
@model IdentitySample.Models.VerifyCodeViewModel
@{
    ViewBag.Title = "Enter Verification Code";
}

<h2>@ViewBag.Title.</h2>

@using (Html.BeginForm("VerifyCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary("", new { @class = "text-danger" })
    @Html.Hidden("provider", @Model.Provider)
    <h4>@ViewBag.Status</h4>
    <hr />

    <div class="form-group">
        @Html.LabelFor(m => m.Code, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.Code, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <div class="checkbox">
                @Html.CheckBoxFor(m => m.RememberBrowser)
                @Html.LabelFor(m => m.RememberBrowser)
            </div>
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" class="btn btn-default" value="Submit" />
        </div>
    </div>
}

同样,突出显示的代码行可能会带来麻烦,应予删除。

保持邮件和短信帐户设置安全

本文中的示例将帐户用户名和密码、Auth Token 等硬编码到它们使用的相应方法中。我们不太可能希望这样部署,同样也不太可能希望将代码推送到源代码管理系统而将这些内容暴露出来(尤其是推送到 Github!!)。

将任何私有设置放在安全位置要好得多。有关更多信息,请参阅 ASP.NET MVC:将私有设置移出源代码管理

冰山一角

ASP.NET Identity 2.0 引入了许多以前作为“开箱即用”功能不适用于 ASP.NET 开发人员的令人兴奋的新功能。电子邮件帐户验证和双因素身份验证只是其中最引人注目且易于实现(至少在 Identity 团队提供的示例项目的框架内!)的两个功能。

像这样的新功能极大地增强了普通开发人员可用的安全武器库,但也增加了使用 Identity 2.0 的网站开发的复杂性。Identity 2.0 团队提供了强大的安全工具,但如果我们不小心,可能会更容易引入新的、更难发现的安全漏洞。

其他资源和感兴趣的项目

以下内容关注 Identity 1.0
© . All rights reserved.