ASP.NET Identity 2集成到MVC5项目--笔记02

ASP.NET Identity 2集成到MVC5项目--笔记01
ASP.NET Identity 2集成到MVC5项目--笔记02


继上一篇,本篇主要是实现邮件、用户名登陆和登陆前邮件认证。


1. 登陆之前

到现在为止现在,涉及到身份认证的解决方案大致完成了。需要我们在Identity2Study项目下面按照运行前面的Nuget命令。下面才是真正的用到项目中去。我们演示一个简单的登录。
在我们建立登录控制器之前,我们需要为项目添加一个Startup类和简单配置一下web.config文件。
在项目的App_Start文件夹下新建一个分部类文件命名为:Startup.Auth.cs

namespace Identity2Study
{
    public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder app)
        {
            app.CreatePerOwinContext(ApplicationDbContext.Create);
            app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
            app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
            app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);


            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Account/Login"),
                Provider = new CookieAuthenticationProvider
                {
                    OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                        validateInterval: TimeSpan.FromMinutes(30),
                        regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
                }
            });
            app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

            app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));


            app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);


        }
    }
}

需要注意的是这个类的命名空间是Identity2Study
然后在项目根文件夹建立一个分部类名为Startup.cs

namespace Identity2Study
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }
    }
}

打开Web.config在configuration节点下增加一下节点(实际上是配置EF的数据库连接字符串)

  <connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=Identity2Study;Integrated Security=SSPI" providerName="System.Data.SqlClient" />
  </connectionStrings>

2. 简单登陆

在Identity2Study项目下增加一个控制器名为:AccountController

[Authorize]
public class AccountController : Controller
{
    public AccountController()
    {
    }

    public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager)
    {
        UserManager = userManager;
        SignInManager = signInManager;
    }

    private ApplicationUserManager _userManager;
    public ApplicationUserManager UserManager
    {
        get
        {
            return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
        }
        private set
        {
            _userManager = value;
        }
    }
    private ApplicationSignInManager _signInManager;
    public ApplicationSignInManager SignInManager
    {
        get
        {
            return _signInManager ?? HttpContext.GetOwinContext().Get<ApplicationSignInManager>();
        }
        private set { _signInManager = value; }
    }

    [HttpGet]
    [AllowAnonymous]
    public ActionResult Login(string returnUrl)
    {
        ViewBag.ReturnUrl = returnUrl;
        return View();
    }

    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);

        switch (result)
        {
            case SignInStatus.Success:
                return RedirectToLocal(returnUrl);
            case SignInStatus.LockedOut:
                return View("Lockout");
            case SignInStatus.RequiresVerification:
                return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
            case SignInStatus.Failure:
            default:
                ModelState.AddModelError("", "Invalid login attempt.");
                return View(model);
        }
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult LogOff()
    {
        AuthenticationManager.SignOut();
        return RedirectToAction("Index", "Home");
    }

    //以下为辅助方法
    private ActionResult RedirectToLocal(string returnUrl)
    {
        if (Url.IsLocalUrl(returnUrl))
        {
            return Redirect(returnUrl);
        }
        return RedirectToAction("Index", "Home");
    }

    private IAuthenticationManager AuthenticationManager
    {
        get
        {
            return HttpContext.GetOwinContext().Authentication;
        }
    }
}

为Login添加一个视图

@model Identity2Study.Models.LoginViewModel

@{
    ViewBag.Title = "登录";
}
<h2>登录</h2>
@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Password, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.RememberMe, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                <div class="checkbox">
                    @Html.EditorFor(model => model.RememberMe)
                    @Html.ValidationMessageFor(model => model.RememberMe, "", new { @class = "text-danger" })
                </div>
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="登录" class="btn btn-default" />
            </div>
        </div>
    </div>
}

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

这里我们不添加注册账号的功能了,下面懒得改。这里我们直接使用上述添加的默认管理员登陆即可,如果不出意外,我们可以使用默认管理账号登陆


3. 使用邮箱或者用户名登陆

我们默认使用登陆的时候会提示我们输入邮箱登陆。这造成一个错觉,Identity2是使用邮箱登陆的,然后我们要改成其它登陆方式比如用户名登陆会需要重写方法什么的。但是!!这只是一个错觉,Identity默认用的就是用户名登陆,来看一眼我们的数据库:

单独看数据库是看不出什么来的。回到我们的登录控制器里面的代码

我下载了Identity2的源代码之后找到这个方法。

明明是用户名呀,联合前面的数据库。相信诸位看官已经明白是怎么一回事了。当然,这只能用用户名登陆了么?其实不是的,当然还有方法FindByEmailAsync。(图中那个user1是我自己写了,是为了打印出FindByEmailAsync())

其实除了用户名、邮箱登陆之外,还可以用手机号登陆。诸位看官接着看下去就会明白。
如果要验证我们的猜想是不是正确,我这里用了一个最笨的办法,直接修改数据库里面的UserName。

UserName字段值改成字符串001say

更改LoginViewModel

更改视图登陆代码

控制器这里只需改动点点

把原来的Email改成Name
运行登陆成功

