2012年5月28日 星期一

MiniProfiler 安全性釋疑


經常來這個部落格的人應該會發現到我一直推薦大家使用 MiniProfiler,大家的反應也都是很好,但卻一直保持觀望,為什麼會這樣呢?其實了解一些朋友的疑慮之後就讓我不禁搖頭,我必須說,好用的工具介紹給大家,各位也必須要自己抓下來安裝之後來做個了解,而不能只是看看文章,覺得東西不錯但有些疑慮擺在心理然後就把這好用的工具給拋在腦後,這樣是不對的啦!

我這邊聽到最多的都是 MiniProfiler 的安全性以及上線後的 Profiling Popup 的顯示……

如果是真的有實際玩過 MiniProfiler 的人就不會有這樣的疑問,玩過的人反而會問的是如何設置權限……

所以就藉著這篇文章來試圖解除還沒有玩過 MiniProfiler 的人存在心中以久的疑慮。

 


很多人跟我說:「看你介紹 MiniProfiler 的文章,覺得這東西真的是不錯,但是我專案要上線,總不能要上線前又要把 MiniProfiler 的設定移除後才能上線,要是不移除 MiniProfiler 的設定,專案上線後,那個 Profiling Popup 不就每個使用者都能看得到嗎?這東西好歸好就覺得這樣的設計很麻煩!」

XD ! 會這樣說的人就知道九成九根本沒有把 MiniProfiler 給導入專案中使用,更不用說建一個測試專案來玩玩了。

這麼說吧!

MiniProfiler 預設的情境中,只會對 Local 端的連線顯示 Profiler Popup,所以專案上線後,任何遠端連線都不會顯示 Profiling Popup,而且因為預設非 Local 端連線不會顯示 Profiling Popup 的狀況下,專案上線前也無須將 MiniProfiler 的設定或是程式中的設定給移除,因為預設非 Local 端連線不會起始 MiniProfiler 的執行,所以專案上線後不會因為有加上 MinProfiler 而減損系統執行效能。

 

各位看下面這張圖的程式

image

 

MVC 網站專案使用 NuGet 加入專案後就預設會在系統中加入有關安全性的程式片段,而 WebForms 網站專案則是我們必須手動去加入,讓每個 Request 都必須是 Local 端 才會起始 MiniProfiler 的功能。

以下就來測試一個新建立的 MVC 網站專案,在只有加入 MiniProfiler 後未作任何修改設定的情況下,遠端瀏覽網頁會有什麼樣的狀況。

 


遠端電腦的連線

建立一個 ASP.NET MVC 3 的網站專案,尚未做任何的功能,也還沒有加入資料庫類別,只有加入 MiniProfiler 而已,以下是已經加入 MiniProfiler 後的 Global.asax 內容,

image

 

唯一有修改的就是 _Layout.cshtml 的內容,分別加上「@using StackExchange.Profiling;」「@MiniProfiler.RenderIncludes()」

image

Local 端的測試,自動就會顯示 Profiling Popup

image

 

那如果是遠端電腦來瀏覽會不會顯示 Profiling Popup 呢?

以 TeamViewer 遠端登入別台主機,然後在這電腦上連回來測試,遠端電腦開啟網頁是不會出現 Profiling Popup 。

SNAGHTML130aa87

網站專案加入 MiniProfiler 之後,雖然沒有做任何安全性的修改,也不會讓網站的 Profiler 資料在非本地端的連線上顯示,所以不用擔心加入 MiniProfiler 之後會把重要資料給外洩出去,而且專案要上線也不用大份周章的煩惱要把 MiniProfiler 給移除。

 


自行設定顯示與否

在前面就有列出一段在 Global.asax 中判別是否為 Local 的程式片段「if ( Request.IsLocal ) …… 」

image

這是可以讓我們自己來決定專案中的 MiniProfiler 的 Profiling Popup 是否需要顯示,因為我們可以自己來做操控的,所以我們接下來就來試試看如何讓遠端的網頁上顯示 Profiling Popup。

 

