Introducción
Este artículo intentara plantear la forma en que se puede crear un login que pueda ser usado tanto para un entorno Web, como para uno WinForms.
Se vera como mediante la definición de funcionalidad en una capa de Servicio el código de autenticación puede ser reutilizado, en realidad este nombre que le he dado a la capa podría llevar cualquier otro nombre, podrías ser directo la capa de Datos, o un Repository, la idea es mantener la técnica que permita reutilizar la funcionalidad.
Algo particular que encontraran en el ejemplo esta referido al password no se guarda de forma plana en la db, sino que por motivos de seguridad se aplica una función de hash.
La misma tiene una particularidad interesante, ya que el algoritmo solo se puede aplicar en un solo sentido, siempre obteniendo el hash de un texto o mensaje, pero nunca de un valor hash obtener el mensaje original.
Al aplicar el algoritmo hash a un mensaje, el resultado será siempre el mismo, por lo tanto esta técnica utilizada en el login permite que el password este siempre seguro. En el ejemplo verán como en el momento de login se aplica el algoritmo al la entrada del usuario y este valor se busca como password en la tabla, al asegurarse que un mismo texto produce el mismo resultado de hash, si se ingreso correctamente usuario y password este debe coincidí con el presente en la base de datos.
Definición de la capa de Servicio
El primer paso que explicaremos en el articulo es el código común que será reutilizado tanto por la capa web como winform, se trata de la denominada capa de servicio.
La misma básicamente se compone de una fachada estática, con métodos para autenticar o insertar un usuario, por supuesto la funcionalidad adicional para actualizar no se incluyo en el ejemplo, pero podrías agregarse si fuera necesario.
public static bool Autenticar(string usuario, string password)
{
string sql = @"SELECT COUNT(*)
FROM Usuarios
WHERE NombreLogin = @nombre AND Password = @password";
using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
{
conn.Open();
SqlCommand command = new SqlCommand(sql, conn);
command.Parameters.AddWithValue("@nombre", usuario);
string hash = Helper.EncodePassword(string.Concat(usuario, password));
command.Parameters.AddWithValue("@password", hash);
int count = Convert.ToInt32(command.ExecuteScalar());
if (count == 0)
return false;
else
return true;
}
}
public static UsuarioEntity Insert(string nombre, string apellido, string nombreLogin, string password)
{
UsuarioEntity usuario = new UsuarioEntity();
usuario.Nombre = nombre;
usuario.Apellido = apellido;
usuario.NombreLogin = nombreLogin;
usuario.Password = password;
return Insert(usuario);
}
public static UsuarioEntity Insert(UsuarioEntity usuario)
{
string sql = @"INSERT INTO Usuarios (
Nombre
,Apellido
,NombreLogin
,Password)
VALUES (
@Nombre,
@Apellido,
@NombreLogin,
@Password)
SELECT SCOPE_IDENTITY()";
using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
{
SqlCommand command = new SqlCommand(sql, conn);
command.Parameters.AddWithValue("Nombre", usuario.Nombre);
command.Parameters.AddWithValue("Apellido", usuario.Apellido);
command.Parameters.AddWithValue("NombreLogin", usuario.NombreLogin);
string password = Helper.EncodePassword(string.Concat(usuario.NombreLogin, usuario.Password));
command.Parameters.AddWithValue("Password", password);
conn.Open();
usuario.Id = Convert.ToInt32(command.ExecuteScalar());
return usuario;
}
}
El método importante en estos dos métodos es el siguiente:
string password = Helper.EncodePassword(string.Concat(usuario.NombreLogin, usuario.Password));
Esta línea es la que arma el password real que se salvara en el campo de la tabla, como se observa para una seguridad aun mayor se une el nombre del usuario y el password, pasándolos luego por la función que aplica el hash.
internal class Helper
{
public static string EncodePassword(string originalPassword)
{
SHA1 sha1 = new SHA1CryptoServiceProvider();
byte[] inputBytes = (new UnicodeEncoding()).GetBytes(originalPassword);
byte[] hash = sha1.ComputeHash(inputBytes);
return Convert.ToBase64String(hash);
}
}
En el método Autenticar() como se había comentado no se obtiene el password que originalmente el usuario tienen, sino que se aplica el hash sobre los valores ingresados al momento de realizar el login, ejecutando el query con esta información como filtro.
Si el usuario y password ingresados en la autenticación son los correctos al aplicar el hash retornara la misma cadena que se tiene en la tabla, por lo tanto al ejecutar el query debería devolver el registro del usuario.
Autenticación desde una aplicación Winform
En el método Main() del archivo Program.cs se encontrara la llamada a el formulario que será responsable de al autenticación.
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
frmLogin frm = new frmLogin();
frm.ShowDialog();
if(frm.DialogResult == DialogResult.OK)
Application.Run(new frmPrincipal());
}
}
El formulario de login (frmLogin.cs), tendrá un código muy simple, solo tomara el usuario y password invocara al servicio y según el resultado retornara la respuesta al método Main() para que se abra o no el formulario principal.
private void btnAceptar_Click(object sender, EventArgs e)
{
string nombre = txtNombre.Text;
string password = txtPassword.Text;
if (LoginService.Autenticar(nombre, password))
this.DialogResult = DialogResult.OK;
else
this.DialogResult = DialogResult.Abort;
}
private void btnCancelar_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.Cancel;
}
Adicionalmente se agrego un formulario para la creación de nuevos usuarios, si bien en este no se ha programado la edición completa del usuario, o sea toda la funcionalidad de ABM, el Insert permitirá analizar como se registra un nuevo usuario y se guarda su password.
private void btnAceptar_Click(object sender, EventArgs e)
{
UsuarioEntity usuario = new UsuarioEntity();
usuario.Nombre = txtUsuario.Text;
usuario.Apellido = txtApellido.Text;
usuario.NombreLogin= txtNombreLogin.Text;
usuario.Password = txtPassword.Text;
usuario = LoginService.Insert(usuario);
MessageBox.Show(string.Format("Se ha creado el usuario, ID: {0}", usuario.Id));
}
Nuevamente gracias a la funcionalidad encapsulada del Servicio, el código del formulario es muy simple.
Autenticación desde una aplicación Web
La autenticación en un entorno web es algo distinta a una aplicación Windows, en este caso se ha configurado en el web.config, las líneas necesarias para que todo el sitio este bajo una autenticación por medio de “Forms”
<authentication mode="Forms">
<forms name="appNameAuth" path="/" loginUrl="frmLogin.aspx" defaultUrl="Default.aspx" protection="All" />
</authentication>
<authorization>
<deny users="?" />
</authorization>
En el archivo de configuración del sitio se especifica cual será la pagina por defecto y cual la de login, de esta forma si no hay usuario autenticado, solo el sitio redirección a estas url.
El formulario web frmLogin.aspx tiene un código muy simple:
protected void ProcessLogin(object sender, EventArgs e)
{
if (LoginService.Autenticar(txtUser.Text, txtPassword.Text))
{
FormsAuthentication.RedirectFromLoginPage(txtUser.Text, chkPersistLogin.Checked);
}
else
ErrorMessage.InnerHtml = "<b>Usuario o contraseña incorrectos...</b> por favor re-ingrese las credenciales...";
}
Como se observa luego de la autenticación se hace uso de los métodos provistos por .net en el namespace System.Web.Security, para trabajar con al seguridad.
Aquí se esta indicando el nombre del usuario que paso la validación, y esto hace que solo el sitio se redireccione a la pagina marcada por defecto en el web.config.
En la pagina principal del sitio se ha agregado algo de código para mostrar el nombre del usuario autenticado, y dar la posibilidad de un logout.
protected void Page_Load(object sender, EventArgs e)
{
Label1.Text = string.Format("Bienvenido al Sistema: {0}", Thread.CurrentPrincipal.Identity.Name);
}
protected void Menu1_MenuItemClick(object sender, MenuEventArgs e)
{
if (Menu1.SelectedValue == "Salir")
{
FormsAuthentication.SignOut();
FormsAuthentication.RedirectToLoginPage();
}
}
También se ha creado una pagina para crear nuevos usuarios (Usuarios.aspx)
protected void btnAceptar_Click(object sender, EventArgs e)
{
UsuarioEntity usuario = new UsuarioEntity();
usuario.Nombre = txtNombre.Text;
usuario.Apellido = txtApellido.Text;
usuario.NombreLogin = txtLogin.Text;
usuario.Password = txtPassword.Text;
usuario = LoginService.Insert(usuario);
ClearControls();
lblMessage.InnerHtml = string.Format("Se ha creado el usuario, ID: {0}", usuario.Id);
}
protected void btnCancelar_Click(object sender, EventArgs e)
{
Response.Redirect("~/Default.aspx");
}
private void ClearControls()
{
txtNombre.Text="";
txtApellido.Text="";
txtLogin.Text="";
txtPassword.Text="";
}
Código de ejemplo
Como requerimiento será necesario contar con al menos Sql Server Express 2008, para que pueda ejecutarse la aplicación.
La tabla de usuario ya posee un registro para ingresar la primera vez, la información es la siguiente:
Usuario: admin
Password: pass123
Para no tener dos base de datos iguales, se agrego un Build Event en el proyecto web para que copie la db desde el proyecto de Class Library a la carpeta App_Data del sitio Web.
Puede que a veces si se compila repetida veces la compilación lance un error al intentar copiar la db a la carpeta App_Data, cuya operación no pueda hacerlo porque el servicio de sql server la tenga loqueada, en ese caso simplemente ejecuten a pesar de este mensaje de error, ya que funcionara sin problemas.