ASP.NET MVC第012天_ASP.NET Identity使用筆記_初始配置到註冊篇

 

從.net framework 4.5之後 微軟為了改善Membership
釋出了.net Identity新的一套身分驗證系統的框架
不會和System.Web有強烈相依且能夠支持OWIN。
也開放了跟其他第三方平台(Facebook , Twitter , Google) 登入的彈性



首先安裝好如下套件



EntityFramework (因為.net Identity 會依賴EF)





Microsoft.AspNet.Identity.EntityFramework

Microsoft.AspNet.Identity.EntityFramework.zh-Hant





Microsoft.AspNet.Identity.Owin

Microsoft.AspNet.Identity.Owin.zh-Hant




Microsoft.Owin.Host.SystemWeb (為了讓其能夠RUN在IIS )


當你把asp.net identity相關插件都安裝好後預設你會發現重新建置並RUN
原本好端端的.net mvc專案突然會報錯


[錯誤訊息]

'/' 應用程式中發生伺服器錯誤。


The following errors occurred while attempting to load the app.
- No assembly found containing an OwinStartupAttribute.
- No assembly found containing a Startup or [AssemblyName].Startup class.
To disable OWIN startup discovery, add the appSetting owin:AutomaticAppStartup with a value of "false" in your web.config.
To specify the OWIN startup Assembly, Class, or Method, add the appSetting owin:AppStartup with the fully qualified startup class or configuration method name in your web.config.




此時可以先到 web.config暫時去設置

<add key="owin:AutomaticAppStartup" value="false" /> 


Step1.建立繼承IdentityUser的Model Sub Class
Identity本身框架中
IdentityUser就有包含一些跟身分驗證有關的欄位

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#region 組件 Microsoft.AspNet.Identity.EntityFramework, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
// D:\SideProjects\AgricultureManagementSystem\packages\Microsoft.AspNet.Identity.EntityFramework.2.2.3\lib\net45\Microsoft.AspNet.Identity.EntityFramework.dll
#endregion

using System;
using System.Collections.Generic;

namespace Microsoft.AspNet.Identity.EntityFramework
{
    public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
        where TLogin : IdentityUserLogin<TKey>
        where TRole : IdentityUserRole<TKey>
        where TClaim : IdentityUserClaim<TKey>
    {
        public IdentityUser();

        public virtual string Email { get; set; }
        public virtual bool EmailConfirmed { get; set; }
        public virtual string PasswordHash { get; set; }
        public virtual string SecurityStamp { get; set; }
        public virtual string PhoneNumber { get; set; }
        public virtual bool PhoneNumberConfirmed { get; set; }
        public virtual bool TwoFactorEnabled { get; set; }
        public virtual DateTime? LockoutEndDateUtc { get; set; }
        public virtual bool LockoutEnabled { get; set; }
        public virtual int AccessFailedCount { get; set; }
        public virtual ICollection<TRole> Roles { get; }
        public virtual ICollection<TClaim> Claims { get; }
        public virtual ICollection<TLogin> Logins { get; }
        public virtual TKey Id { get; set; }
        public virtual string UserName { get; set; }
    }
}


一般起手式會去新增一個User相關的Class並繼承IdentityUser再去做其餘欄位的擴充
這裡我新增一個Account欄位 、 一個Region欄位

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace AgricultureManagementSystem.Models
{
    public class User : IdentityUser
    {
        public virtual string Account { get; set; }
public virtual string Region { get; set; } } }

Step2.建立繼承IdentityDbContext<剛建的繼承IdentityUser的Sub Class>的Sub Class
並準備好web.config連線字串

在asp.net identity框架中

本身有封裝IdentityDbContext<T>可供我們傳入自訂的泛型
讓我們可透過EF進行DB相關操作互動

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#region 組件 Microsoft.AspNet.Identity.EntityFramework, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
// D:\SideProjects\AgricultureManagementSystem\packages\Microsoft.AspNet.Identity.EntityFramework.2.2.3\lib\net45\Microsoft.AspNet.Identity.EntityFramework.dll
#endregion

using System.Data.Common;
using System.Data.Entity.Infrastructure;

namespace Microsoft.AspNet.Identity.EntityFramework
{
    public class IdentityDbContext<TUser> : IdentityDbContext<TUser, IdentityRole, string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim> where TUser : IdentityUser
    {
        public IdentityDbContext();
        public IdentityDbContext(string nameOrConnectionString);
        public IdentityDbContext(DbCompiledModel model);
        public IdentityDbContext(string nameOrConnectionString, bool throwIfV1Schema);
        public IdentityDbContext(DbConnection existingConnection, bool contextOwnsConnection);
        public IdentityDbContext(string nameOrConnectionString, DbCompiledModel model);
        public IdentityDbContext(DbConnection existingConnection, DbCompiledModel model, bool contextOwnsConnection);
    }
}