验证了我们猜想是正确的。
这里是被Identity2给的那个例子挖了个坑,其实也怪我没仔细看源代码,回去看看我们建立默认账号的代码

if (user == null)
{
    user = new ApplicationUser { UserName = name, Email = name };
    var result = userManager.Create(user, password);
    result = userManager.SetLockoutEnabled(user.Id, false);
}

name同时被创建成UserName和Email了。剩下的事情就好办了
建立一个最简单的RegisterViewModel

public class RegisterViewModel
{
    [Required(ErrorMessage="用户名不能为空")]
    [Display(Name="用户名")]
    public string Name { get; set; }

    [Required]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}

控制器下添加如下代码

[HttpGet]
[AllowAnonymous]
public ActionResult Register()
{
    return View();
}

[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser { UserName = model.Name, Email = model.Email };
        var result = await UserManager.CreateAsync(user, model.Password);
        AddErrors(result);
    }
    return View(model);
}
        private void AddErrors(IdentityResult result)
{
    foreach (var error in result.Errors)
    {
        ModelState.AddModelError("", error);
    }
}

还有对应的视图代码,这里不贴出来了。动作选择Create模型选择RegisterViewModel即可。

查看数据库的AspNetUsers表多了一条记录

说了半天下面才是重点
重写ApplicationSignInManager类下面的PasswordSignInAsync方法。
找到Mvc.Identity解决方案的BLL文件夹下的类ApplicationSignInManager。需要我们添加一个辅助用的方法和重写PasswordSignInAsync方法。

private async Task<SignInStatus> SignInOrTwoFactor(ApplicationUser user, bool isPersistent)
{
    var id = Convert.ToString(user.Id);
    if (await UserManager.GetTwoFactorEnabledAsync(user.Id)
        && (await UserManager.GetValidTwoFactorProvidersAsync(user.Id)).Count > 0
        && !await AuthenticationManager.TwoFactorBrowserRememberedAsync(id))
    {
        var identity = new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie);
        identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id));
        AuthenticationManager.SignIn(identity);
        return SignInStatus.RequiresVerification;
    }
    await SignInAsync(user, isPersistent, false);
    return SignInStatus.Success;
}

public override async Task<SignInStatus> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout)
{
    if (UserManager == null)
    {
        return SignInStatus.Failure;
    }
    ApplicationUser user;
    string strRegex = @"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$";
    Regex re = new Regex(strRegex);
    if(re.IsMatch(userName))
    {
        user = await UserManager.FindByEmailAsync(userName);
    }
    else{
        user = await UserManager.FindByNameAsync(userName);
    }

    if (user == null)
    {
        return SignInStatus.Failure;
    }
    if (await UserManager.IsLockedOutAsync(user.Id))
    {
        return SignInStatus.LockedOut;
    }
    if (await UserManager.CheckPasswordAsync(user, password))
    {
        await UserManager.ResetAccessFailedCountAsync(user.Id);
        return await SignInOrTwoFactor(user, isPersistent);
    }
    if (shouldLockout)
    {
        // If lockout is requested, increment access failed count which might lock out the user
        await UserManager.AccessFailedAsync(user.Id);
        if (await UserManager.IsLockedOutAsync(user.Id))
        {
            return SignInStatus.LockedOut;
        }
    }
    return SignInStatus.Failure;
}

改一下LoginViewModel

public class LoginViewModel
{
    [Required(ErrorMessage="用户名或者邮箱不能为空")]
    [Display(Name = "用户名或邮箱")]
    public string Name { get; set; }

    [Required]
    [DataType(DataType.Password)]
    [Display(Name = "密码")]
    public string Password { get; set; }

    [Display(Name = "记住我?")]
    public bool RememberMe { get; set; }
}

剩下都不用改,直接运行。会发现我们使用用户名或者邮箱均可登陆!


3. 用户注册必须邮件验证后登陆

这里我个人理解为两个意思

  • 注册后是可以登陆,但是会给没有用邮件确认的用户分配一个权限最小的角色
  • 注册后必须通过邮件确认后才允许登陆,否则登陆不成功

第一种比较好办,就是在默认注册的控制器里面给新建的用户分配一个最小的角色,然后在邮件确认的方法里面重新分配一个角色即可。我这里想用的是第二种,没有确定邮件之前不允许登录。

由于Identity2的SignInStatus枚举类型里面并没有邮件是否确定的项,所以我们需要自己另外定义一个枚举类型(可能是我没发现,如果有知道的希望能指点我)

如图,打开Mvc.Identity根目录新建立一个Common的文件夹,新建一个类AppSignInStatus.cs添加如下代码

namespace Mvc.Identity.Common
{
    /// <summary>
    /// Possible results from a sign in attempt
    /// </summary>
    public enum AppSignInStatus
    {
        /// <summary>
        /// Sign in was successful
        /// </summary>
        Success,

        /// <summary>
        /// User is locked out
        /// </summary>
        LockedOut,

        /// <summary>
        /// Sign in requires addition verification (i.e. two factor)
        /// </summary>
        RequiresVerification,

