概要的說,.NET內建的Session其實是藉助於Cookie機制實現的。下面先解釋為什麼這麼說,然後通過對比一個.NET內建的Session執行個體和一個手動類比的Session機制(會藉助於Cookie)來進一步驗證。
為什麼這麼說
(1)首先,由於需要用到一些必要的Cookie相關知識,這裡簡要介紹:
Ø Cookie存放於用戶端,所以東西是在別人手中,所以就不會很安全
Ø 由於瀏覽器可以設定清除、禁用Cookie,所以不可以將不可或缺的東西放到Cookie中,只能將可有可無的東西放到Cookie中。
Ø 伺服器將Cookie寫到用戶端,之後當客戶訪問伺服器(瀏覽器不變)該網站的任何內容時,request的資訊中都會帶上所有的Cookie的值。
Ø Cookie不能誇不同品牌的瀏覽器,互相訪問(比如Chrom寫的Cookie,IE不能使用)。
Ø 若不設定Cookie到期時間(Expires),則稱為會話Cookie,表示這個Cookie的生命期為瀏覽器會話期間,關閉瀏覽器視窗,Cookie就消失,會話Cookie一般不儲存在硬碟上而是儲存在記憶體裡;若設定了到期時間,瀏覽器就會把Cookie儲存到硬碟上,關閉後再次開啟瀏覽器,這些Cookie仍然有效直到超過設定的到期時間,或許應該這麼說,如果設定了Expires,則這個Cookie的最長生命週期為Expires的值,如果Cookie已存滿(個數和容量大小都有限制)則即使沒到期,瀏覽器也會給刪除。
(2)下面開始解釋.NET的Session存放原理:
上面介紹了,ASP.NET為了保持和傳遞狀態,一種機制就是上面的Cookie。寫入Cookie的資料是存放在用戶端的,不會很安全,所以需要將資料從用戶端移到伺服器端,即所謂的Session。但Session並不是將單純的資料直接儲存到伺服器記憶體中,而是建立一個類似散列表的id-value映射表結構,value即指我們要儲存的資料,id是由ASP.NET自動產生的一個編號(ASP.NET_SessionId),即:每當我們要寫資料到Session時,ASP.NET就會產生一個編號(ASP.NET_SessionId,它是一個既不會重複,又不容易被找到規律以仿造的字串),然後將編號作為資料的索引把二者進行綁定,然後會將這個編號以Cookie的形式儲存到用戶端,而將資料保留到伺服器端的記憶體中,這樣在用戶端請求Session的同時會將Cookie中的ASP.NET_SessionId一同發送到伺服器端,伺服器就可以根據這個編號在自己的資料中進行檢索,進而取出它的value值。這就是ASP.NET中的Session機制原理。可參考下面的草圖:
執行個體說明-分析
下面做這麼一個小例子:在登陸的時候將使用者名稱寫入Session,然後才能瀏覽網站內部的其它頁面。如果沒有登入,直接請求網站內部的page,則會檢測Session,如果為空白則跳轉到登陸頁面。
分別用.NET內建的Session執行個體和手動類比的Session機制(會藉助於Cookie)來實現。
當然,處理頁面的很少的部分仍然採用NVelocity,所以首先添加它的DLL,並引用(參考前面兩篇文章)。然後添加封裝好的”渲染”代碼, 如下的CommonHelper類:
public class CommonHelper { /// <summary> /// 用data資料填充templateName模板,渲染產生html,返回 /// </summary> /// <param name="templateName"></param> /// <param name="data"></param> /// <returns></returns> public static string RenderHtml(string templateName, object data) { VelocityEngine vltEngine = new VelocityEngine(); vltEngine.SetProperty(RuntimeConstants.RESOURCE_LOADER, "file"); vltEngine.SetProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, System.Web.Hosting.HostingEnvironment.MapPath("~/templates"));//模板檔案所在的檔案夾 vltEngine.Init(); VelocityContext vltContext = new VelocityContext(); vltContext.Put("TemData", data);//設定參數,在模板中可以通過$data來引用 Template vltTemplate = vltEngine.GetTemplate(templateName); System.IO.StringWriter vltWriter = new System.IO.StringWriter(); vltTemplate.Merge(vltContext, vltWriter); string html = vltWriter.GetStringBuilder().ToString(); return html; } }
.NET內建的Session執行個體
先用我們平時用的Session實現上例,看看它的儲存機制。
添加三個檔案:Login3.html、Login3.ashx(做登入處理);TestLogin3.ashx(作為網站內部的其它頁面)。代碼如下:
Login3.html源碼及運行介面:
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>.net內建的Session測試</title></head><body> <form action="../Login3.ashx" method="get"> 使用者名稱:<input type="text" name="username" /> <br /> 密 碼:<input type="text" name="password"/> <br /> <input type="submit" name="login" value="提交" /> </form></body></html>
Login3.ashx:
public class Login3 : IHttpHandler, IRequiresSessionState { public void ProcessRequest(HttpContext context) { context.Response.ContentType = "text/html"; string UserName = context.Request["username"]; string PassWord = context.Request["password"]; //登陸時,判斷使用者名稱是否正確(這裡設定為admin),正確則儲存使用者名稱到Session,並跳轉到TestLogin3.ashx //否則清空輸入框,頁面不變 if (UserName == "admin") { //將使用者名稱儲存到.net內建Session中 context.Session["username"] = UserName; //儲存好使用者名稱後進入TestLogin3.ashx頁面 context.Response.Redirect("TestLogin3.ashx"); } else { string html = CommonHelper.RenderHtml("Login3.html", null); context.Response.Write(html); } } public bool IsReusable { get { return false; } } }
TestLogin3.ashx:
public class TestLogin3 : IHttpHandler,IRequiresSessionState { public void ProcessRequest(HttpContext context) { context.Response.ContentType = "text/html"; //如果Session為null,則跳轉到登陸頁 if (context.Session == null) { context.Response.Redirect("Login3.ashx"); } else { string username=(string)context.Session["username"]; //如果Session中的username對象為空白或不存在則跳轉到登陸頁,否則將它顯示出來 if (string.IsNullOrEmpty(username)) { context.Response.Redirect("Login3.ashx"); } else { context.Response.Write(username); } } } public bool IsReusable { get { return false; } } }
如下為填寫使用者名稱為admin點擊“登入”捕獲的資料,請求的是Login3.ashx,裡面的處理是將使用者名稱儲存寫入到了Session中,而可以看到響應標題中,有一個寫Cookie的過程,而且Cookie的內容為ASP.NET_SessionId。
當Login3.ashx驗證使用者名稱正確後,立刻跳轉到TestLogin3.ashx,如為跳轉時捕獲的資料,可以看到在請求的標題中攜帶有Cookie,而Cookie的值正是上面寫Session時儲存到用戶端的ASP.NET_SessionId。
TestLogin3中的處理是,驗證.Session["username"]是否存在,存在則取其值並顯示。而驗證是否存在和取其值的過程都是根據請求中的Cookie中的ASP.NET_SessionId來做的。
上例即可說明Session機制的原理。下面通過手動用Cookie來類比Session來做相同的執行個體,以便大家理解的更深刻。
手動用Cookie來類比Session機制
添加四個檔案:Login2.html、Login2.ashx(做登入處理);TestLogin2.ashx(作為網站內部的其它頁面)、SessionMgr.cs(類比.net內部對Session對象的封裝)。代碼如下:
Login2.html與上面的Login3.html代碼相同,只有title不同,代碼:
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>類比.net內建的Session機制</title></head><body> <form action="../Login2.ashx" method="get"> 使用者名稱:<input type="text" name="username" /> <br /> 密 碼:<input type="text" name="password"/> <br /> <input type="submit" name="login" value="提交" /> </form></body></html>
SessionMgr.cs:
//自訂Session的操作類 public class SessionMgr { //Static在.net framework啟動並執行時候一直存在 //自訂一個Dictionary類型的對象,來類比.net內建的Session對象 private static Dictionary<Guid,string> testSession=new Dictionary<Guid,string>(); //寫(自訂)session public static void WriteSession(Guid id, string value) { testSession[id] = value; } //判斷是否已經存在編號為id的(自訂)session public static bool IsWriteSession(Guid id) { return testSession.Keys.Contains(id); } //擷取編號為id的(自訂)session的value值 public static string Get(Guid id) { return testSession[id]; } }
Login2.ashx:
public class Login2 : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType = "text/html"; //取登陸時填寫的使用者名稱、密碼 string UserName=context.Request["username"]; string PassWord = context.Request["password"]; //登陸時,判斷使用者名稱是否正確(這裡設定為admin),正確則儲存使用者名稱到Session,並跳轉到TestLogin3.ashx //否則清空輸入框,頁面不變 if (UserName == "admin") { //關鍵點 Guid id = Guid.NewGuid();//產生一個session編號,Guid可產生一個全球唯一的編碼,來類比.net內建Session的ASP.NET_SessionId SessionMgr.WriteSession(id, UserName);//寫入(自訂的)session,將UserName與產生的id綁定(藉助於SessionMgr類) HttpCookie cookie1 = new HttpCookie("sessionid", id.ToString());//只將id儲存到cookie中,並未儲存對應的資料(UserName) context.Response.SetCookie(cookie1); context.Response.Redirect("TestLogin2.ashx"); } else { string html = CommonHelper.RenderHtml("Login2.html", null); context.Response.Write(html); } } public bool IsReusable { get { return false; } } }
TestLogin2.ashx:
public class TestLogin2 : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType = "text/html"; HttpCookie cookie = context.Request.Cookies["sessionid"]; //請求此頁時,如果cookie為null則跳轉到登陸頁 //如果不為空白,則取出cookie的值作為一個Guid編號 if (cookie == null) { context.Response.Redirect("Login2.ashx"); } else { Guid id = new Guid(cookie.Value); //判斷伺服器端的Dictionary中是否包含編號為id的對象 //如果包含則取出其value,並顯示 //否則跳轉到登陸頁 if (SessionMgr.IsWriteSession(id)) { string value = SessionMgr.Get(id); context.Response.Write(value); } else { context.Response.Write("<script>alert('沒有登入!');</script>"); context.Response.Redirect("Login2.ashx"); } } } public bool IsReusable { get { return false; } } }
上例中用一個Dictionary<id,value>對象來類比Session對象,用它的id(Guid)來類比.NET自動產生的編號(ASP.NET_SessionId),用它的value來類比要寫入Session的資料。
瀏覽器禁用Cookie問題
上面介紹Cookie時談到,瀏覽器可以設定清除、禁用Cookie。而剛剛談到的Session機制是依賴於Cookie的。那麼按上面的意思,當禁用Cookie時就意味著ASP.NET_SessionId 無法回傳伺服器,Session也就無法使用。是否是這樣?答案肯定是否,應對這種情況,經常用兩種方法:
URL地址修正:
即將該使用者Session的id資訊重寫到URL地址中。伺服器能夠解析重寫後的URL擷取Session的id。這樣即使用戶端不支援Cookie,也可以使用Session來記錄使用者狀態。
表單隱藏欄位
伺服器會自動修改表單,添加一個隱藏欄位,以便在表單提交時能夠把session id傳遞迴伺服器。例如:
總結
之前寫過相關的文章,完全是理論的學習,現在所有的東西都可以通過寫代碼和瀏覽器調試來驗證自己的想法。上面是目前階段自己的理解,希望能協助大家理解ASP.NET的Session機制。