以下我們在Models目錄去新增一個IdentityDbContext的Class讓它去繼承自
IdentityDbContext<T>
這裡泛型傳入剛建立的User型別

我們這裡要採用Code First方式來建立Identity資料庫

web.config

<connectionStrings>
    <add name="IdentityDbConn" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=IdentityDb;uid=sa;pwd=rootroot" providerName="System.Data.SqlClient" />
  </connectionStrings>




調整改寫後的IdentityDbContext的Class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace AgricultureManagementSystem.Models
{
    public class IdentityDbContext : IdentityDbContext<User>
    {
        public IdentityDbContext() : base("IdentityDbConn", false)
        {

        }

        public static IdentityDbContext Create()
        {
            return new IdentityDbContext();
        }

    }
}

使用其第四種建構子多載形式傳入web.config中的連線字串相關資訊
這裡要跟web.config 連線的name一致

產生Create 的static方法用於建立Db Context



Step3.在 App_Start 資料夾中加入 IdentityConfig.cs並建立繼承自UserManager<T>的用戶管理Sub Class

namespace預設產生的後綴.App_Start去除
將預設的class名稱IdentityConfig改為UserManager繼承UserManager<T>
UserManager<T>這裡泛型傳入User






UserManager<T>當中的封裝

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#region 組件 Microsoft.AspNet.Identity.Core, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
// D:\SideProjects\AgricultureManagementSystem\packages\Microsoft.AspNet.Identity.Core.2.2.3\lib\net45\Microsoft.AspNet.Identity.Core.dll
#endregion


namespace Microsoft.AspNet.Identity
{
    //
    // 摘要:
    //     UserManager for users where the primary key for the User is of type string
    //
    // 類型參數:
    //   TUser:
    public class UserManager<TUser> : UserManager<TUser, string> where TUser : class, IUser<string>
    {
        //
        // 摘要:
        //     Constructor
        //
        // 參數:
        //   store:
        public UserManager(IUserStore<TUser> store);
    }
}


由於和User一系列操作主要會定義於IUserStore Interface<User>之中。
因此需在Constructor中把IUserStore<User>做參數


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using AgricultureManagementSystem.Models;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace AgricultureManagementSystem
{
    public class UserManager : UserManager<User>
    {
        public UserManager(IUserStore<User> store) : base(store)
        {

        }

        public static UserManager Create(IdentityFactoryOptions<UserManager> options, IOwinContext context)
        {
            var manager = new UserManager(new UserStore<User>(context.Get<Models.IdentityDbContext>()));
            return manager;
        }
    }
}



這裡額外擴充的
UserManager Create(IdentityFactoryOptions<UserManager> options, IOwinContext context)
用來去產生UserManager的實體加以管理後續用戶相關的操作


Step4.註冊UserManager

身分驗證的配置主要藉由CreatePerOwinContext 這個方法
來去設置DbContext還有UserManager

在此可以於App_Start的目錄下添加一個Startup.Auth.cs的partial class
當中定義的Class命名為Startup,視作OWIN Startup class的一部分。
這裡還要記得將Startup用partual修飾並更改namespace
把namespace .App_Start後綴去除改多加.IdentityAuth來區分

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using AgricultureManagementSystem;
using AgricultureManagementSystem.Models;
using Microsoft.AspNet.Identity;
using Microsoft.Owin.Security.Cookies;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace AgricultureManagementSystem.IdentityAuth
{
    public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder app)
        {
            app.CreatePerOwinContext(IdentityDbContext.Create);
            app.CreatePerOwinContext<UserManager>(UserManager.Create);
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new Microsoft.Owin.PathString("/User/Login")
            });
        }
    }
}


這邊設置驗證型別為ApplicationCookie
並藉由LoginPath指定認證失敗後轉向的目標URL



Step5.建立OWIN Startup class

自vs2017之後的版本都能夠直接新建OWIN Startup的class
此外OWIN Startup必須被建立在專案根目錄下
這裡也要記得改為partial修飾並後綴改多加.IdentityAuth
進行區分

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
using Microsoft.Owin;
using Owin;
using System;
using System.Threading.Tasks;

[assembly: OwinStartup(typeof(AgricultureManagementSystem.IdentityAuth.Startup))]

namespace AgricultureManagementSystem.IdentityAuth
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }
    }
}


在專案裡面我們新增兩隻js套件
Microsoft.jQuery.Unobtrusive.Validation
jQuery.Validation



jQuery由於一些安全問題也記得升級至最新3.6


用戶註冊處理

在RegisterController.cs中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
using AgricultureManagementSystem.Models;
using AgricultureManagementSystem.ViewModels;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;

