ASP.NET MVC第014天_ASP.NET Identity使用筆記_使用者登入_Claims-based identity理解

 


註冊完後的帳戶之後再訪問只要登入帳號密碼即可瀏覽存取會員專享的一些資源


Claim 動詞則有宣稱,聲稱, 主張, 自稱, 指控, 認領,索賠等意思
名詞則有權利 , 要求權和聲明等

Claims-based 則是一個專有詞,代表基於聲明的認證(Claims-based identity)
用於Google,Facebook,Microsoft等都有此機制的概念導入

A claim is a statement that one subject, such as a person or organization, makes about itself or another subject. For example, the statement can be about a name, group, buying preference, ethnicity, privilege, association or capability. The subject making the claim or claims is the provider. Claims are packaged into one or more tokens that are then issued by an issuer (provider), commonly known as a security token service (STS).



You can think of claims as being a statement about, or a property of, a particular identity. That statement consists of a name and a value. For example you could have a DateOfBirth claim, FirstName claim, EmailAddress claim or IsVIP claim. Note that these statements are about what or who the identity is, not what they can do.

The identity itself represents a single declaration that may have many claims associated with it. For example, consider a driving license. This is a single identity which contains a number of claims - FirstName, LastName, DateOfBirth, Address and which vehicles you are allowed to drive. Your passport would be a different identity with a different set of claims.


可以把Claim理解成一張身分證或者駕照裡面還有包含其他組成元素
像是居住戶籍通訊地址、出生年月日、國籍之類的....這些都是其中一個claim
就程式面會有點類似DTO Key-value pair的資料結構


Step1.至User.cs中擴充一個Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<User> manager)方法


 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
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;

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

        public virtual bool IsFirstTimeRequest { get; set; }

        public User()
        {
            this.IsFirstTimeRequest = true;
        }

        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<User> manager)
        {
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            return userIdentity;
        }

    }
}


Step2.至 IdentityConfig.cs 中增加一個繼承自SignInManager的Sub Class





 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
using AgricultureManagementSystem.Models;
using AgricultureManagementSystem.Public;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
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
            };

            manager.EmailService = new EmailService();
            var dataProtectionProvider = options.DataProtectionProvider;
            if (dataProtectionProvider != null)
            {
                manager.UserTokenProvider = new DataProtectorTokenProvider<User>(dataProtectionProvider.Create("EmailConfirm"));
            }

            return manager;
        }
    }

    public class EmailService : IIdentityMessageService
    {

        //https://developers.google.com/gmail/imap/imap-smtp
        public Task SendAsync(IdentityMessage message)
        {
            //smtp-mail.outlook.com 
            Common.SendMail(ConfigurationManager.AppSettings["SMTP_SERVER"], int.Parse(ConfigurationManager.AppSettings["SMTP_PORT"]), ConfigurationManager.AppSettings["MailPwd"], ConfigurationManager.AppSettings["MailFrom"], message.Destination, message.Subject, message.Body);
            //Common.SendMail("smtp.gmail.com", 587, ConfigurationManager.AppSettings["MailPwd"], ConfigurationManager.AppSettings["MailFrom"], message.Destination, message.Subject, message.Body);
            return Task.FromResult(0);
        }
    }



    public class SignInManager : SignInManager<User, string>
    {
        public SignInManager(UserManager userManager, IAuthenticationManager authenticationManager)
            : base(userManager, authenticationManager)
        {

        }

        public override Task<ClaimsIdentity> CreateUserIdentityAsync(User user)
        {
            return user.GenerateUserIdentityAsync((UserManager)UserManager);
        }

        public static SignInManager Create(IdentityFactoryOptions<SignInManager> options, IOwinContext context)
        {
            return new SignInManager(context.GetUserManager<UserManager>(), context.Authentication);
        }

    }

}


Step3.至Startup.Auth.cs註冊SignInManager


 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
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.CreatePerOwinContext<SignInManager>(SignInManager.Create);
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new Microsoft.Owin.PathString("/User/Login")
            });
        }
    }
}



Step4.新增LoginViewModel (在ViewModels目錄下)


 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 System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;

namespace AgricultureManagementSystem.ViewModels
{
    public class LoginViewModel
    {

