經常來這個部落格的人應該會發現到我一直推薦大家使用 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 而減損系統執行效能。
各位看下面這張圖的程式
MVC 網站專案使用 NuGet 加入專案後就預設會在系統中加入有關安全性的程式片段,而 WebForms 網站專案則是我們必須手動去加入,讓每個 Request 都必須是 Local 端 才會起始 MiniProfiler 的功能。
以下就來測試一個新建立的 MVC 網站專案,在只有加入 MiniProfiler 後未作任何修改設定的情況下,遠端瀏覽網頁會有什麼樣的狀況。
遠端電腦的連線
建立一個 ASP.NET MVC 3 的網站專案,尚未做任何的功能,也還沒有加入資料庫類別,只有加入 MiniProfiler 而已,以下是已經加入 MiniProfiler 後的 Global.asax 內容,
唯一有修改的就是 _Layout.cshtml 的內容,分別加上「@using StackExchange.Profiling;」「@MiniProfiler.RenderIncludes()」
Local 端的測試,自動就會顯示 Profiling Popup
那如果是遠端電腦來瀏覽會不會顯示 Profiling Popup 呢?
以 TeamViewer 遠端登入別台主機,然後在這電腦上連回來測試,遠端電腦開啟網頁是不會出現 Profiling Popup 。
網站專案加入 MiniProfiler 之後,雖然沒有做任何安全性的修改,也不會讓網站的 Profiler 資料在非本地端的連線上顯示,所以不用擔心加入 MiniProfiler 之後會把重要資料給外洩出去,而且專案要上線也不用大份周章的煩惱要把 MiniProfiler 給移除。
自行設定顯示與否
在前面就有列出一段在 Global.asax 中判別是否為 Local 的程式片段「if ( Request.IsLocal ) …… 」
這是可以讓我們自己來決定專案中的 MiniProfiler 的 Profiling Popup 是否需要顯示,因為我們可以自己來做操控的,所以我們接下來就來試試看如何讓遠端的網頁上顯示 Profiling Popup。
接下來先以 ASP.NET WebForms 來做講解
首先,我們可以在 Web.Config 中增加一個 AppSetting「enable_prifiling」,然後依據這個設定來決定 Profiling Popup 是否需要顯示,
修改 Global.asax 中的設定
前面增加一個型別為 bool 的欄位與屬性,預設為 false,用來取得並記錄在 Web.Config 的「enable_profiling」設定
修改,Global.asax - Application_Start
修改,Global.asax - Application_BeginRequest 與 Application_EndRequest
AppSetting「enable_prifiling」設定為 true,所以不論在 Local 端 或是遠端電腦都可以看到 Profiling Popup,
Local 端
遠端
再來看看 ASP.NET MVC 網站專案的設定,MVC 網站專案的設定與 WebForms 網站專案的設定其實大致相同,
Web.Config 的 AppSettings 設定
型別為 bool 的 enableProfiling 欄位與屬性
Global.asax - Application_Start
Global.asax – Application_BeginRequest, Application_EndRequest
但如果你以為這樣的設定就讓遠端可以顯示 Profiling Popup 嗎?
答案是:「不行!」
我原本也以為與 WebForms 網站專案一樣的設定,後來我才發現到,MVC 網站專案還要修改另一個地方,一開始使用 NuGet 加入 MiniProfiler 之後,會在網站專案中加入一個目錄「App_Start」,這個目錄下的 MiniProfiler.cs 中有地方需要我們來做修改,
在下圖中用紅線框起來的地方,一開始的初始狀態並非是註解起來的,因為安全性的問題,所以一開始 MiniProfiler 會以 IHttpModile 的方式,讓 BeginRequest 增加是否為本地端的判別,而我們可以選擇在 Global.asax 中來做這兩個部分的處理,所以就可以把 MiniProfilerStartupModule 中的兩個地方給註解掉,
以上的設定都完成之後,就可以來看看 Local 端與遠端所呈現的頁面,
Local 端
遠端
依據登入者角色來決定 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 也一樣不給顯示。
而 MiniProfiler.Stop() 中的「discardResults: true」則是說設定為 true ,就會清除掉目前的 MiniProfiler.Current 所記錄的內容,因為在最後要顯示前清除掉所有的記錄,那麼頁面上的 Profiling Popup 也就沒東西可以顯示。
看看未登入前的頁面(以下都是用遠端電腦來做測試):
不會顯示 Profiling Popup
接著以使用者「Manage」來做登入
使用者身分角色不是 Admin,所以也就不會顯示 Profiling Popup
接著以使用者「Admin」做登入,因為我們只有限定登入的使用者角色為 Admin 才能夠顯示 Profiling Popup,所以用 Admin 登入就可顯示 Profiling Popup
經過以上的幾個說明與實際操作之後希望可以解除對於 MiniProfilier 的疑慮與不清楚,最後與登入權限的功能整合,我只是做一個極為簡單的例子,而各位也可以將自己所實作的登入權限功能與 MiniProfiler 做整合,這麼一來就可以用更方便與清楚的方式來獲取遠端系統的系統執行效能的資訊,而另一方面也可以對 MiniProfiler 的安全性能做更進一步的管理與掌控。
參考資料:
Minifiler 官方網站 - http://miniprofiler.com/
Mostly working... 「Query Performance Logging with MiniProfiler」
以上
您好
回覆刪除我是用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 不知道這樣的作法正確嗎?
這麼做是可以的(看到你的發問才發覺到...我沒有交代 MVC 要怎麼處理)
刪除