2013年11月20日 星期三

ASP.NET MVC 實做具有多個角色權限的登入功能

這一篇老實說拖了蠻久的,因為是延續上個月所寫的 Code First 內容,

ASP.NET MVC 使用 Entity Framework Code First - 基礎入門

ASP.NET MVC 使用 Entity Framework Code First - 變更多對多關聯資料

在這兩篇文章裡,我們使用 Code First 建立了 SystemUser 以及 SystemRole 兩個類別以及兩者的多對多關係,其實已經建立好基礎的類別與資料存取方法,看來只差登入頁面、登出入程式處理就可以完成一個系統最基本的登入功能。

這一篇就來說明如何做一個基本且符合多個角色權限的登入功能。

 


這邊我先準備好一個登入頁的 Layout,

image

登入功能是在 HomeController 裡,另外登入頁面所使用的 Model 並不是直接使用 SystemUser,而是會另外建立一個 LogonViewModel,這是根據頁面上的輸入欄位而建立的,

image

登入頁面為獨立頁面,所以不需要使用 _Layout.cshtml,另外勾選「建立強型別檢視」並使用 LogonViewModel,

SNAGHTML4d88695

完整的 Logon.cshtml 內容如下:

@model BlogSample.ViewModels.LogonViewModel
@{
    ViewBag.Title = "Logon";
    Layout = null;
}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Logon</title>
    <link href="~/Content/bootstrap.min.css" rel="stylesheet" />
    <link href="~/Content/bootstrap-responsive.min.css" rel="stylesheet" />
    <style type="text/css">
        body {
            padding-top: 100px;
            padding-bottom: 50px;
        }
    </style>
</head>
<body>
 
    <div class="container">
        <div class="row">
            <div class="span4 offset4 well">
                <legend>登入</legend>
 
                @if (!ViewData.ModelState.IsValid)
                {
                    <div class="alert alert-error">
                        <a class="close" data-dismiss="alert" href="#">×</a>
                        @Html.ValidationSummary()
                    </div>
                }
 
                @using (Html.BeginForm("Logon", "Home", FormMethod.Post))
                {
                    @Html.AntiForgeryToken()
 
                    @Html.TextBoxFor(x => x.Account, new { @class = "span4", placeholder = "帳號", AutoComplete = "Off", tabindex = "1" })
                    @Html.PasswordFor(x => x.Password, new { @class = "span4", placeholder = "密碼", AutoComplete = "Off", tabindex = "2" })
 
                    <label>
                        @Html.CheckBoxFor(x => x.Remember)
                        @Html.DisplayNameFor(x => x.Remember)
                    </label>
 
                    <div style="padding-top: 10px;">
                        <button type="submit" name="submit" class="btn btn-primary">登入</button>
                        <button type="button" name="reset" class="btn">清除</button>
                    </div>
                }
            </div>
        </div>
    </div>
 
    <script src="~/Scripts/jquery-2.0.2.min.js"></script>
    <script src="~/Scripts/bootstrap.min.js"></script>
    <script type="text/javascript">
        $(function () {
            $('#Account').focus();
            $('button[name=reset]').click(function () {
                $('#Account').val('');
                $('#Password').val('');
                $('#Remember').prop('checked', '');
                if ($('.alert>a.close').length > 0) {
                    $('.alert>a.close').trigger('click');
                }
            });
        });
    </script>
</body>
</html>

完成 Logon.cshtml 頁面的內容之後,接下來就是建立頁面送出登入資料的 POST 處理,

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Logon(LogonViewModel logonModel)
{
    if (!ModelState.IsValid)
    {
        return View();
    }
 
    var systemuser = db.SystemUsers
        .Include(x => x.SystemRoles)
        .FirstOrDefault(x => x.Account == logonModel.Account);
 
    if (systemuser == null)
    {
        ModelState.AddModelError("", "請輸入正確的帳號或密碼!");
        return View();
    }
 
    var password = CryptographyPassword(logonModel.Password, BaseController.PasswordSalt);
 
    if (systemuser.Password.Equals(password))
    {
        this.LoginProcess(systemuser, logonModel.Remember);
        return RedirectToAction("Index", "Home");
    }
    else
    {
        ModelState.AddModelError("", "請輸入正確的帳號或密碼!");
        return View();
    }
}

上面的處理中有個 CryptographyPassword 的 Method,內容如下:

將使用者在登入頁面所輸入的密碼加上加密所使用的鹽,取得加密後的結果後再與資料庫所儲存的加密後密碼比對