        [Required]
        [Display(Name ="電子信箱")]
        [EmailAddress]
        public string Email { get; set; }
        
        
        [Required]
        [Display(Name = "密碼")]
        [DataType(DataType.Password)]
        public string Password { get; set; }

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




Step5.至LoginController.cs中獲取SignInManager
並實作分別是取得登入畫面GET 以及 Post提交資料的Index Action


 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
// GET: Login
[HttpGet]
[AllowAnonymous]
public ActionResult Index(string retUrl)
{
    ViewBag.ReturnUrl = retUrl;
    return View();
}

//await SignInManager.PasswordSignInAsync() always results in a failure in my ASP.NET MVC project
//https://stackoverflow.com/questions/31734994/await-signinmanager-passwordsigninasync-always-results-in-a-failure-in-my-asp

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


    //只適用UserName等於Email情況
    //var result = await SignInManager.PasswordSignInAsync(loginViewModel.Email, 
    //    loginViewModel.Password, loginViewModel.RememberMe, shouldLockout: false);

    //username != email
    User signedUser = await UserManager.FindByEmailAsync(loginViewModel.Email);

    var result = await SignInManager.PasswordSignInAsync(signedUser.UserName,
        loginViewModel.Password, loginViewModel.RememberMe, shouldLockout: false);


    switch (result)
    {
        case SignInStatus.Success:
            return RedirectToLocal(retUrl);
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", new { ReturnUrl = retUrl, RememberMe = loginViewModel.RememberMe });
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "無效的登入");
            return View(loginViewModel);
    }
}


這邊需留意如果預設username不是email的時候在使用密碼驗證方法之前多留意
各參數定義避免怎麼驗證都失敗的問題





對應Login Index檢視

 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
@{
    Layout = null;
}

@model AgricultureManagementSystem.ViewModels.LoginViewModel

<!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">

        <!-- Outer Row -->
        <div class="row justify-content-center">

            <div class="col-xl-10 col-lg-12 col-md-9">

                <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-6 d-none d-lg-block bg-login-image"></div>
                            <div class="col-lg-6">
                                <div class="p-5">
                                    <div class="text-center">
                                        <h1 class="h4 text-gray-900 mb-4">稻穀收購系統</h1>
                                    </div>
                                    @using (Html.BeginForm("Index", "Login", new { retUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "user", role = "form" }))
                                    {
                                        @Html.AntiForgeryToken()
                                        <hr>
                                        @Html.ValidationSummary(true,"", new { @class = "text-danger" })
                                        <div class="form-group">
                                            @Html.TextBoxFor(m => m.Email, new { @class = "form-control form-control-user", placeholder = "請輸入帳號或信箱" })
                                        </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">
                                            <div class="custom-control custom-checkbox small">
                                                @Html.CheckBoxFor(m => m.RememberMe)
                                                @Html.LabelFor(m => m.RememberMe)
                                            </div>
                                        </div>
                                        <input type="submit" class="btn btn-primary btn-user btn-block" value="Login" />
                                        <hr>
                                        <div class="text-center">
                                            <a class="small" href="/Password/Index">Forgot Password?</a>
                                        </div>
                                        <div class="text-center">
                                            <a class="small" href="/Register/Add">Create an Account!</a>
                                        </div>
                                    }
                                </div>
                            </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>



Email是否有驗證過才給登入這種邏輯要額外自己添加










Ref:
MVC Prevent login when EmailConfirmed is false

how to check emailconfirmed in login controller.

PasswordSignInAsync succeeded always false

Identity - 網站會員管理 (四)

MVC5 - ASP.NET Identity登录原理 - Claims-based认证和OWIN


Introduction to Authentication with ASP.NET Core

Claims-Based Authentication Unleashed


ASP.NET Core 之 Identity 入门(一)


學英文 _ 惱人的一字多義: claim 到底怎麼用?


留言

這個網誌中的熱門文章

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

經得起原始碼資安弱點掃描的程式設計習慣培養(三)_7.Cross Site Scripting(XSS)_Stored XSS_Reflected XSS All Clients

(2021年度)駕訓學科筆試準備題庫歸納分析_法規是非題