namespace AgricultureManagementSystem.Controllers
{
    public class RegisterController : Controller
    {
        private UserManager _userManager;
        public UserManager UserManager
        {
            get
            {
                return _userManager ?? HttpContext.GetOwinContext().GetUserManager<UserManager>();
            }
            private set
            {
                _userManager = value;
            }
        }

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


        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Add(AddUserViewModel addUserViewModel)
        {
            if (ModelState.IsValid)
            {
                var user = new User
                {
                    UserName = addUserViewModel.Account,
                    Email = addUserViewModel.Email,
                    Region = addUserViewModel.Region
                };

                var result = await UserManager.CreateAsync(user, addUserViewModel.Password);
                if (result.Succeeded)
                {
                    return RedirectToAction("Index", "Login");
                }
                AddErrors(result);
            }

            //return RedirectToAction("Index", "Register", addUserViewModel);
            return View(addUserViewModel);
        }

        private void AddErrors(IdentityResult identityResult)
        {
            foreach (var error in identityResult.Errors)
            {
                ModelState.AddModelError("", error);
            }
        }

    }
}




藉由HttpCOntext.GetOwinContext().Get()來將有使用CreatePerOwinContext()的方法註冊的
IdentityDbContext和UserManager實體做存取獲得後就能在當前Controller中使用。


新增好get用於一開始注冊頁面的呈現
跟post提交註冊表單資訊
兩種 action之後




再回去web.config把之前寫的
<add key="owin:AutomaticAppStartup" value="false" /> 

註解或移除掉
因為目前已經有把Startup設計好了

若沒移除做註冊的表單提交會報如下錯誤
No owin.Environment item was found in the context.



前端View
.\Views\Register\Add.cshtml

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
@{
    Layout = null;
}
@model AgricultureManagementSystem.ViewModels.AddUserViewModel

<!DOCTYPE html>

<html>

<head>

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>註冊</title>

    <!-- Custom fonts for this template-->
    <link href="~/vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
    <link href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"
          rel="stylesheet">

    <!-- Custom styles for this template-->
    <link href="~/css/sb-admin-2.min.css" rel="stylesheet">

</head>

<body class="bg-gradient-primary">

    <div class="container">

        <div class="card o-hidden border-0 shadow-lg my-5">
            <div class="card-body p-0">
                <!-- Nested Row within Card Body -->
                <div class="row">
                    <div class="col-lg-5 d-none d-lg-block bg-register-image"></div>
                    <div class="col-lg-7">
                        <div class="p-5">
                            <div class="text-center">
                                <h1 class="h4 text-gray-900 mb-4">Create an Account!</h1>
                            </div>
                            @using (Html.BeginForm("Add", "Register", FormMethod.Post, new { @class = "user", role = "form" }))
                            {
                                @Html.AntiForgeryToken()
                                <hr />
                                @Html.ValidationSummary("", new { @class = "text-danger" })
                                <div class="form-group">
                                    @Html.TextBoxFor(m => m.Account, new { @class = "form-control form-control-user", placeholder = "請輸入帳號" })
                                </div>

                                <div class="form-group">
                                    @{
                                        List<SelectListItem> items = new List<SelectListItem>();
                                        items.Add(new SelectListItem() { Text = "請選擇一個外場地名", Value = "", Selected = true });
                                        items.Add(new SelectListItem() { Text = "萬合", Value = "萬合", Selected = false });
                                        items.Add(new SelectListItem() { Text = "二林", Value = "二林", Selected = false });
                                        items.Add(new SelectListItem() { Text = "山寮", Value = "山寮", Selected = false });
                                        items.Add(new SelectListItem() { Text = "丈八斗", Value = "丈八斗", Selected = false });
                                        items.Add(new SelectListItem() { Text = "梨頭厝", Value = "梨頭厝", Selected = false });
                                    }
                                    @Html.DropDownListFor(m => m.Region, items, new { @class = "form-control", style = "font-size: 0.8rem; border-radius: 10rem;" })
                                    @*@Html.DropDownList("Region", items, new { @class= "form-control",style= "font-size: 0.8rem; border-radius: 10rem;" })*@
                                </div>

                                <div class="form-group">
                                    @Html.TextBoxFor(m => m.Email, new { @class = "form-control form-control-user", @type = "email", placeholder = "請輸入Email" })
                                </div>

                                <div class="form-group">
                                    @Html.TextBoxFor(m => m.Password, new { @class = "form-control form-control-user", @type = "password", placeholder = "請輸入密碼" })
                                </div>
                                <div class="form-group">
                                    @Html.TextBoxFor(m => m.ConfirmPassword, new { @class = "form-control form-control-user", @type = "password", placeholder = "請再次輸入密碼" })
                                </div>
                                <input type="submit" class="btn btn-primary btn-user btn-block" value="Register Account" />
                                <hr>
                            }

                            <div class="text-center">
                                <a class="small" href="/Password/Index">Forgot Password?</a>
                            </div>
                            <div class="text-center">
                                <a class="small" href="/Login/Index">Already have an account? Login!</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

    </div>

    <!-- Bootstrap core JavaScript-->
    <script src="~/vendor/jquery/jquery.min.js"></script>
    <script src="~/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>

    <!-- Core plugin JavaScript-->
    <script src="~/vendor/jquery-easing/jquery.easing.min.js"></script>

    <!-- Custom scripts for all pages-->
    <script src="~/js/sb-admin-2.min.js"></script>

