杜絕ASP.NET網站JavaScript註解外露
6 |
ASP.NET MVC的壓縮打包能有效縮小CSS與JS檔案體積,減少HTTP往返次數,進而提升網站效能。JavaScript經壓縮可讀性雖然已大幅下降,但"保護程式邏輯不外洩"的效果仍然有限,不必過度期望。只是壓縮對我還有另一層重大意義: "JavaScript中的註解會被一併移除!"
我很愛在程式裡寫故事註解,把程式邏輯修正的來龍去脈交待清楚,例如:
//2012-04-01 Bug Fix: VIP級使用者呼叫MehtodA前需呼叫MethodB以校正狀態
//2013-04-01 應客服部要求,錯誤次數上限調整為5
//2014-04-01 TODO: 不支援資料量大於1MB,未來如有超過上限需求,此段需修改
手上維護的系統一多,修改動機甚至更動本身很容易迷失在時間的洪流裡,等某天射茶包查出該處修改是禍源,要不是靠註解喚醒記憶,恐怕會有不小心問候到自己爹娘的風險;或者,急著把Bug修掉還原修改,卻忽略當初調整的理由,就會上演"修好A問題,以前改過的B問題又他X的冒出來"悲劇,少不了挨刮。腦海有幾段鮮明記憶: 某段修改在很久之後造成死傷慘重的大爆炸,靠著程式註解詳載使用者堅持調整的始末,第一時間列為呈堂證供,程式人員才沒被當祭品~ 註解能協助程式碼理解、防止邏輯被誤改,緊急時刻還能用來驅邪防身,是程序員的好朋友! 如果講成這樣還不能讓你寫註解,試試這個: 老闆請了個脾氣暴躁的程序員接手維護你的程式,有流言說那傢伙有重傷害前科...
總之,被註解救過小命,就更愛在程式碼加上滿滿註解,救人又救已,連JavaScript也不例外。但這類註解並不適合外流,其中可能涉及能當成八卦的敏感人事時地物,也可能透露開發者真性情,尤其JavaScript註解會原封不動送到Client端,要上到正式環境前,隱藏或移除才是上策。
ASP.NET MVC的打包壓縮功能啟用後,Client將出現<script src="/bundles/script_pack_name?v=XGaE…" type="text/javascript">及壓縮過內容,理論上使用者無從得知原始JavaScript路徑取得原始碼。但我的資安偏執容不下"看起來夠安全",畢竟原始檔案還是放在網站上,萬一有人得知原始路徑或壓縮功能被意外關閉,JavaScript的原始檔含註解就會經由/scripts/blah.js公諸於世,我決心要社絕這一層風險。
當初為專案客製的Script已集中在/scripts/mycode目錄方便管理,因此針對該目錄統一處理即可。一開始想到的做法是用WebGrease工具在每次部署時將JS檔壓成min.js,再將.js的內容也換成壓縮版,當同檔名的.js及min.js同時存在,ASP.NET MVC打包壓縮機制會直接取用min.js版打包,不致重複壓縮;而.js檔已被換成壓縮版,即使用/scripts/mycode/boo.js也看不到原始碼及註解。但這有個缺點 -- 當迫不得已必須在正式台偵錯時,看到的永遠是壓縮後的版本,無法用原始碼偵錯。
最後,我找到一個更彈性的解法: 使用HttpHandler攔截所有/scripts/mycode/*.js請求,讀取JS檔後即時壓縮再傳到Client端;而少數允許偵錯的內部機器,可事先在web.config中列舉IP,針對這些偵錯用IP,則提供未壓縮版本,魚與熊掌兼得。
在ASP.NET MVC網站加入AppScriptHandler.cs:
using Microsoft.Ajax.Utilities;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Hosting;
namespace MyWeb.Models
{
public class AppScriptsHandler : IHttpHandler
{
public bool IsReusable
{
get { return false; }
}
static string[] debugClients = null;
static string[] DebugClients
{
get
{
if (debugClients == null)
{
var list = new List<string>();
string config =
ConfigurationManager.AppSettings["AppScriptsDebugClients"];
if (!string.IsNullOrEmpty(config))
{
foreach (string ip in config.Split(',', ';'))
{
//Replace to regular expression pattern
list.Add(ip.Replace("*", "[0-9]+").Replace(".", "\\."));
}
}
debugClients = list.ToArray();
}
return debugClients;
}
}
//Check if the ip address matches any pattern in DebugClients
static bool IsDebugClient(string ip)
{
foreach (string ipPattern in DebugClients)
{
if (Regex.IsMatch(ip, ipPattern))
return true;
}
return false;
}
public void ProcessRequest(HttpContext context)
{
string jsFile = Path.GetFileName(context.Request.Url.AbsolutePath).ToLower();
string jsPath = Path.Combine(
HostingEnvironment.MapPath("~/Scripts/mycode"), jsFile);
var resp = context.Response;
if (!jsFile.EndsWith(".js") || !File.Exists(jsPath))
{
resp.Write("//JavaScript not found: " + jsFile);
}
else
{
string js = File.ReadAllText(jsPath);
resp.ContentType = "text/javascript";
if (jsFile.EndsWith(".min.js") ||
IsDebugClient(context.Request.UserHostAddress))
{
//if already minified or from debug clients
resp.Write(js);
}
else
{
//return minified result
Minifier minifier = new Minifier();
resp.Write(minifier.MinifyJavaScript(js));
}
}
}
}
}
在web.config中,加入appSetting指定可偵錯的來源IP: (支援萬用符號"*",酷吧?)
<add key="AppScriptsDebugClients" value="::1;127.0.0.1;192.168.1.*;172.28.*.*"/>
接著,在web.config要指定將script/mycode/*的存取請求交給AppScriptsHandler處理: <system.webserver>
<handlers>
<add name="scripts" path="scripts/mycode/*" verb="GET" type="MyWeb.Models.AppScriptsHandler" preCondition="integratedMode,runtimeVersionv4.0"/>
</handlers>
</system.webserver>
就這樣,使用偵錯機器可以看到原始版,其他IP只能看到壓縮版,成功對外隱藏開發者不為人知的一面,又能兼顧線上偵錯的便利性,很棒吧!
【延伸閱讀】
Comments
# by demo
類似的需求我會直接在VS內設定原始的JS在編譯時不輸出來解決^^
# by 挖賽
酷ㄟ~
# by metavige
是說現在如果是用 Angularjs,但是一堆 Html Template 是否也可以用類似的方式,移除註解以及 minify 呢?
# by Jeffrey
to metavige, WebGrease的壓縮針對JavaScript與CSS,如果您所指的Html Template內容主要都為HTML Tag,恐需另覓壓縮元件,但原理相同,應該可行。
# by metavige
瞭解,我去看了 WebGrease 的 Source ,的確只提供 js/css 的壓縮 因為我想 HTML 壓縮可能要考慮的東西比較多吧~~~ 另外我實際應用了一下這段程式碼,我發現如果指定的目錄下,有子目錄,子目錄的 js 檔案會無法顯示 所以我改了一下程式碼,讓子目錄的檔案也可以正常顯示 主要改的是 ProcessRequest 這個方法的前幾行 改的程式如下: string jsFile = Path.GetFileName(context.Request.Url.AbsolutePath); // 這邊跟原始檔案不同,是為了如果有子目錄的話,可以遞迴下去處理,避免把目錄寫死 var scriptRoot = "~" + context.Request.Url.AbsolutePath.Replace(jsFile, ""); string jsPath = Path.Combine( HostingEnvironment.MapPath(scriptRoot), jsFile); 提拱你參考!
# by Jeffrey
to metavige, 原程式的確未考慮到有子目錄的情境,謝謝你的補充。