        /// <summary>
        /// Sign in failed
        /// </summary>
        Failure,
        /// <summary>
        /// make sure email
        /// </summary>
        NotSureEmail
    }
}

我们只在SignInStatus基础上加了最后一个NotSureEmail
重新修改ApplicationUserManager类里面的两个方法如下:

private async Task<AppSignInStatus> SignInOrTwoFactor(ApplicationUser user, bool isPersistent)
{
    var id = Convert.ToString(user.Id);
    if (await UserManager.GetTwoFactorEnabledAsync(user.Id)
        && (await UserManager.GetValidTwoFactorProvidersAsync(user.Id)).Count > 0
        && !await AuthenticationManager.TwoFactorBrowserRememberedAsync(id))
    {
        var identity = new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie);
        identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id));
        AuthenticationManager.SignIn(identity);
        return AppSignInStatus.RequiresVerification;
    }
    await SignInAsync(user, isPersistent, false);
    return AppSignInStatus.Success;
}

public new async Task<AppSignInStatus> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout,bool markSureEmail)
{
    if (UserManager == null)
    {
        return AppSignInStatus.Failure;
    }
    ApplicationUser user;
    string strRegex = @"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$";
    Regex re = new Regex(strRegex);
    if(re.IsMatch(userName))
    {
        user = await UserManager.FindByEmailAsync(userName);
    }
    else{
        user = await UserManager.FindByNameAsync(userName);
    }

    if (user == null)
    {
        return AppSignInStatus.Failure;
    }
    if (await UserManager.IsLockedOutAsync(user.Id))
    {
        return AppSignInStatus.LockedOut;
    }
    if (!await UserManager.IsEmailConfirmedAsync(user.Id) && markSureEmail )
    {
        return AppSignInStatus.NotSureEmail;
    }
    if (await UserManager.CheckPasswordAsync(user, password))
    {
        await UserManager.ResetAccessFailedCountAsync(user.Id);
        return await SignInOrTwoFactor(user, isPersistent);
    }
    if (shouldLockout)
    {
        // If lockout is requested, increment access failed count which might lock out the user
        await UserManager.AccessFailedAsync(user.Id);
        if (await UserManager.IsLockedOutAsync(user.Id))
        {
            return AppSignInStatus.LockedOut;
        }
    }
    return AppSignInStatus.Failure;
}

注意此时的PasswordSignInAsync方法不再是重写父类的了,而是显示的覆盖掉了父类里面的PasswordSignInAsync方法

回到Account控制器,添加一下方法:

//该方法为辅助方法
 private void AddErrors(IdentityResult result)
 {
     foreach (var error in result.Errors)
     {
         ModelState.AddModelError("", error);
     }
 }

//用户邮件确认
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
    if (userId == null || code == null)
    {
        return View("Error");
    }
    var result = await UserManager.ConfirmEmailAsync(userId, code);
    return View(result.Succeeded ? "SureEmail" : "Error");
}

修改Register方法为以下代码

[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser { UserName = model.Name, 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>");
            return View("ConfirmeEmail");
        }

        AddErrors(result);
    }
    return View(model);
}

修改Login方法为以下代码

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }
    
    var result = await SignInManager.PasswordSignInAsync(model.Name, model.Password, model.RememberMe, shouldLockout: false,markSureEmail: true);

    switch (result)
    {
        case AppSignInStatus.Success:
            return RedirectToLocal(returnUrl);
        case AppSignInStatus.LockedOut:
            return View("Lockout");
        case AppSignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
        case AppSignInStatus.NotSureEmail:
            return View("ConfirmeEmail");
        case AppSignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");
            return View(model);
    }
}

并且添加两个视图文件位于View => Account文件夹下
ConfirmeEmail.cshtml

@{
    ViewBag.Title = "登录";
}

<h3>请登录你的邮箱并确认!</h3>

SureEmail.cshtml

@{
    ViewBag.Title = "确认邮件";
}
<h3>邮件已确认,现在您登录本网站了</h3>

完成后,我们使用默认建立的账号登陆试试。

因为我们这个默认账号在建立的时候,并未指定它已经验证过了。所以登陆时会提示我们没有确认邮件

怎么让我们建立的默认账号从一开始就不需要邮件验证呢?
修改一下建立默认账号的那段代码,把EmailConfirmed为true即可。

接下来我们建立一个新的账号并且测试一下。

注册没有验证过后登录会需要我们确认注册。

假如我们不需要注册的用户邮件验证而是直接可以登陆怎么办?还需要改代码么?回到刚刚说的是覆盖不是重写的PasswordSignInAsync方法,仔细看一下我们最后一个参数。如果需要则传一个true进去,这里需要显示指定是哪个参数

var result = await SignInManager.PasswordSignInAsync(model.Name, model.Password, model.RememberMe, shouldLockout: false,markSureEmail: true);

至于到底在注册登陆的时候需不需要验证邮箱,可以在数据库里面存。也可以在web.config文件里面添加节点存。自由发挥!

至此,我们可以使用邮箱或者用户名登陆,并且登陆之前必须确认邮件有效


posted @ 2015-04-27 23:41  001say  阅读(4004)  评论(6编辑  收藏  举报