</body>
</html>


當完成提交過程後會發現DB自動被創建並且Identity相關的table也自動替我們建立完成











另外補充Identity註冊部分其實目前還不夠嚴謹
比方像是email可以重複註冊
還有Account (UserName)是可以重複同樣名稱的註冊


在Identity中預設可以透過到IdentityConfig.cs中
我們的UserManager的Create方法去做設定

manager.UserValidator = new UserValidator<User>(manager){....}

而預設其實只有提供RequireUniqueEmail跟AllowOnlyAlphanumericUserNames
是不足夠的

IdentityConfig.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using AgricultureManagementSystem.Models;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace AgricultureManagementSystem
{
    public class UserManager : UserManager<User>
    {
        public UserManager(IUserStore<User> store) : base(store)
        {

        }

        public static UserManager Create(IdentityFactoryOptions<UserManager> options, IOwinContext context)
        {
            var manager = new UserManager(new UserStore<User>(context.Get<Models.IdentityDbContext>()));
            //manager.UserValidator = new UserValidator<User>(manager)
            //{
            //    AllowOnlyAlphanumericUserNames = false,
            //    RequireUniqueEmail = true
            //};

            manager.UserValidator = new CustomUserValidator(manager)
            {
                AllowOnlyAlphanumericUserNames = false,
                RequireUniqueEmail = true
            };

            return manager;
        }
    }
}


因此可以藉由客製化的UserValidator技巧
額外定義一個繼承UserValidator<User>
的CustomUserValidator

CustomUserValidator.cs


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using AgricultureManagementSystem.Models;
using Microsoft.AspNet.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;

namespace AgricultureManagementSystem
{
    public class CustomUserValidator : UserValidator<User>
    {
        private UserManager<User> _userManager;

        public CustomUserValidator(UserManager mgr) : base(mgr)
        {
            _userManager = mgr;
        }

        public override async Task<IdentityResult> ValidateAsync(User user)
        {
            IdentityResult result = await base.ValidateAsync(user);
            int cntAccount =_userManager.Users.Select(u => u.Account == user.Account).Count();
            if (cntAccount > 0)
            {
                var errors = result.Errors.ToList();
                //errors.Add("This Account Name already exists");
                result = new IdentityResult(errors);
            }
            return result;
        }

    }
}


透過UserManager屬性Users
Users : Returns an enumeration of the users. See the “Enumerating User Accounts” section.
可以幫我們去做該Account跟UserName是否已經被用過





總之Identity一整個配置下來真的還滿繁瑣的
沒有做個筆記過幾天可能就忘記了>~<|||
也可以不用Identity做身分權限認證不過有機會就順便學習起來
看他人包裝的框架設計








Ref:
.Net MVC 擴充會員欄位 Identity 2.0

Asp.Net Identity 練習 – 增加欄位與角色

Asp.net Identity 2.0 extend UserValidator with custom unique property

Using IUserValidator to provide additional user validation rules in ASP.NET Core Identity

Introduction to ASP.NET Identity


OwinStartupAttribute required in web.config to correct Server Error


[MVC] Server Error "owin:AutomaticAppStartup"


ASP.NET Identity

Overview of Custom Storage Providers for ASP.NET Identity


[ASP.NET Identity] Identity 起手式


將 ASP.NET Identity 加至 ASP.NET MVC Empty 專案中


C# ASP.NET MVC 手動將 Identity 加入現有專案

Identity - 網站會員管理 (一)

【ASP.NET Identity系列教程(一)】ASP.NET Identity入门


【ASP.NET Identity系列教程(二)】运用ASP.NET Identity


【ASP.NET Identity系列教程(三)】Identity高级技术


How can customize Asp.net Identity 2 username already taken validation message?





留言

這個網誌中的熱門文章

何謂淨重(Net Weight)、皮重(Tare Weight)與毛重(Gross Weight)

Architecture(架構) 和 Framework(框架) 有何不同?_軟體設計前的事前規劃的藍圖概念

經得起原始碼資安弱點掃描的程式設計習慣培養(五)_Missing HSTS Header