The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

如何讓 IIS6 / IIS7 中同站台不同應用程式間共用 Session 資料

在 Web Farm 環境下部署網站需要關注的細節可不少,在部署大型網站的時候 IIS 這部分到底要算 IT 的領域還是開發人員(Developer)的領域其實分不太清楚,像要在「同一個站台」區分「不同應用程式」且還要能讓 Session 彼此互通,這到底應該歸誰管呢?這可不是用「ASP.NET 開發伺服器」可以模擬出來的,而 IT 人員如果不會寫程式應該也不知該如何是好,這也是我認為 ASP.NET 開發人員應該多熟悉 IIS 的原因。

要在 Web Farm 環境下要達成 Session 互通有以下條件:

  1. 確保 <machineKey> 要設定一致
  2. 確保各站台在 IIS 中 metabase 定義的「應用程式路徑」必須一致
  3. 使用者的 Session Cookie 的 名稱/值 必須一致 (不能跨越不同的網域網址不同的父網域網址)

這三個條件是我從四處的文件與實際經驗累積而來的結果,而其中最不容易發現的是「應用程式路徑」��須一致這個條件。

例如在 IIS 中,預設站台的 ID 為 1,在站台根目錄這個預設用程式的「應用程式路徑」為:

/LM/W3SVC/1/ROOT

若你在 Web Farm 環境下的第二台伺服器,除了原本的預設站台外,再建立另一個新的站台,則該站台的 ID 為 2,其「應用程式路徑」為:

/LM/W3SVC/2/ROOT

那麼這兩個站台便無法共用 Session,即便 Session 儲存後端用 SQL ServerASP.NET 狀態服務 且也設定好相同的 <machineKey> 也一樣無法互通 Session 資料。

基於這個規則,在架設 Web Farm 網站時就必須特別小心設定,否則可能查設定查到天荒地老也不知道為什麼會這樣,但在不太複雜的網站下,通常多台 Web Farm 主機下也各別只會有一個站台,所以也不一定會遇到這狀況。

那麼在「同一個站台」下「不同應用程式」時,其實就是「兩個應用程式」,我們可以利用 appcmd 指令列工具測試一下:

C:\Windows\System32\inetsrv>appcmd list app
APP "Default Web Site/" (applicationPool:DefaultAppPool)
APP "Default Web Site/WebSite2" (applicationPool:DefaultAppPool)

以上述為例,在 Default Web Site 站台下另外新增了一個「應用程式」為 /WebSite2,其「應用程式路徑」分別為:

/LM/W3SVC/1/ROOT
/LM/W3SVC/1/ROOT/WebSite2

所以這兩個應用程式基本上是無法互通 Session 的,但你可能會納悶在同一個站台下為何 Session 無法互通,因為在 .NET Framework 裡的 System.Web.SessionState 命名空間已經預先設定了取得 Session 資料的邏輯,所以這是 by design 的情況,程式碼並無法修改。

 

這些屬性無法透過「正規」的管道進行修正或調整,所以「表面上」似乎無解,除非你自行撰寫新的 Session 機制並將 IIS 中內建的 Session Module 抽換掉,但這實在太累人了,沒必要重新發明輪子。

還好在 .NET 有 Reflection (反映) 機制,透過自訂的 HttpModule 可以讓 SessionStateModule 模組被載入之前動態將 System.Web.HttpRuntime 型別中的 _appDomainAppId 的值換成我們希望的值,這樣就可以讓不同應用程式之間共用 Session 資料了,程式碼如下:

FieldInfo runtimeInfo = typeof(HttpRuntime).GetField("_theRuntime", 
BindingFlags.Static | BindingFlags.NonPublic);

HttpRuntime theRuntime = (HttpRuntime)runtimeInfo.GetValue(null);

FieldInfo appDomainAppIdInfo = typeof(HttpRuntime).GetField("_appDomainAppId",
BindingFlags.Instance | BindingFlags.NonPublic);

appDomainAppIdInfo.SetValue(theRuntime, "/LM/W3SVC/1/ROOT");

請注意:不同的站台會有不同的 Website ID 所以請不要照抄喔,一般來說預設站台的 ID 都是 1 所以才可以用上述的程式碼。

假設我們這個 HttpModule 的類別名稱為 SharedSessionModule 並放置在 App_Code 動態編譯目錄下,我們就可以修改 web.config 將我們自訂的 SharedSessionModule 註冊到 IIS6 的 <httpModules> 區段,或 IIS7 的 <modules> 區段中。

IIS6

<httpModules>
<add name="SharedSessionModule" type="SharedSessionModule, App_Code"/>
</httpModules>

IIS7

<modules>
<remove name="Session" />
<add name="SharedSessionModule" type="SharedSessionModule, App_Code"/>
<add name="Session1" type="System.Web.SessionState.SessionStateModule,
System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
/>
</modules>

注意: 在 IIS7 中由於 整合管線(Integrated Pipeline) 的關係 HttpModule 的載入順序有些應該注意的地方,你不能使用在 applicationHost.config 中已經定義過的模組名稱,否則將無法調整載入模組的順序,所以才必須「先移除 Session」再「載入 SharedSessionModule」然後再「重新載入 SessionStateModule 並命名為 Session1」才行。如果預設的 SessionStateModule 模組在 SharedSessionModule 之前先執行,在 SessionStateModule 模組完成初始化動作後 SharedSessionModule 再做任何修改就沒有作用了。

相關連結