我正在设置SQL Server会话状态的一个非常基本的演示,但是在使其工作时遇到了一些麻烦。我正在尝试使用IIS 7.5和SQL Server 2008 R2在本地运行Windows 7进行测试。
最终,我需要一种方法来跟踪登录到几个不同Web服务器之间负载平衡的系统的用户数量。因此,每次用户登录或注销时,我都需要更新会话变量(存储在SQL中)。因此会话ID可能总是不同的。这可能吗?
这是我到目前为止所做的:
<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设置的值吗?
更新
我可以看到会话数据存储在SQL Management Studio的ASPStateTempSessions
表中,但每个站点仍然只能看到它写入的值。两个站点都设置会话变量,如下所示:
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:
我已按照 this article 中的说明修改了SP,并添加了一列用于分组到ASPStateTempApplications
表。这种类有效,但前提是我的两个网站都在同一个浏览器中打开(使用标签)。我需要能够在IE中打开Site1,将值保存到我的会话变量,关闭浏览器,在Chrome中打开Site2,然后读取值。从我用SQL Server会话状态读取的所有内容......这应该是可能的。
更新4 - 解决方案:
我按照@SilverNinja提供的 this article 的回答。我通过命令提示符重新创建ASPState
数据库(在Update#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)之前我的想法是错误的,会使用相同的会话 - 新的浏览器将启动它自己的会话。但是,在负载均衡的情况下,这不会导致任何问题。
答案 0 :(得分:7)
您遇到的问题是跨ASP.NET应用程序共享会话状态。 SQL Server会话提供程序不支持此功能。请参阅此SO post regarding what changes to make to SQL Server session provider to support cross-application sessions。
我不会使用会话来管理这个共享状态(即LastSiteUsed
)。此信息应与用户个人资料商店( Membership,Profile provider,自定义数据库等保持一致。由于会话可能会过期 - 跨应用程序使用共享会话不是跟踪持久用户特定状态的可靠机制。如果持久性并不比使用AppFabric Cache用于内存中跨应用程序共享状态更重要。
答案 1 :(得分:2)
对于任何想要或需要在不修改数据库存储过程的情况下解决此问题的人,请考虑这种方法(不适合胆小的人)。
基本思想是SqlSessionStateStore.SqlPartitionInfo
中存储了System.Web.SessionState.SqlSessionStateStore.s_singlePartitionInfo
的静态实例,我通过调用InitSqlInfo
来初始化。初始化此实例后,我会设置内部_appSuffix
字段的值。在设置_appSuffix
字段之前,需要初始化实例,否则我的自定义值将在初始化期间被覆盖。我使用与ASP.NET状态数据库中相同的哈希函数从应用程序名称计算哈希码。
请注意,这与它变得非常讨厌,并且完全依赖于可能在任何时间点发生变化的内部实现细节。但是,我不知道有任何实际的选择。自行决定使用并承担风险。
现在我已经给出了免责声明,如果.NET BCL中的这种实现确实发生了变化,我会感到惊讶,因为这种身份验证方法正在被替换,我认为微软没有任何理由在那里进行修补。 / p>
/*
* 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);
}
}