protected string CryptographyPassword(string password, string salt)
{
    string cryptographyPassword =
        FormsAuthentication.HashPasswordForStoringInConfigFile(password + salt, "sha1");
 
    return cryptographyPassword;
}

LoginProcess Method

因為一個使用者可以有多個角色,所以將角色資料取出後轉換為逗號分隔的字串,然後再將角色資料置入 FormAuthenticationTicket 的 userData 裡,而使用者的辨識資料則是將 User 的 ID 放在 FormAuthenticationTicket 的 name 裡,最後將 FormAuthenticationTicket 加密後放在 Cookie 中。

private void LoginProcess(SystemUser user, bool isRemeber)
{
    var now = DateTime.Now;
    string roles = string.Join(",", user.SystemRoles.Select(x => x.Name).ToArray());
 
    var ticket = new FormsAuthenticationTicket(
        version: 1,
        name: user.ID.ToString(),
        issueDate: now,
        expiration: now.AddMinutes(30),
        isPersistent: isRemeber,
        userData: roles,
        cookiePath: FormsAuthentication.FormsCookiePath);
 
    var encryptedTicket = FormsAuthentication.Encrypt(ticket);
    var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
    Response.Cookies.Add(cookie);
}

如果登入頁有輸入錯誤或是輸入的資料不正確,就會顯示在 Bootstrap Alert 的區域裡,

image

image

 

最後再對 HomeController 做個修改,加上 Authorize Filter,但是在 Logon 方法則是要加上 AllowAnonymous Attribute,然後在 HomeController 裡加入 Logout 登出的處理,完整的 HomeController 內容如下:

using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using BlogSample.Models;
using BlogSample.ViewModels;
 
namespace BlogSample.Controllers
{
    [Authorize]
    public class HomeController : BaseController
    {
        public ActionResult Index()
        {
            return View();
        }
 
        public ActionResult Examples()
        {
            return View();
        }
 
        //=========================================================================================
 
        [AllowAnonymous]
        public ActionResult Logon()
        {
            return View();
        }
 
        [HttpPost]
        [ValidateAntiForgeryToken]
        [AllowAnonymous]
        public ActionResult Logon(LogonViewModel logonModel)
        {
            if (!ModelState.IsValid)
            {
                return View();
            }
 
            var systemuser = db.SystemUsers
                .Include(x => x.SystemRoles)
                .FirstOrDefault(x => x.Account == logonModel.Account);
 
            if (systemuser == null)
            {
                ModelState.AddModelError("", "請輸入正確的帳號或密碼!");
                return View();
            }
 
            var password = CryptographyPassword(logonModel.Password, BaseController.PasswordSalt);
 
            if (systemuser.Password.Equals(password))
            {
                this.LoginProcess(systemuser, logonModel.Remember);
                return RedirectToAction("Index", "Home");
            }
            else
            {
                ModelState.AddModelError("", "請輸入正確的帳號或密碼!");
                return View();
            }
        }
 
        private void LoginProcess(SystemUser user, bool isRemeber)
        {
            var now = DateTime.Now;
            string roles = string.Join(",", user.SystemRoles.Select(x => x.Name).ToArray());
 
            var ticket = new FormsAuthenticationTicket(
                version: 1,
                name: user.ID.ToString(),
                issueDate: now,
                expiration: now.AddMinutes(30),
                isPersistent: isRemeber,
                userData: roles,
                cookiePath: FormsAuthentication.FormsCookiePath);
 
            var encryptedTicket = FormsAuthentication.Encrypt(ticket);
            var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
            Response.Cookies.Add(cookie);
        }
 
        public ActionResult Logout()
        {
            FormsAuthentication.SignOut();
 
            //清除所有的 session
            Session.RemoveAll();
 
            //建立一個同名的 Cookie 來覆蓋原本的 Cookie
            HttpCookie cookie1 = new HttpCookie(FormsAuthentication.FormsCookieName, "");
            cookie1.Expires = DateTime.Now.AddYears(-1);
            Response.Cookies.Add(cookie1);
 
            //建立 ASP.NET 的 Session Cookie 同樣是為了覆蓋
            HttpCookie cookie2 = new HttpCookie("ASP.NET_SessionId", "");
            cookie2.Expires = DateTime.Now.AddYears(-1);
            Response.Cookies.Add(cookie2);
 
            return RedirectToAction("Index", "Home");
        }
 
    }
}

