10

我正在设置一个非常基本的 SQL Server 会话状态演示,但是在让它工作时遇到了一些麻烦。我正在尝试使用 IIS 7.5 和 SQL Server 2008 R2 在本地运行 Windows 7 进行测试。

最终,我需要一种方法来跟踪登录到在几个不同 Web 服务器之间进行负载平衡的系统的用户数量。因此,每次用户登录或注销时,我都需要更新一个会话变量(存储在 SQL 中)。所以会话 ID 可能总是不同的。这可能吗?

这是我到目前为止所做的:

  1. 在 IIS 中创建了两个新站点(Site1 和 Site2)
  2. 在 VS 2010 中创建了两个新的 Web 应用程序项目(Site1 和 Site2)
  3. 在这两个站点中,在 Default.aspx 页面上,我创建了一个按钮,单击该按钮时会将“Site1”或“Site2”(取决于站点)保存到名为“LastSiteUsed”的会话变量中。还有另一个按钮,单击该按钮时,将从“LastSiteUsed”会话变量中读取值并将其显示在标签中。
  4. 在 SQL 中,我创建了一个名为 dbSessionTest 的新数据库。然后我按照MSDN 上的说明使用 aspnet_regsql 工具安装会话状态数据库
  5. 在我的两个 Web 应用程序的 Web.config 文件中,我在下面添加了以下内容<System.Web>

<sessionState mode="SQLServer" sqlConnectionString="server=.\sqlexpress;database=dbSessionTest;uid=myUsername;pwd=myPassword" cookieless="false" timeout="20" allowCustomSqlDatabase="true"/>

两个站点都正常运行,但它们似乎没有共享会话。例如,当我单击按钮从 Site1 保存会话变量时,我希望能够从 Site2 读取该值,但它不起作用。Site2 只会返回“Site2”,Site1 只会返回“Site1”。

有人对我可能做错的事情有任何想法吗?我认为我应该能够从 Site2 读取 Site1 设置的值是错误的吗?

更新:

我可以看到会话数据存储在ASPStateTempSessionsSQL Management Studio 的表中,但每个站点仍然只能看到它写入的值。两个站点都像这样设置会话变量:

Session("LastSiteUsed") = "Site2"

两个站点都在检索以下值:

lblValue.Text = "Value from Session: " & Session("LastSiteUsed").ToString

访问存储在 SQL Server 中的会话变量是否需要以不同的方式执行此操作?

更新 2:

我尝试使用默认数据库 ASPState,它是通过运行以下命令创建的:

aspnet_regsql.exe -S MyServerName -E -ssadd -sstype p

然后简化了我的每个 Web.config 文件,例如:

<sessionState mode="SQLServer" sqlConnectionString="Data Source=.\sqlexpress;User ID=myUsername;Password=myPassword" cookieless="false" timeout="20"/>

但同样,没有运气。我希望能够从 Site1 设置会话变量,然后从 Site2 读取值,但它不起作用。同样,我可以看到表格中出现的条目,ASPStateTempSessions所以我知道它们输入正确。我注意到的一件事是他们有不同的会话 ID?

在设置/读取相同的会话变量时,我是否需要做一些不同的事情来确保在我的站点之间使用相同的会话 ID?

更新 3:

我已按照本文中的说明修改了 SP 并向表中添加了用于分组的ASPStateTempApplications列。这种工作,但前提是我的两个网站都在同一个浏览器中打开(使用标签)我需要能够在 IE 中打开 Site1,将值保存到我的会话变量,关闭浏览器,在 Chrome 中打开 Site2,然后读取值。从我用 SQL Server 会话状态读到的所有内容......这应该是可能的。

更新 4 - 解决方案:

