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文件里面添加節點存。自由發揮!

至此,我們可以使用郵箱或者用戶名登陸,並且登陸之前必須確認郵件有效



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM