MVC5默認項目解析

前言

本文簡單介紹Identity的使用,使用的案例是基於默認的Mvc5項目(只勾選MVC,不勾選WebApi).讀者能夠拿着項目源碼對照地看.前端

ASP.NET Identity特性

  • One ASP.NET Identity 系統
  • 更容易加入用戶的我的資料信息
  • 持久化控制
  • 單元測試能力
  • 角色提供程序
  • 基於聲明的 (Claims Based)
  • 社交帳號登陸提供程序 (Social Login Providers)
  • Windows Azure Active Directory
  • OWIN 集成
  • NuGet 包

Identity包

Identity是依賴於EF的Code First 和Owin的,固然你能夠本身拿着Micsoft.AspNet.Identity.Core重寫一份不依賴EF的Identity.git

用戶數據庫由EF Code First建立,帳號等功能經過Owin的中間件形式加入到程序中(Owin中間件至關於Asp.Net 的Module)數據庫

  • Microsoft.AspNet.Identity.EntityFramework安全

    這個包容納了 ASP.NET Identity 基於 Entity Framework 的實現。它將 ASP.NET Identity 的數據和架構存入 SQL Server。cookie

  • Microsoft.AspNet.Identity.Core架構

    這個包容納了 ASP.NET Identity 的核心接口。它能夠用來編寫 ASP.NET Identity 的其餘實現,用以支持其餘持久化存儲系統,如 Windows Azure 表存儲, NoSQL 數據庫等等。app

  • Microsoft.AspNet.Identity.OWIN框架

    這個包爲 ASP.NET 應用程序提供了將 ASP.NET Identity 引入到 OWIN 身份驗證的功能。當你在爲應用程序加入登陸功能,調用 OWIN Cookie 身份驗證中間件來生成 cookie 時,會用到這個包。async

技術分享

如上圖所示Identity依賴了不少東西每一個都是大框架,所以本文要求讀者有必定的EF Code First和Owin知識ide

基本

Identity採用EF Code First,他內置了一些類用戶建立數據庫,所以 
在默認狀況下Identity會建立下列表格 
技術分享

Identity用的數據庫上下文有點特別,是繼承IdentityDbContext,正是繼承了這個特殊的上下文,纔會有那麼多默認表

    1. public class MyIdentityDbContext : IdentityDbContext<MyIdentityUser>
    2. {
    3. //能夠在這裏擴展本身的表,配置數據表
    4. }

MyIdentityUser我自定義的,是實現IdentityUser接口的類

默認狀況下是沒有數據庫的,直到建立一個新用戶,EF纔會去建立數據庫 
這個數據庫會建立在App_Data下 
技術分享 
由於在Web.config配置了數據庫生成位置

    1. <connectionStrings>
    2. <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\aspnet-DefaultMVC5-20160806094030.mdf;Initial Catalog=aspnet-DefaultMVC5-20160806094030;Integrated Security=True"
    3. providerName="System.Data.SqlClient" />
    4. </connectionStrings>

IdentityUser

對應數據表中AspNetUsers 
該類描述用戶數據.咱們先只注意用戶部分忽略登入記錄,角色,申明的部分

IdentityUser默認成員

名稱 描述
Claims 返回用戶的聲明集合
Email 返回用戶的E-mail地址
Id 返回用戶的惟一ID
Logins 返回用戶的登陸集合
PasswordHash 返回哈希格式的用戶口令,在「實現Edit特性」中會用到它
Roles 返回用戶所屬的角色集合
PhoneNumber 返回用戶的電話號碼
SecurityStamp 返回變動用戶標識時被修改的值,例如被口令修改的值
UserName 返回用戶名

AspNetUser表結構 
技術分享

可使用EF Code First相關的知識對默認表進行配置

    1. //改表名
    2. protected override void OnModelCreating(DbModelBuilder modelBuilder)
    3. {
    4. modelBuilder.Entity<IdentityUserRole>().ToTable("MyUserRoles");
    5. }
    6. //忽略列,注意不是全部列都能忽略
    7. modelBuilder.Entity<MyIdentityUser>().Ignore(x => x.PhoneNumberConfirmed);

UserManager

用戶管理類,其職責至關於業務邏輯層