我遵循了@SilverNinja 提供的这篇文章的答案。我通过命令提示符重新创建了ASPState数据库(以撤消更新 #3 中的 SP 更改),然后TempGetAppID根据链接中的答案修改了 SP。我还更新了我的两个 Web.config 文件以匹配该文章中的答案:

<sessionState mode="SQLServer"
              sqlConnectionString="Data Source=.\sqlexpress;User ID=myUsername;Password=myPassword;Application Name=CacheTest"/>

我还在两个 Web.config 文件中包含了相同的机器密钥:

<machineKey validationKey="59C42C5AB0988049AB0555E3F2128061AE9B75E3A522F25B34A21A46B51F188A5C0C1C74F918DFB44C33406B6E874A54167DFA06AC3BE1C3FEE8506E710015DB" decryptionKey="3C98C7E33D6BC8A8C4B7D966F42F2818D33AAB84A81C594AF8F4A533ADB23B97" validation="SHA1" decryption="AES" />

现在我可以(使用 IE)打开我的两个站点,从 Site1 设置一个值,然后从 Site2 读取它(反之亦然)。当我检查表格时,只有一个 SessionID 存在(两个站点都正确使用相同的会话)。在打开新浏览器(例如,使用 Chrome)之前,我的想法是错误的,会使用相同的会话 - 新浏览器将启动它自己的会话。但是,在负载平衡的情况下,这不会导致任何问题。

4

2 回答 2

7

您遇到的问题是跨 ASP.NET 应用程序共享会话状态。SQL Server 会话提供程序不支持开箱即用。请参阅此SO 帖子,了解对 SQL Server 会话提供程序进行哪些更改以支持跨应用程序会话

我不会使用 session 来管理这个共享状态(即LastSiteUsed)。此信息应与用户配置文件存储(成员资格配置文件提供者、自定义数据库等)一起保存。由于会话可能过期 - 跨应用程序使用共享会话不是跟踪持久用户特定状态的可靠机制。如果持久性不重要,则将AppFabric 缓存用于内存中的跨应用程序共享状态。

于 2012-10-30T17:53:11.037 回答
2

对于任何想要或需要在不修改数据库存储过程的情况下解决此问题的人,请考虑这种方法(不适合胆小的人)。

基本思想是有一个SqlSessionStateStore.SqlPartitionInfo存储在其中的静态实例System.Web.SessionState.SqlSessionStateStore.s_singlePartitionInfo,我通过调用InitSqlInfo. 初始化此实例后,我设置内部_appSuffix字段的值。在设置字段之前需要初始化实例_appSuffix,否则我的自定义值将在初始化期间被覆盖。我使用与 ASP.NET 状态数据库中相同的哈希函数从提供的应用程序名称计算哈希码。

请注意,这很糟糕,并且完全依赖于可能随时更改的内部实现细节。但是,我不知道任何实用的替代方案。自行决定使用并承担风险。

现在我已经给出了我的免责声明,如果 .NET BCL 中的这个实现确实发生了变化,我会感到惊讶,因为这种身份验证方法正在被替换,我认为微软没有任何理由在其中进行修补。

/*
 * This code is a workaround for the fact that session state in ASP.NET SqlServer mode is isolated by application. The AppDomain's appId is used to
 * segregate applications, and there is no official way exposed to modify this behaviour.
 * 
 * Some workarounds tackle the problem by modifying the ASP.NET state database. This workaround approaches the problem from the application code
 * and will be appropriate for those who do not want to alter the database. We are using it during a migration process from old to new technology stacks, 
 * where we want the transition between the two sites to be seamless.
 * 
 * As always, when relying on implementation details, the reflection based approach used here may break in future / past versions of the .NET framework.
 * Test thoroughly.
 * 
 * Usage: add this to your Global.asax:
 *       protected void Application_BeginRequest()
 *       {
 *           SessionStateCrossApplicationHacker.SetSessionStateApplicationName("an application");
 *       }
 */

using System;
using System.Data.SqlClient;
using System.Globalization;
using System.Reflection;
using System.Web.SessionState;

public static class SessionStateCrossApplicationHacker
{
    static string _appName;
    static readonly object _appNameLock = new object();

    public static void SetSessionStateApplicationName(string appName)
    {
        if (_appName != appName)
        {
            lock (_appNameLock)
            {
                if (_appName != appName)
                {
                    SetSessionStateApplicationNameOnceOnly(appName);

                    _appName = appName;
                }
            }
        }
    }

    static void SetSessionStateApplicationNameOnceOnly(string appName)
    {
        //get the static instance of SqlSessionStateStore.SqlPartitionInfo from System.Web.SessionState.SqlSessionStateStore.s_singlePartitionInfo
        var sqlSessionStateStoreType = typeof (SessionStateMode).Assembly.GetType("System.Web.SessionState.SqlSessionStateStore");
        var sqlSessionStatePartitionInfoInstance = GetStaticFieldValue(sqlSessionStateStoreType, "s_singlePartitionInfo");
        if (sqlSessionStatePartitionInfoInstance == null)
            throw new InvalidOperationException("You'll need to call this method later in the pipeline - the session state mechanism (SessionStateModule, which is an IHttpModule) has not been initialised yet. Try calling this method in Global.asax's Application_BeginRequest. Also make sure that you do not specify a partitionResolverType in your web.config.");

        //ensure that the session has not been used prior to this with an incorrect app ID
        var isStaticSqlPartitionInfoInitialised = GetFieldValue<bool>(sqlSessionStatePartitionInfoInstance, "_sqlInfoInited");
        if (isStaticSqlPartitionInfoInitialised)
            throw new InvalidOperationException("You'll need to call this method earlier in the pipeline - before any sessions have been loaded.");

        //force initialisation of the static SqlSessionStateStore.SqlPartitionInfo instance - otherwise this will happen later and overwrite our change
        var connectionString = GetFieldValue<string>(sqlSessionStatePartitionInfoInstance, "_sqlConnectionString");
        using (var connection = new SqlConnection(connectionString))
        {
            connection.Open();
            CallInstanceMethod(sqlSessionStatePartitionInfoInstance, "InitSqlInfo", connection);
        }

        //calculate and set the application hash code
        string applicationNameHashCode = GetHashCode(appName).ToString("x8", CultureInfo.InvariantCulture);
        GetField(sqlSessionStatePartitionInfoInstance, "_appSuffix").SetValue(sqlSessionStatePartitionInfoInstance, applicationNameHashCode);
    }

    static int GetHashCode(string appName)
    {
        string s = appName.ToLower();
        int hash = 5381;
        int len = s.Length;

        for (int i = 0; i < len; i++)
        {
            int c = Convert.ToInt32(s[i]);
            hash = ((hash << 5) + hash) ^ c;
        }

        return hash;
    }

    static void CallInstanceMethod(object instance, string methodName, params object[] parameters)
    {
        var methodInfo = instance.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
        methodInfo.Invoke(instance, parameters);
    }

    static object GetStaticFieldValue(Type typeWithStaticField, string staticFieldName)
    {
        return typeWithStaticField.GetField(staticFieldName, BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
    }

    static FieldInfo GetField(object instance, string name)
    {
        return instance.GetType().GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
    }

    static T GetFieldValue<T>(object instance, string name)
    {
        return (T)GetField(instance, name).GetValue(instance);
    }
}
于 2013-12-17T23:34:18.460 回答