而既有的 SystemUserController 與 SystemRoleController 本來就應該是登入之後才能夠使用的功能,所以直接在這兩個 Controller 類別加上 Authorize attribute,

image

image

 

再來就是當未登入的使用者進入到系統時,要將使用者導向預設的登入頁,這裡我們在 Web.Config 的 System.Web 區段裡加入 Authentication 的設定,

image

 

接著要處理的是當我們透過 Home/Logon 登入到系統之後,要如何讓系統取得使用者登入時存放在 FormAuthenticationTicket 的使用者資訊呢?這時候我們可以在 Global.asax 的 Application_AuthenticateRequest 方法裡做處理,

image

 

最後要處理的是使用者登入後可以在頁面上顯示使用者名稱以及登出的連結,這裡要修改的地方是在 _Layout.cshtml,

修改前

接著要修改的地方就是圖中箭頭所指的地方,

image

修改後

這邊會使用 WebSiteHelper 的方法來顯示已登入的使用者名稱

image

<div class="navbar navbar-fixed-top">
    <div class="navbar-inner">
        <div class="container">
            <a class="brand" href="@Url.Action("Index", "Home")">MVC4 with EF Code-First</a>
            <ul class="nav">
                @Html.MenuLink("Home", "Index", "Home")
                @Html.MenuLink("SystemRole", "Index", "SystemRole")
                @Html.MenuLink("SystemUser", "Index", "SystemUser")
            </ul>
            @if (!string.IsNullOrWhiteSpace(WebSiteHelper.CurrentUserName))
            {
                <ul class="nav pull-right">
                    <li class="dropdown">
                        <a href="#" class="dropdown-toggle" data-toggle="dropdown">@WebSiteHelper.CurrentUserName <b class="caret"></b></a>
                        <ul class="dropdown-menu">
                            <li>
                                <a href="@Url.Action("Logout", "Home")"><i class="icon-off"></i> 登出</a>
                            </li>
                        </ul>
                    </li>
                </ul>
            }
        </div>
    </div>
</div>

WebSiteHelper.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using BlogSample.Models;
 
namespace BlogSample.Helpers
{
    public class WebSiteHelper
    {
        public static string CurrentUserName
        {
            get
            {
                var httpContext = HttpContext.Current;
                var identity = httpContext.User.Identity as FormsIdentity;
 
                if (identity == null)
                {
                    return string.Empty;
                }
                else
                {
                    var userID = identity.Name;
                    return SystemUserName(userID);
                }
            }
        }
 
        /// <summary>
        /// Systems the name of the user.
        /// </summary>
        /// <param name="id">The identifier.</param>
        /// <returns></returns>
        public static string SystemUserName(Object id)
        {
            string userName = string.Empty;
 
            Guid systemUserID;
 
            if (!Guid.TryParse(id.ToString(), out systemUserID))
            {
                return userName;
            }
            if (systemUserID.Equals(Guid.Empty))
            {
                userName = "系統預設";
            }
            else
            {
                using (SampleContext db = new SampleContext())
                {
                    var user = db.SystemUsers.FirstOrDefault(x => x.ID == systemUserID);
                    userName = (user == null) ? string.Empty : user.Name;
                }
            }
            return userName;
        }
 
    }
}

執行結果:

image

 


延伸閱讀:

MSDN - FormsAuthentication 類別 (System.Web.Security)

ASP.NET MVC Form 驗證 | demo小鋪

ASP.NET MVC 實做登入機制 | demo小鋪

The Will Will Web | 概略解釋 Forms Authentication (表單驗證) 的運作

The Will Will Web | ASP.NET 自訂角色的方式(不用實做 Role Provider)

 

以上

5 則留言:

  1. 作者已經移除這則留言。

    回覆刪除
  2. 作者已經移除這則留言。

    回覆刪除
  3. 不好意思 這個範例程式可供下載嗎 謝謝

    回覆刪除
    回覆
    1. Hello 你好
      如果在這裡沒有看到的話,就表示沒有範例專案可以提供囉 https://kevintsengtw.blogspot.com/p/github.html
      畢竟這是將近六年前的文章了,程式大部分都已經在文章裡了

      刪除
  4. 請問一下mrkt我測試SystemUsers和SystemRoles的Create功能,都無系統預設值且無法新增資料,在必填欄位中均會出現[值 'XXX' 不是 建立者 的有效值。],請問預設資料類型為uniqueidentifier該如何能正常新增資料,還是有同文章介紹範例資料可以供測試使用

    回覆刪除

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。