名稱 描述
ChangePasswordAsync(id, old, new) 爲指定用戶修改口令
CreateAsync(user) 建立一個不帶口令的新用戶
CreateAsync(user, pass) 建立一個帶有指定口令的新用戶
DeleteAsync(user) 刪除指定用戶
FindAsync(user, pass) 查找表明該用戶的對象,並認證其口令
FindByIdAsync(id) 查找與指定ID相關聯的用戶對象
FindByNameAsync(name) 查找與指定名稱相關聯的用戶對象,第14章「種植數據庫」時會用到這個方法
UpdateAsync(user) 將用戶對象的修改送入數據庫
Users 返回用戶枚舉

一樣的能夠用繼承的方式擴展本身的用戶管理類

準備工做

在使用Identity前先要作一些配置 
首先是Owin 
默認的項目會建立一個Startup.cs,該類上的OwinStartupAttribute特性標註了該類爲啓動類 
這個類含有一個名稱爲Configuration的方法,該方法由OWIN基礎架構進行調用,併爲該方法傳遞一個Owin.IAppBuilder接口的實現,由它支持應用程序所需中間件的設置

    1. [assembly: OwinStartupAttribute(typeof(DefaultMVC5.Startup))]
    2. namespace DefaultMVC5
    3. {
    4. public partial class Startup
    5. {
    6. public void Configuration(IAppBuilder app)
    7. {
    8. ConfigureAuth(app);
    9. }
    10. }
    11. }

同時這個類是個部分類,在App_start中能找到另外一部分ConfigureAuth就是用於配置Identity

    1. public void ConfigureAuth(IAppBuilder app)
    2. {
    3. app.CreatePerOwinContext(ApplicationDbContext.Create);
    4. app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
    5. //省略,先不解釋
    6. }

Startup只在網站啓動的時候執行,上面這段代碼的CreatePerOwinContext須要傳入一個委託,這個委託能返回一個對象,而這個對象在一次請求中是惟一的.因此很是時候放置相似數據庫上下文之類的類. 
本質是每當用戶請求時候Owin講調用這些委託來建立對象,並把對象保存到OwinContext中.而後能夠在應用程序的任何位置使用

    1. HttpContext.GetOwinContext().Get<ApplicationSignInManager>()
    2. //你可能會看到
    3. HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
    4. //這個泛型擴展方法內部調用了context.Get<TManager>();,感受這個擴展方法只是用來打醬油的

來得到這個對象.

GetOwinContext是擴展方法,他會從HttpContext.Items中得到Owin以前保存在裏面的信息,再生成OwinContext

總之使用CreatePerOwinContext能夠保存一個對象在Owin上下文,使得一次請求中用到同一個對象.

ApplicationDbContext

/Models/IdentityModels.cs

數據庫上下文類和用戶類都是繼承Identity內置類的,爲了能擴展本身想要的表或表的字段

    1. public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    2. {
    3. public ApplicationDbContext()
    4. : base("DefaultConnection", throwIfV1Schema: false)
    5. {
    6. }
    7. //給Owin用的
    8. public static ApplicationDbContext Create()
    9. {
    10. return new ApplicationDbContext();
    11. }
    12. }

ApplicationUserManager

/App_Start/IdentityConfig.cs

    1. public class ApplicationUserManager : UserManager<ApplicationUser>
    2. {
    3. public ApplicationUserManager(IUserStore<ApplicationUser> store)
    4. : base(store)
    5. {
    6. }
    7. public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
    8. {
    9. var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
    10. /*
    11. manager配置代碼
    12. */
    13. return manager;
    14. }
    15. }

值得注意的是Manager的建立須要用到UserStore,若是ApplicationUserManager相等於業務層,那麼他的職責至關於數據層. 
還有一點是這個Create方法的參數與ApplicationDbContext的Create不一樣

    1. IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context

這個Create也也是能被Owin的CreatePerOwinContext使用.參數context就是Owin上下文,Create中使用context.Get<ApplicationDbContext>得到保存在context中的ApplicationDbContext對象而不用再次手動建立

註冊案例

Controllers/AccountController.cs

