Estrategia 1: Emplear la caché de aplicación como registro de sesiones activas
La diferencia clave entre Cache y Session radica en su alcance: Session está limitado a un usuario concreto, mientras que Cache es compartido por toda la aplicación. Aprovechando esta característica, se puede almacenar el identificador del usuario como clave de caché con una expiración equivalente al tiempo de espera de la sesión. Al intentar iniciar sesión, basta con verificar si esa clave ya existe.
private void ProcesarInicioSesion(object originador, EventArgs args)
{
string nombreAcceso = txtUsuario.Text.Trim();
string claveCache = "sess_" + nombreAcceso;
if (HttpRuntime.Cache[claveCache] == null)
{
var duracionSesion = new TimeSpan(0, 0, HttpContext.Current.Session.Timeout, 0, 0);
HttpRuntime.Cache.Insert(
claveCache,
DateTime.Now.ToString(),
null,
Cache.NoAbsoluteExpiration,
duracionSesion,
CacheItemPriority.High,
null);
Session["UsuarioActual"] = nombreAcceso;
lblEstado.Text = "Sesión iniciada correctamente.";
}
else
{
lblEstado.Text = "Esta cuenta ya tiene una sesión activa.";
}
}
Estrategia 2: Seguimiento mediante el objeto Application y un diccionario concurrente
Otra aproximación consiste en mantener un diccionario en el ámbito de Application que registre qué usuarios están conectados, asociando cada nombre de usuario con su identificador de sesión. Este enfoque es útil cuando no se cuenta con los componentes de membresía de ASP.NET.
Validación al iniciar sesión
private void ValidarCredenciales(object origen, EventArgs evt)
{
var usuario = new CuentaUsuario { Login = txtLogin.Text.Trim() };
var sesionesActivas = (Hashtable)Application["SesionesActivas"];
if (sesionesActivas == null)
sesionesActivas = new Hashtable();
bool yaConectado = sesionesActivas.ContainsValue(usuario.Login);
if (yaConectado)
{
MostrarMensaje("Ya existe una sesión abierta para esta cuenta.");
return;
}
int resultadoLogin = usuario.Autenticar(txtClave.Text.Trim());
if (resultadoLogin == 0)
{
sesionesActivas[Session.SessionID] = usuario.Login;
Application["SesionesActivas"] = sesionesActivas;
Session["LoginUsuario"] = usuario.Login;
Response.Redirect("Inicio.aspx");
}
else
{
MostrarMensaje("Credenciales inválidas.");
}
}
Limpieza al cerrar sesión: Global.asax
Cuando la sesión expira o se abandona deliberadamente, se elimina la entrada correspondiente del diccionario global.
protected void Session_End(object remitente, EventArgs evt)
{
var sesiones = (Hashtable)Application["SesionesActivas"];
if (sesiones != null && sesiones[Session.SessionID] != null)
{
sesiones.Remove(Session.SessionID);
Application["SesionesActivas"] = sesiones;
}
}
Verificación en cada página
try
{
var sesiones = (Hashtable)Application["SesionesActivas"];
bool enLinea = sesiones != null && sesiones.ContainsValue(Session["LoginUsuario"]);
if (!enLinea)
{
Response.Redirect("Acceso.aspx");
}
}
catch
{
Response.Redirect("Acceso.aspx");
}
Estrategia 3: Desconexión de sesiones anteriores mediante marca temporal
En lugar de bloquear el acceso del usuario que intenta conectarse de nuevo, esta variante expulsa la sesión previa, dejando que la más reciente permanezca activa. Se utiliza MembershipUser.Comment para guardar una marca temporal única en cada inicio de sesión.
Registro de la marca temporal al autenticar
if (Membership.ValidateUser(nombreUsuario, clave))
{
var miembro = Membership.GetUser(nombreUsuario);
string marcaTemporal = Guid.NewGuid().ToString("N");
miembro.Comment = marcaTemporal;
Membership.UpdateUser(miembro);
Session["MarcaSesion"] = marcaTemporal;
FormsAuthentication.RedirectFromLoginPage(nombreUsuario, false);
}
else
{
Response.Write("<script>alert('Credenciales incorrectas');</script>");
}
Comparación de marca temporal en cada solicitud de página
protected void Page_Init(object emisor, EventArgs evt)
{
if (Request.IsAuthenticated)
{
var miembro = Membership.GetUser();
string marcaSesion = Session["MarcaSesion"] as string;
if (marcaSesion != null && miembro != null)
{
if (marcaSesion != miembro.Comment)
{
FormsAuthentication.SignOut();
Response.Write("<script>alert('Su sesión fue cerrada porque la cuenta se utilizó en otro dispositivo.'); location.href='Login.aspx';</script>");
Response.End();
}
}
}
}
El principio es sencillo: cada nuevo inicio de sesión sobrescribe el valor de Comment en la base de datos. La sesión anterior conserva su copia local en Session, que ya no coicnide con la versión almacenada, por lo que se cierra automáticamente.
Detección del cierre inesperado del navegador
Para los casos en que el usuario cierra la ventana sin pulsar el botón de salida, se puede enviar una petición asíncrona mediante el evento beforeunload.
$(window).on('beforeunload', function() {
$.ajax({
url: '/api/cuenta/cerrar',
method: 'POST',
async: false
});
});
Extensión del modelo
Almacenando adicionalmente la dirección IP del cliente junto con el identificador de usuario, es posible comparar los orígenes de conexión y decidir si se permite la nueva sesión o se rechaza, ofreciendo así un control más granular sobre la concurrencia de accesos.