接下來先以 ASP.NET WebForms 來做講解

首先,我們可以在 Web.Config 中增加一個 AppSetting「enable_prifiling」,然後依據這個設定來決定 Profiling Popup 是否需要顯示,

image

修改 Global.asax 中的設定

前面增加一個型別為 bool 的欄位與屬性,預設為 false,用來取得並記錄在 Web.Config 的「enable_profiling」設定

image

修改,Global.asax - Application_Start

image

修改,Global.asax - Application_BeginRequest 與 Application_EndRequest

image

AppSetting「enable_prifiling」設定為 true,所以不論在 Local 端 或是遠端電腦都可以看到 Profiling Popup,

Local 端

image

遠端

image

 

再來看看 ASP.NET MVC 網站專案的設定,MVC 網站專案的設定與 WebForms 網站專案的設定其實大致相同,

Web.Config 的 AppSettings 設定

image

型別為 bool 的 enableProfiling 欄位與屬性

image

Global.asax - Application_Start

image

Global.asax – Application_BeginRequest, Application_EndRequest

image

但如果你以為這樣的設定就讓遠端可以顯示 Profiling Popup 嗎?

答案是:「不行!」

我原本也以為與 WebForms 網站專案一樣的設定,後來我才發現到,MVC 網站專案還要修改另一個地方,一開始使用 NuGet 加入 MiniProfiler 之後,會在網站專案中加入一個目錄「App_Start」,這個目錄下的 MiniProfiler.cs 中有地方需要我們來做修改,

image

在下圖中用紅線框起來的地方,一開始的初始狀態並非是註解起來的,因為安全性的問題,所以一開始 MiniProfiler 會以 IHttpModile 的方式,讓 BeginRequest 增加是否為本地端的判別,而我們可以選擇在 Global.asax 中來做這兩個部分的處理,所以就可以把 MiniProfilerStartupModule 中的兩個地方給註解掉,

image

以上的設定都完成之後,就可以來看看 Local 端與遠端所呈現的頁面,

Local 端

image

遠端

image

 


依據登入者角色來決定 Profiling Popup 是否顯示

有時候我們也希望遠端也能夠有 Profiling Popup 的顯示,但又不希望每個人都可以看得到,這時候我們就可以結合登入的功能,依據登入者的權限來決定是否要顯示 Profiling Popup,接下來就不說 WebForms 專案要怎麼做的,而會以 MVC 網站專案來做說明,兩種專案的登入權限都差不多,我這邊是用 Forms Authentication 來做登入者的角色判斷,

以 MVC 網站專案中預設建立的 AccountController 來做修改,修改的地方為「LogOn」與「LogOff」

[HttpPost]
public ActionResult LogOn(string account, string password)
{
    if (string.IsNullOrEmpty(account) || string.IsNullOrEmpty(password))
    {
        return Content("您輸入的帳號資料錯誤,請重新登入!");
    }
    else
    {
        string role = account.Equals("Admin", StringComparison.OrdinalIgnoreCase)
            ? "Admin"
            : account.Equals("Manage", StringComparison.OrdinalIgnoreCase)
                ? "Manage"
                : "Other";
 
        string encryptTicket = Utils.SignIn(account, role);
        if (string.IsNullOrEmpty(encryptTicket))
        {
            return Content("Faild");
        }
        else
        {
            HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptTicket);
            authCookie.Expires = DateTime.Now.AddDays(1);
            this.Response.Cookies.Add(authCookie);
 
            string redirectUrl = Url.Action("Index", "Home");
            return Content(redirectUrl);
        }
    }
}
 
public ActionResult LogOff()
{
    //原本號稱可以清除所有 Cookie 的方法...
    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");
}

Utils.cs