帳號管理的相關代碼在這個控制器中,你會常看到這類代碼,從Owin上下文得到ApplicationUserManager對象,以便管理用戶

    1. private ApplicationUserManager _userManager;
    2. public ApplicationUserManager UserManager
    3. {
    4. get
    5. {
    6. return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
    7. }
    8. private set
    9. {
    10. _userManager = value;
    11. }
    12. }
    1. [HttpPost]
    2. [AllowAnonymous]
    3. [ValidateAntiForgeryToken]
    4. public async Task<ActionResult> Register(RegisterViewModel model)
    5. {
    6. if (ModelState.IsValid)
    7. {
    8. //建立新用戶
    9. var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
    10. var result = await UserManager.CreateAsync(user, model.Password);
    11. if (result.Succeeded)
    12. {
    13. //若是註冊成功同時登入,SignInManager後面解釋
    14. await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
    15. return RedirectToAction("Index", "Home");
    16. }
    17. AddErrors(result);
    18. }
    19. // 若是咱們進行到這一步時某個地方出錯,則從新顯示錶單
    20. return View(model);
    21. }

登入案例

用戶登入後有一個比較重要的步驟讓網站記住這個用戶登入了(能夠說是受權),傳統作法會把用戶數據類保存到Session中用戶再請求使用查看他是否在Session保存了用戶數據.而Session這種作法是利用Cookie來標識用戶. 
在Identity中並非用Session,但仍是借用了Cookie 
爲了開啓Cookie受權在Startup類中使用這個中間件(Middleware)

    1. app.UseCookieAuthentication(new CookieAuthenticationOptions
    2. {
    3. AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    4. LoginPath = new PathString("/Account/Login"),//當訪問未受權頁面時將自定跳轉到這個位置
    5. CookieName = "MyCookieName",//自定義Cookie名稱
    6. Provider = new CookieAuthenticationProvider
    7. {
    8. // 當用戶登陸時使應用程序能夠驗證安全戳。
    9. // 這是一項安全功能,當你更改密碼或者向賬戶添加外部登陸名時,將使用此功能。
    10. OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
    11. validateInterval: TimeSpan.FromMinutes(30),
    12. regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
    13. }
    14. });

在亮出MVC5默認項目代碼前,我想先展現下<<Pro Asp.Net MVC5 Platform>>的代碼,由於他更加的直觀.

    1. [HttpPost]
    2. [AllowAnonymous]
    3. [ValidateAntiForgeryToken]
    4. public async Task<ActionResult> Login(LoginModel details, string returnUrl) {
    5. if (ModelState.IsValid) {
    6. AppUser user = await UserManager.FindAsync(details.Name,details.Password);
    7. if (user == null) {
    8. ModelState.AddModelError("", "Invalid name or password.");
    9. } else {
    10. //得到用戶的標識,全部的標識都實現IIdentity接口,這個是基於聲明的標識,聲明下文再講,只要知道他與受權有關
    11. ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,DefaultAuthenticationTypes.ApplicationCookie);
    12. AuthManager.SignOut();
    13. AuthManager.SignIn(new AuthenticationProperties {
    14. IsPersistent = false}, ident);
    15. return Redirect(returnUrl);
    16. }
    17. }
    18. ViewBag.returnUrl = returnUrl;
    19. return View(details);
    20. }

這塊代碼很直觀,得到用戶帳號密碼,去數據庫查是否存在,若是存在就登入,順帶得到用戶的聲明信息.

下面是MVC5默認項目中的代碼

    1. [HttpPost]
    2. [AllowAnonymous]
    3. [ValidateAntiForgeryToken]
    4. public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
    5. {
    6. if (!ModelState.IsValid)
    7. {
    8. return View(model);
    9. }
    10. // 這不會計入到爲執行賬戶鎖定而統計的登陸失敗次數中
    11. // 若要在屢次輸入錯誤密碼的狀況下觸發賬戶鎖定,請更改成 shouldLockout: true
    12. var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
    13. switch (result)
    14. {
    15. case SignInStatus.Success:
    16. return RedirectToLocal(returnUrl);
    17. case SignInStatus.LockedOut:
    18. return View("Lockout");
    19. case SignInStatus.RequiresVerification:
    20. return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
    21. case SignInStatus.Failure:
    22. default:
    23. ModelState.AddModelError("", "無效的登陸嘗試。");
    24. return View(model);
    25. }
    26. }

這份代碼中並無上面那樣直觀,它用了SignInManager,這個是個ApplicationSignInManager類,很容易猜到他是自定義的類,繼承自SignInManager(Identity內置的).該類是利用UserManager執行一系列登入操做 
其實內部實現大體就與上上段代碼同樣,也是查找用戶判斷是否存在….

但它作的更多,單PasswordSignInAsync這個方法它不只負責查詢用戶,登入用戶,還負責記錄用戶登入記錄(登入失敗幾回,對於被鎖定用戶的處理…).

