65.9K
CodeProject 正在变化。 阅读更多。
Home

MVC 中的登录功能

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (27投票s)

2016 年 7 月 15 日

CPOL

6分钟阅读

viewsIcon

142825

downloadIcon

4930

使用表单身份验证在 MVC 中实现自定义登录功能。

引言

让我们首先确定代码的目的是什么。

本文代码的目的是使用表单身份验证在 MVC 中创建登录和注销功能。我们还将讨论使用哈希(Hashing)将密码存储在数据库中的最佳方法

背景

最重要的问题是密码如何受到保护。如果您以纯文本形式存储密码,或使用加密/解密(双向)则是一个糟糕的主意。如果您以加密格式存储密码,则有可能使用加密输出恢复到纯文本值。

这是将密码存储在数据库中的最佳解决方案。我们使用单向哈希算法加密密码。首先,我们使用 SHA512 算法(bcrypt/PBKDF2/scrypt 也是最好的哈希算法)创建密码、一个唯一字段(例如用户名、手机号或电子邮件)和盐(SALT)组合的哈希值,并使用 CSPRNG 创建一个唯一的盐。然后我们将哈希值和盐存储在数据库中。

我们不需要知道密码,只需验证输入的密码。因此,当用户尝试登录时,我们会将用户输入的密码和唯一字段(用户输入的)以及盐组合创建一个哈希值,然后与从数据库检索到的真实密码的哈希值进行比较。如果哈希值匹配,用户将被授予访问权限。否则,将告知用户输入的登录凭据无效。

Using the Code

首先,我们需要创建一个包含用户信息的数据库和数据表。在此开始编写代码。

步骤 01:创建一个名为“DemoLoginFunctionality”的数据库。

Script:

CREATE DATABASE DemoLoginFunctionality;

以下脚本用于创建包含数据条目(Data Entries)的数据表。

