

Introduction
In some situations, we need exclusive login, which means double login is disabled, the account can only be used by one person at a time. In other words,
two users can not use the same account at the same time from different places.
I have looked at some popular solutions. Usually, people do it in this way: the session timeout is set to a small number, like 1 minute.
For the logged in user, the session is kept alive by a background timer or something else. And the login state is stored in a global place.
When the session is started, the login state is initiated, otherwise if the session ends, the login state is released.
There are some defects in this solution:
- HTTP connection is not a long-time connection, so if the user closes the page,
Session_End is not triggered immediately. That causes many problems.
If I login using Internet Explorer and then I want to switch to Firefox, and if I forget to click on the "sign out" link on the page before I close IE,
then the site won't allow me to login in Firefox before the session times out.
- If we keep the session alive by a background process, for example, we use the
setTimeout in JavaScript, and refresh a back-end page every 30 seconds,
that will increase the pressure on the server.
We need a better solution, and here I have one:
- We always allow account login.
- We store the latest login session ID for the given account in a global place.
- We monitor every request, and detect that if the current user is signed in but the session ID is not equal to the latest session ID of this account, kick the user out.
Background
To implement it in this way, we need extra code in two places:
- login process
PreInit in the HTTPModule
Next, I will explain the code.
Using the code
First, we need some variables.
private static Hashtable s_UserID_SessionID_Pair_Collection = new Hashtable();
public static long CurrentUserID
{
get
{
if (HttpContext.Current.Session == null ||
HttpContext.Current.Session["CurrentUserID"] == null)
{
return -1L;
}
return (long)HttpContext.Current.Session["CurrentUserID"];
}
set
{
HttpContext.Current.Session["CurrentUserID"] = value;
}
}
Next, in the login process...
protected void btnLogin_Click(object sender, EventArgs e)
{
const long userId = 288L;
LoginState.Login(userId);
Response.Redirect("Default.aspx");
}
The LoginState.Login() is listed here:
public static void Login(long userid)
{
CurrentUserID = userid;
s_UserID_SessionID_Pair_Collection[userid] =
HttpContext.Current.Session.SessionID;
}
Now add an HTTPModule class:
public class DoubleLoginMonitorModule : IHttpModule
{
public String ModuleName
{
get { return "DoubleLoginMonitorModule"; }
}
public void Init(HttpApplication application)
{
application.PreRequestHandlerExecute +=
new EventHandler(Application_PreRequestHandlerExecute);
}
private void Application_PreRequestHandlerExecute(Object source, EventArgs e)
{
HttpApplication application = (HttpApplication)source;
HttpContext context = application.Context;
if (LoginState.IsDoubleLogin())
{
HttpContext.Current.Response.Redirect("logoff.htm");
HttpContext.Current.Response.End();
}
}
public void Dispose()
{
}
}
The LoginState.IsDoubleLogin() is listed in detail here:
public static bool IsDoubleLogin()
{
if (CurrentUserID == -1L)
return false;
bool isDoubleLogin =
string.Compare( (string)
s_UserID_SessionID_Pair_Collection[CurrentUserID]
, HttpContext.Current.Session.SessionID
) != 0;
if (isDoubleLogin)
{
HttpContext.Current.Session.Clear();
HttpContext.Current.Session.Abandon();
}
return isDoubleLogin;
}
Finally, modify the web.config, and add the HTTP Module:
="1.0"
<configuration>
<appSettings/>
<connectionStrings/>
<system.web>
-->
<httpModules>
<add name="DoubleLoginMonitorModule"
type="DisableDoubleLogin.DoubleLoginMonitorModule" />
</httpModules>
</system.web>
-->
<system.webServer>
<validation validateIntegratedModeConfiguration="false" />
<modules>
<add name="DoubleLoginMonitorModule"
type="DisableDoubleLogin.DoubleLoginMonitorModule" />
</modules>
</system.webServer>
</configuration>
Afterword
It is very simple, isn't it?
For the servers behind load balancing, we need to care about:
- Keep the session sync, MS provides several options like the ASP.NET State Service.
- The SessionID--AccountID pair should be stored in a global place, database is a good choice.