用戶信息驗證

任何用戶輸入都須要驗證,用戶信息更是如此. 
在默認項目中不只利用了MVC內置的模型驗證,還利用了Identity的驗證器. 
就拿註冊來講,首先自定義了ViewModel,並打上驗證特性

    1. public class RegisterViewModel
    2. {
    3. [Required]
    4. [EmailAddress]
    5. [Display(Name = "電子郵件")]
    6. public string Email { get; set; }
    7. [Required]
    8. [StringLength(100, ErrorMessage = "{0} 必須至少包含 {2} 個字符。", MinimumLength = 6)]
    9. [DataType(DataType.Password)]
    10. [Display(Name = "密碼")]
    11. public string Password { get; set; }
    12. [DataType(DataType.Password)]
    13. [Display(Name = "確認密碼")]
    14. [Compare("Password", ErrorMessage = "密碼和確認密碼不匹配。")]
    15. public string ConfirmPassword { get; set; }
    16. }

這裏的驗證能配合HtmlHelper實現客戶端校驗. 
其次利用Identity的驗證器,關鍵點在下面代碼第一行,嘗試登入,若是失敗的話把result中的錯誤信息返回給前端,AddErrors方法添加的是模型級錯誤,經過@Html.ValidationSummary()能顯示出來

    1. var result = await UserManager.CreateAsync(user, model.Password);
    2. if (result.Succeeded)
    3. {
    4. await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
    5. return RedirectToAction("Index", "Home");
    6. }
    7. AddErrors(result);
    8. ......
    9. private void AddErrors(IdentityResult result)
    10. {
    11. foreach (var error in result.Errors)
    12. {
    13. ModelState.AddModelError("", error);
    14. }
    15. }

用戶名密碼驗證器

App_Start/ApplicationUserManager/Create

    1. // 配置用戶名的驗證邏輯
    2. manager.UserValidator = new UserValidator<ApplicationUser>(manager)
    3. {
    4. AllowOnlyAlphanumericUserNames = false,
    5. RequireUniqueEmail = true
    6. };
    7. // 配置密碼的驗證邏輯
    8. manager.PasswordValidator = new PasswordValidator
    9. {
    10. RequiredLength = 6,
    11. RequireNonLetterOrDigit = true,
    12. RequireDigit = true,
    13. RequireLowercase = true,
    14. RequireUppercase = true,
    15. };

PasswordValidator屬性定義

名稱 描述
RequiredLength 指定合法口令的最小長度
RequireNonLetterOrDigit 當設置爲true時,合法口令必須含有非字母和數字的字符
RequireDigit 當設置爲true時,合法口令必須含有數字
RequireLowercase 當設置爲true時,合法口令必須含有小寫字母
RequireUppercase 當設置爲true時,合法口令必須含有大寫字母

UserValidator屬性定義

名稱 描述
AllowOnlyAlphanumericUserNames 當爲true時,用戶名只能含有字母數字字符
RequireUniqueEmail 當爲true時,郵件地址必須惟一

配置驗證器後就能在有UserManager的地方使用它UserManager.PasswordValidator.ValidateAsync
一般SignInAsync這些方法內部都會調用他們的.

自定義驗證器

自定義用戶驗證器

    1. public class CustomUserValidator : UserValidator<AppUser> {
    2. public CustomUserValidator(AppUserManager mgr) : base(mgr) {
    3. }
    4. public override async Task<IdentityResult> ValidateAsync(AppUser user) {
    5. //使用內建驗證策略
    6. IdentityResult result = await base.ValidateAsync(user);
    7. //在此基礎上添加本身的驗證策略
    8. if (!user.Email.ToLower().EndsWith("@example.com")) {
    9. var errors = result.Errors.ToList();
    10. errors.Add("Only example.com email addresses are allowed");
    11. result = new IdentityResult(errors);
    12. }
    13. return result;
    14. }
    15. }

自定義口令驗證器

    1. public class CustomPasswordValidator : PasswordValidator {
    2. public override async Task<IdentityResult> ValidateAsync(string pass) {
    3. IdentityResult result = await base.ValidateAsync(pass);
    4. if (pass.Contains("12345")) {
    5. var errors = result.Errors.ToList();
    6. errors.Add("Passwords cannot contain numeric sequences");
    7. result = new IdentityResult(errors);
    8. }
    9. return result;
    10. }
    11. }
相關文章
相關標籤/搜索