USE [DemoLoginFunctionality]
GO
/****** Object:  Table [dbo].[UserMaster]    Script Date: 15-07-2016 11:30:49 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[UserMaster](
    [UserID] [bigint] IDENTITY(1,1) NOT NULL,
    [Username] [nvarchar](50) NOT NULL,
    [HASH] [nvarchar](max) NOT NULL,
    [SALT] [varbinary](512) NOT NULL,
 CONSTRAINT [PK_UserMaster] PRIMARY KEY CLUSTERED
(
    [UserID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO
SET ANSI_PADDING OFF
GO
SET IDENTITY_INSERT [dbo].[UserMaster] ON

GO
INSERT [dbo].[UserMaster] ([UserID], [Username], [HASH], [SALT]) VALUES (1, N'suchit', N'duD96EJIW3AhCKE9vcl8aev1M2yULlLS4dHqqyFISrpvAdjNvNlVjKHC2Xsi4wsDyHRWa/wxeGmNqd37nQdgS+IhX/AsWEpPklL2LlBMFxsXiviznHWOMb/OjcTDWhM6', 0xE2215FF02C584A4F9252F62E504C171B178AF8B39C758E31BFCE8DC4C35A133A)
GO
SET IDENTITY_INSERT [dbo].[UserMaster] OFF
GO

注意:我们将密码的哈希值和盐存储在数据库中。

--- 注册时(生成哈希值和盐) ---

1) 首先,我们需要创建盐。我们使用CSPRNG(加密安全的伪随机数生成器)来创建盐。

什么是盐(SALT)?

盐是随机数据,用作单向函数(对密码进行“哈希”)的附加输入。

什么是 CSPRNG?

加密安全的伪随机数生成器(CSPRNG)是一种伪随机数生成器(PRNG),其特性使其适合在密码学中使用。它使用数学公式生成随机数序列。

#region --> Generate SALT Key
    
private static byte[] Get_SALT()
{
      return Get_SALT(saltLengthLimit);
}

private static byte[] Get_SALT(int maximumSaltLength)
{
     var salt = new byte[maximumSaltLength];

      //Require NameSpace: using System.Security.Cryptography;
      using (var random = new RNGCryptoServiceProvider())
      {
           random.GetNonZeroBytes(salt);
      }

     return salt;
}

#endregion

2) 接下来,第二步是创建哈希值。我们使用SHA512来创建哈希值。

什么是密码哈希(password hashing)?

哈希对密码执行单向转换,将密码转换为另一个字符串,称为哈希密码。“单向”意味着几乎不可能反向操作——将哈希密码还原为原始密码。它们还具有一个特性,即如果输入发生微小变化,生成的哈希值也会完全不同。

i.e.
hashSHA512("hello") = ekFc4y+0WZ/jHte2PNzvSLC/WLKel86uRb71r6JliSrgjEkKeFQkTL2Ltix+1mcybYsJZuaAp7x7dMUi+1sQCmzR/JoMziQU/XKv0YK92oYMywLb9QJYNYiZDITZNlNu

hashSHA512("hollo") = HntRl+7m3KQ6fDNhqGPYDz3tCAan9gtVL3ad/Z6Qm5aQ3bzDUXUBy+mfgmSVmzfQBYV+3K8A9pWwcRFJxpNYwwVKsB4HudvUsCGPE0JiDx+nX9ot/UU7fn5V1jOTos1g

hashSHA512("helle") = UXaSsPL0G0sY/jqccVMrjazoFgpeDihMu5xwSo3AYWqpHuozHee9zfRviHL1SF+6E/0nU/aV/rM+LLjbTUfBpNI4nPKFwCTmRYmvMrT2npHHsBaIO/w8cpIsJ7y5WiNv

什么是 SHA512?

安全哈希算法(SHA512)是由美国国家安全局(NSA)设计的一组加密哈希函数。

//CODE: Generate HASH Using SHA512
public static string Get_HASH_SHA512(string password, string username, byte[] salt)
{
            try
            {
                //required NameSpace: using System.Text;
                //Plain Text in Byte
                byte[] plainTextBytes = Encoding.UTF8.GetBytes(password + username);

                //Plain Text + SALT Key in Byte
                byte[] plainTextWithSaltBytes = new byte[plainTextBytes.Length + salt.Length];

                for (int i = 0; i < plainTextBytes.Length; i++)
                {
                    plainTextWithSaltBytes[i] = plainTextBytes[i];
                }

                for (int i = 0; i < salt.Length; i++)
                {
                    plainTextWithSaltBytes[plainTextBytes.Length + i] = salt[i];
                }

                HashAlgorithm hash = new SHA512Managed();
                byte[] hashBytes = hash.ComputeHash(plainTextWithSaltBytes);
                byte[] hashWithSaltBytes = new byte[hashBytes.Length + salt.Length];

                for (int i = 0; i < hashBytes.Length; i++)
                {
                    hashWithSaltBytes[i] = hashBytes[i];
                }

                for (int i = 0; i < salt.Length; i++)
                {
                    hashWithSaltBytes[hashBytes.Length + i] = salt[i];
                }

                return Convert.ToBase64String(hashWithSaltBytes);
            }
            catch
            {
                return string.Empty;
            }
}

现在,将哈希值和盐存储在数据库中。

步骤 02:创建新的 MVC 应用程序项目。

1) 在“文件”菜单上,单击“新建项目”

2) 在“新建项目”对话框中,在“项目类型”下展开“Visual C#”,然后单击“Web”,并在“名称”框中键入“LoginFunctionalityMVC”,然后单击“确定”。

3) 现在,在对话框中,在 ASP.NET 4.5.2 模板下单击“MVC”,然后单击位于右侧中间的“更改身份验证”

步骤 03:现在,新的 MVC 应用程序已创建。接下来,我们需要创建一个 EDMX 并将我们的数据库“DemoLogin”与 EDMX 绑定。

1) 在右侧,您可以找到“解决方案资源管理器”

2) 在“解决方案资源管理器”中,右键单击“Models”文件夹。然后单击“添加”,然后单击“新建项...”

3) 现在单击 Visual C# 并选择ADO.NET Entity Data Model,将其命名为“DBModel”,然后单击“确定”。

4) 选择“EF Designer from Database”,然后单击“下一步”。

5) 现在单击“新建连接”,然后定义“服务器名称”,选择身份验证模式,是 Windows 还是 SQL Server,如果是 SQL Server,则输入用户名或密码。最后,在“连接到数据库”下选择数据库“DemoLoginFunctionality”,然后单击“确定”。

6) 现在,在“将连接设置保存在 Web.config 中,名称为:”下将连接字符串命名为“DBEntities”,然后单击“下一步”。

7) 选择实体框架的版本。选择“Entity Framework 6.x”,然后单击“下一步”。


8) 展开“Table”,然后展开“dbo”,然后选择我们的数据表“UserMaster”。现在,在“模型命名空间”下将命名空间设置为“Models”,然后单击“确定”。

9) 现在,通过按CLTR + B构建您的项目,以完美更新所有实体。

步骤 04:添加一个空的控制器。

1) 要添加控制器,请右键单击“controller 文件夹”,选择“添加”,然后单击“控制器”

2) 现在,在“添加脚手架对话框”中选择“MVC 5 Controller - Empty”,然后单击“添加”并将其命名为“HomeController”,然后单击“添加”。

创建一个名为“Login”的新 ActionResult 方法。

        [HttpGet]
        public ActionResult Login(string returnURL)
        {
            var userinfo = new LoginVM();

            try
            {
                // We do not want to use any existing identity information
                EnsureLoggedOut();

                // Store the originating URL so we can attach it to a form field
                userinfo.ReturnURL = returnURL;

                return View(userinfo);
            }
            catch
            {
                throw;
            }
        }
//Create A New ViewModel Name AS "LoginVM" into Models Folders & Declare Below Properties.

public class LoginVM
    {
        public string Username { get; set; }

        public string Password { get; set; }

        public string ReturnURL { get; set; }

        public bool isRemember { get; set; }

    }

步骤 05:添加一个视图。

1) 为此,请右键单击您的“Login”操作方法,然后选择“添加视图”

2) 现在,取消选中“使用布局页”,然后单击“添加”按钮。

3) 在视图顶部添加模型类。

@model LoginFunctionalityMVC.Models.LoginVM

4) 在 Index.cshtml(视图)的 form 标签中,添加一个电子邮件、密码文本框、复选框(用于“记住我”选项)和提交按钮。

//Paste below code in your view
@model LoginFunctionalityMVC.Models.LoginVM

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Login</title>
</head>
<body>
    @using (Html.BeginForm("Login", "Home", FormMethod.Post))
    {
        @Html.AntiForgeryToken()
        @Html.HiddenFor(s => s.ReturnURL)

        <h1>Login Functionality In MVC</h1>
        <div>

            @if (TempData["ErrorMSG"] != null)
            {
                <label style="color:maroon;"> @TempData["ErrorMSG"] </label>
                <br /><br />
            }

            @Html.TextBoxFor(s => s.Username, new { @placeholder = "Username" })
            <br /> <br />
            @Html.PasswordFor(s => s.Password, new { @placeholder = "Password" })
            <br /> <br />
            @Html.CheckBoxFor(s => s.isRemember) Remember ME
            <br /> <br />
            <button type="submit">Login</button>
        </div>
    }
</body>
</html>

注意:别忘了在视图中声明 AntiForgeryToken。

AntiForgeryToken 防伪令牌可用于帮助保护您的应用程序免受跨站请求伪造(CSRF)的侵害。

步骤 06:当登录页面初始化时,此时我们需要检查当前会话是否已注销。因此,我们首先注销现有用户。

为此,我们创建两个方法:“EnsureLoggedOut”和“Logout”。

        //GET: EnsureLoggedOut
        private void EnsureLoggedOut()
        {
            // If the request is (still) marked as authenticated we send the user to the logout action
            if (Request.IsAuthenticated)
                Logout();
        }

        //POST: Logout
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Logout()
        {
            try
            {
                // First we clean the authentication ticket like always
                //required NameSpace: using System.Web.Security;
                FormsAuthentication.SignOut();

                // Second we clear the principal to ensure the user does not retain any authentication
                //required NameSpace: using System.Security.Principal;
                HttpContext.User = new GenericPrincipal(new GenericIdentity(string.Empty), null);

                Session.Clear();
                System.Web.HttpContext.Current.Session.RemoveAll();

                // Last we redirect to a controller/action that requires authentication to ensure a redirect takes place
                // this clears the Request.IsAuthenticated flag since this triggers a new request
                return RedirectToLocal();
            }
            catch
            {
                throw;
            }
        }

注意:别忘了在 POST 方法的顶部声明 ValidateAntiForgeryToken。

ValidateAntiForgeryToken:此功能并不能阻止任何其他类型的数据伪造或篡改相关的攻击。要使用它,请使用 ValidateAntiForgeryToken 属性修饰操作方法或控制器,并在提交到该方法的表单中调用 @Html.AntiForgeryToken()

步骤 07:我们创建了一个名为“SignInRemember”的方法来设置 Cookie 中的身份验证(记住我选项),以及一个名为“RedirectToLocal”的方法来重定向到页面。

        //GET: SignInAsync   
        private void SignInRemember(string userName, bool isPersistent = false)
        {
            // Clear any lingering authencation data
            FormsAuthentication.SignOut();

            // Write the authentication cookie
            FormsAuthentication.SetAuthCookie(userName, isPersistent);
        }
        //GET: RedirectToLocal
        private ActionResult RedirectToLocal(string returnURL = "")
        {
            try
            {
                // If the return url starts with a slash "/" we assume it belongs to our site
                // so we will redirect to this "action"
                if (!string.IsNullOrWhiteSpace(returnURL) && Url.IsLocalUrl(returnURL))
                    return Redirect(returnURL);

                // If we cannot verify if the url is local to our host we redirect to a default location
                return RedirectToAction("Index", "Dashboard");
            }
            catch
            {
                throw;
            }
        }

步骤 08:现在,所有必需的方法都已完成。最后,我们继续创建一个方法来验证用户名和密码。

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Login(LoginVM entity)
        {
            string OldHASHValue = string.Empty;
            byte[] SALT = new byte[saltLengthLimit];

            try
            {
                using (db = new DBEntities())
                {
                    // Ensure we have a valid viewModel to work with
                    if (!ModelState.IsValid)
                        return View(entity);

                    //Retrive Stored HASH Value From Database According To Username (one unique field)
                    var userInfo = db.UserMasters.Where(s => s.Username == entity.Username.Trim()).FirstOrDefault();

                    //Assign HASH Value
                    if (userInfo != null)
                    {
                        OldHASHValue = userInfo.HASH;
                        SALT = userInfo.SALT;
                    }

                    bool isLogin = CompareHashValue(entity.Password, entity.Username, OldHASHValue, SALT);

                    if (isLogin)
                    {
                        //Login Success
                        //For Set Authentication in Cookie (Remeber ME Option)
                        SignInRemember(entity.Username, entity.isRemember);

                        //Set A Unique ID in session
                        Session["UserID"] = userInfo.UserID;

                        // If we got this far, something failed, redisplay form
                        // return RedirectToAction("Index", "Dashboard");
                        return RedirectToLocal(entity.ReturnURL);
                    }
                    else
                    {
                        //Login Fail
                        TempData["ErrorMSG"] = "Access Denied! Wrong Credential";
                        return View(entity);
                    }
                }
            }
            catch
            {
                throw;
            }

        }
        public static bool CompareHashValue(string password, string username, string OldHASHValue, byte[] SALT)
        {
            try
            {
                string expectedHashString = Get_HASH_SHA512(password, username, SALT);

                return (OldHASHValue == expectedHashString);
            }
            catch
            {
                return false;
            }
        }

步骤 09:现在,最后阶段是创建一个自定义的 Authorize Attribute(授权属性)在 MVC 中。因为它会在任何页面初始化之前检查用户是否已登录。

在“Models 文件夹”中创建一个名为CheckAuthorization.cs”的类。

 public class CheckAuthorization : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            if (HttpContext.Current.Session["UserID"] == null || !HttpContext.Current.Request.IsAuthenticated)
            {
                if (filterContext.HttpContext.Request.IsAjaxRequest())
                {
                    filterContext.HttpContext.Response.StatusCode = 302; //Found Redirection to another page. Here- login page. Check Layout ajaxError() script.
                    filterContext.HttpContext.Response.End();
                }
                else
                {
                    filterContext.Result = new RedirectResult(System.Web.Security.FormsAuthentication.LoginUrl + "?ReturnUrl=" +
                         filterContext.HttpContext.Server.UrlEncode(filterContext.HttpContext.Request.RawUrl));
                }
            }
            else
            {

                //Code HERE for page level authorization

            }
        }
    }

步骤 10:现在,我只需要将 [CheckAuthorization] 属性放在我的控制器顶部,即可访问我的 CheckAuthorization 函数,该函数是自定义的授权属性。

步骤 11:将身份验证模式添加到您的 Web.config。

//Add Below Code To Your Web.confing

<system.web >  
    <authentication mode = "Forms" >  
    <forms loginUrl = "~/Home/Login" timeout = "2880" / > //Declare Your Return URL Here. Mean If Login Fail Then Page Redirect This URL  
    </authentication>
</system.web>
© . All rights reserved.