public class Utils
{
    /// <summary>
    /// Signs the in.
    /// </summary>
    /// <param name="account">The account.</param>
    /// <param name="role">The role.</param>
    /// <returns></returns>
    public static string SignIn(string account, string role)
    {
        FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
            1,
            account,
            DateTime.Now,
            DateTime.Now.AddMinutes(60), // 登入時間 60 分鐘到期
            false,
            role);
 
        return FormsAuthentication.Encrypt(authTicket);
    }
 
    /// <summary>
    /// GetLogonUser
    /// </summary>
    /// <param name="encryptedTicket"></param>
    /// <returns></returns>
    public static string GetLogonUser(string encryptedTicket)
    {
        FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(encryptedTicket);
        if (string.IsNullOrEmpty(ticket.Name))
        {
            return string.Empty;
        }
        else
        {
            return ticket.Name;
        }
    }
 
    /// <summary>
    /// Checks the authenticated.
    /// </summary>
    /// <returns></returns>
    public static bool CheckAuthenticated()
    {
        if (HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName] == null)
        {
            return false;
        }
        else
        {
            string encryptedTicket = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName].Value;
 
            if (string.IsNullOrWhiteSpace(encryptedTicket))
            {
                return false;
            }
            else
            {
                string user = Utils.GetLogonUser(encryptedTicket);
                return !string.IsNullOrWhiteSpace(user);
            }
        }
    }
}

LogOn.cshtml

@model DefalutMVC.Models.LogOnModel
 
@{
    ViewBag.Title = "登入";
}
 
<h2>登入</h2>
<p>
    請輸入您的使用者名稱和密碼。@Html.ActionLink("註冊", "Register") (如果您沒有帳戶)
</p>
 
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
   1:  
   2: <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript">
   1: </script>
   2:  
   3: @Html.ValidationSummary(true, "登入失敗。請更正錯誤後再試一次。")
   4:  
   5: @using (Html.BeginForm()) {
   6:     <div>
   7:         <fieldset>
   8:             <legend>帳戶資訊</legend>
   9:  
  10:             <div class="editor-label">
  11:                 Account
  12:             </div>
  13:             <div class="editor-field">
  14:                 @Html.TextBox("Account", null, new { id = "Account" }) 
  15:             </div>
  16:  
  17:             <div class="editor-label">
  18:                 Password
  19:             </div>
  20:             <div class="editor-field">
  21:                 @Html.Password("Password", null, new { id = "Password" })
  22:             </div>
  23:  
  24:             <p>
  25:                 <input type="button" name="ButtonLogon" id="ButtonLogon" value="LogOn" />
  26:             </p>
  27:         </fieldset>
  28:     </div>
  29: }
  30:  
  31: <script src="../../Scripts/jquery-1.7.2.min.js" type="text/javascript">
   1: </script>
   2: <script language="javascript" type="text/javascript">
   3: <!--
   4:     $(document).ready(function () {
   5:         $('#Account').focus();
   6:         $('#ButtonLogon').bind('click', function () { ExecuteLogOn(); });
   7:     });
   8:  
   9:     function ExecuteLogOn() {
  10:         var account = $.trim($('#Account').val());
  11:         var password = $.trim($('#Password').val());
  12:  
  13:         if (account.length == 0 || password.length == 0) {
  14:             alert('input error');
  15:             return false;
  16:         }
  17:         else {
  18:             $.ajax({
  19:                 url: '@Url.Action("LogOn", "Account")',
  20:                 data: { account: account, password: password },
  21:                 type: 'post',
  22:                 async: false,
  23:                 cache: false,
  24:                 success: function (data) {
  25:                     if (data == 'Faild') {
  26:                         alert('Logon Faild');
  27:                         return false;
  28:                     }
  29:                     else {
  30:                         alert('Logon Success');
  31:                         location.href = data;
  32:                     }
  33:                 }
  34:             });
  35:         }
  36:     }
  37:  
  38: -->
</script>

 

當登入的功能完成之後,接下來就是要讓登入者角色讓 MiniProfiler 做判斷,而這個判斷是要放在哪邊呢?

一開始如果是未登入的狀態,是不能夠讓 Profiling Popup 顯示出來,而如果登入後的登入角色不是可以看到的也不能夠顯示,這個使用者登入角色的判斷就在 Global.asax 的「Application_AuthenticateRequest」中加入,

程式:

/// <summary>
/// Handles the AuthenticateRequest event of the Application control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
    bool hasUser = HttpContext.Current.User != null;
    bool isAuthenticated = hasUser && HttpContext.Current.User.Identity.IsAuthenticated;
    bool isIdentity = isAuthenticated && HttpContext.Current.User.Identity is FormsIdentity;
 
    if (isIdentity)
    {
        // 取得表單認證目前這位使用者的身份
        FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity;
 
        // 取得 FormsAuthenticationTicket 物件
        FormsAuthenticationTicket ticket = id.Ticket;
 
        // 取得 UserData 欄位資料 (這裡我們儲存的是角色)
        string userData = ticket.UserData;
 
        // 如果有多個角色可以用逗號分隔
        string[] roles = userData.Split(',');
 
        // 賦予該使用者新的身份 (含角色資訊)
        HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(id, roles);
 
        if (!HttpContext.Current.User.IsInRole("Admin"))
        {
            StackExchange.Profiling.MiniProfiler.Stop(discardResults: true);
        }
    }
    else
    {
        StackExchange.Profiling.MiniProfiler.Stop(discardResults: true);
    }
}

 

下圖的說明:

(1):如果為登入狀態,則檢查使用者是否符合瀏覽 Profiling Popup 的權限,這邊我只限定使用者角色為 Admin 才能顯示。

(2):如果是未登入狀態,則 Profiling Popup 就不能夠顯示,既使 AppSetting「enable_prifiling」為 true 也一樣不給顯示。

image

而 MiniProfiler.Stop() 中的「discardResults: true」則是說設定為 true ,就會清除掉目前的 MiniProfiler.Current 所記錄的內容,因為在最後要顯示前清除掉所有的記錄,那麼頁面上的 Profiling Popup 也就沒東西可以顯示。

 

看看未登入前的頁面(以下都是用遠端電腦來做測試):

不會顯示 Profiling Popup

image

 

接著以使用者「Manage」來做登入

使用者身分角色不是 Admin,所以也就不會顯示 Profiling Popup

image

 

接著以使用者「Admin」做登入,因為我們只有限定登入的使用者角色為 Admin 才能夠顯示 Profiling Popup,所以用 Admin 登入就可顯示 Profiling Popup

image

 


經過以上的幾個說明與實際操作之後希望可以解除對於 MiniProfilier 的疑慮與不清楚,最後與登入權限的功能整合,我只是做一個極為簡單的例子,而各位也可以將自己所實作的登入權限功能與 MiniProfiler 做整合,這麼一來就可以用更方便與清楚的方式來獲取遠端系統的系統執行效能的資訊,而另一方面也可以對 MiniProfiler 的安全性能做更進一步的管理與掌控。

 

參考資料:

Minifiler 官方網站 - http://miniprofiler.com/

Mostly working...Query Performance Logging with MiniProfiler

 

以上

2 則留言:

  1. 您好
    我是用mvc3的話可否直接修改
    /App_Start/MiniProfiler.cs 內的
    //if (request.IsLocal) { MiniProfiler.Start(); }
    bool prv_Enable_Prifiling = bool.Parse(ConfigurationManager.AppSettings["Enable_Prifiling"]);
    if (prv_Enable_Prifiling)
    {
    MiniProfiler.Start();
    }
    這樣改我測試起來 就可以達到開關MiniProfiler 不知道這樣的作法正確嗎?

    回覆刪除
    回覆
    1. 這麼做是可以的(看到你的發問才發覺到...我沒有交代 MVC 要怎麼處理)

      刪除

提醒

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