INTRODUCCIÓN:
Hola, bienvenido al tercer capítulo del tutorial “Login Completo” en C-Sharp, Visual Basic, SQL Server, WinForms, Programación Orientada a Objetos (POO) y Arquitectura en Capas, con buenas prácticas y estrategias (Nivel intermedio).
En este tutorial aprenderá, como aplicar seguridad y privilegios de usuario , por ejemplo: evitar que alguien intente ingresar al sistema pasando por alto el inicio de sesión de una aplicación, verificar si el usuario que accede a la aplicación realmente es real y esta registrado en la base de datos, ejecutar lineas de código según los permisos y roles del usuario, administrar los privilegios de usuario en la interfaz gráfica. De tal modo que algunas veces, nos resulta ser complicado aplicar seguridad o privilegios de usuario, pero en esta tutorial os enseñaré la manera más sencilla, rápida y de fácil entendimiento. Sin embargo aplicar seguridad y privilegios de usuario depende del tamaño, tipo y plataforma del sistema, que os explicaré durante el artículo. Puede ver este tutorial en Youtube.
En el capítulo anterior realizamos, como validar el usuario y contraseña del usuario al iniciar sesión, mostrar los datos del usuario en el formulario de menú principal, o mostrarlo como un saludo después de iniciar sesión, se creó la opción de cerrar sesión y cambiar de usuario (Te recomiendo verlo, ya que este tutorial estará basado sobre ello).
C#
VB.Net
Puede ver todos los capítulos en los siguientes enlaces:
PRE-TUTORIAL:
En el capitulo anterior; se creó la base de datos de nombre mi Compañía y la tabla Usuarios, para este tutorial, los únicos campos que nos será útil es el campo ID Usuario (IdUser ) y el Cargo (Position), como ejemplo tenemos, administrador, recepcionista y contador.
create database MyCompany create table Users( UserID int identity(1,1) primary key, LoginName nvarchar (100) unique not null, Password nvarchar (100) not null, FirstName nvarchar(100) not null, LastName nvarchar(100) not null, Position nvarchar (100) not null, Email nvarchar(150)not null ) insert into Users values ('admin','admin','Jackson','Collins','Administrator','Support@SystemAll.biz') insert into Users values ('Ben','abc123456','Benjamin','Thompson','Receptionist','BenThompson@MyCompany.com') insert into Users values ('Kathy','abc123456','Kathrine','Smith','Accounting','KathySmith@MyCompany.com')
Generalmente y buena práctica, la contraseña del usuario debe estar cifrada y tener una tabla para los cargos o tipos de usuario y manipularlos por el ID, yo lo hago de esta manera, ya que tendría que trabajar con tablas relacionadas.
También cabe mencionar, que en el capítulo anterior se creó 3 capas de aplicación y una capa de soporte. La capa de presentación, dominio, acceso a datos y la capa de corte transversal, también conocida como capa de soporte o capa común.
La Capa Común de Soporte, es la más importante si se trata de aplicar seguridad al sistema. La seguridad no solamente se aplica a nivel de la capa de presentación, la seguridad está presente en todas las capas, principalmente en las aplicaciones web, ya que es más vulnerable a ataques. Por lo tanto, se creó la capa común y la carpeta cache para el almacenamiento temporal de datos, donde se guarda los datos del usuario que inicia sesión (UserCache.cs / ActiveUser.vb), de esta manera podemos aplicar seguridad en todas las capas a partir de estos datos, principalmente a partir del ID del Usuario, o Cargo del Usuario, dependiendo que tipo de seguridad se quiere lograr. Estos datos son cargados desde el objeto de acceso a datos del usuario (UserDao), y su método de iniciar sesión de la Capa de Acceso a Datos.
Seguridad principal en una aplicación- Ataques maliciosos
La seguridad siempre fue una desafio para los programadores y desarrolladores de software, principalmente para aplicaciones web, ya que son muy vulnerables, se puede acceder a cualquier página mediante localizador de recursos uniforme (URL). Por ejemplo, puedo acceder directamente al formulario o página de menú principal sin pasar por el formulario Login:
http://miDominio.com/mainMenu.php
Para entender mejor, supongamos que un atacante informático logra sobrescribir el formulario de inicio de nuestra aplicación de escritorio, indicando que la aplicación inicie directamente con el formulario de menú principal.
static class Program { [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new FormMainMenu()); } }
Entonces de nada servirá que apliques los privilegios de usuario sin antes aplicar seguridad, el atacante tendrá acceso al formulario de menú principal sin restricciones, ya mayormente los privilegios de usuario se inicializan después de iniciar sesión de acuerdo a los permisos y privilegios de usuario.
Bien, entonces ¿cómo solventamos este problema? la verdad es muy sencillo, con tan solo, la clase estatica UserCache.cs (ActiveUser.vb en caso de VB) de la Capa Común:
public static class UserCache { public static int IdUser { get; set; } public static string LoginName { get; set; } public static string Password { get; set; } public static string FirstName { get; set; } public static string LastName { get; set; } public static string Position { get; set; } public static string Email { get; set; } }
Public Module ActiveUser Public idUser Public loginName Public password Public firstName Public lastName Public position Public email End Module
Bueno, empecemos aplicar la seguridad y privilegios de usuario a nuestra aplicación de inicio de sesión con la arquitectura en capas y programación orientada a objetos, con buenas prácticas y estrategias.
TUTORIAL:
1.-Aplicar seguridad contra ataques (Opcional)
En general: En cualquier clase, método o interfaz gráfica de usuario que desea aplicar seguridad, creamos una condición, donde verificamos si el usuario inició sesión, es decir, verificamos si el cache de usuario tiene datos. Recuerden que los valores de los campos de la clase UserCache.cs se inicializan al ejecutar el método login de la capa de acceso a datos. Por lo tanto, en cualquier método, indicamos que el código se ejecute siempre en cuando que un usuario haya iniciado sesión. PD: Al iniciar sesión el valor de IdUser siempre tendrá un valor mayor o igual a 1, en caso contrario el valor será Cero o Nulo, lo mismo ocurre para los otros campos.
Sin embargo, que pasaría alguien (Suposicion), logra sobrescribir el campo IdUser de la clase UserCache (IdUser=123), en tal sentido, debemos aplicar más seguridad. Entonces realizaremos una consulta a la base de datos si el usuario con tal ID existe realmente, además que concuerde con el nombre de inicio de sesión y contraseña del usuario (3 medidas de seguridad, dudo que el atacante acierte los 3 datos del usuario, pero aun así agregamos esta seguridad, y recuerden que la contraseña del usuario en BD debe estar cifrada para mayor seguridad).
public static class UserCache { public int IdUser=123 public string LoginName="user25" public string Password="admin55" public string FirstName public string LastName public string Position public string Email }
En una aplicación web, para aplicar seguridad, el cache de usuario, es recomendable manejarlo tanto a lado del servidor que puede ser en la capa de soporte y dominio, y a lado del cliente (manejar cache de usuario en el navegador web).
Bien, empecemos a codificar en las capas:
Paso 1: Codificar- Capa de Acceso a Datos
- En la clase UserDao, creamos un método para consultar si el ID USUARIO existe en la base de datos y coincide con el nombre de inicio de sesión y contraseña del usuario.
UserDao.cs / .vb
public class UserDao : ConnectionToSql { public bool existsUser(int id, string loginName, string pass) { using (var connection = GetConnection()) { connection.Open(); using (var command = new SqlCommand()) { command.Connection = connection; command.CommandText = "select *from users where userId=@id and loginName=@loginName and password=@pass"; command.Parameters.AddWithValue("@id", id); command.Parameters.AddWithValue("@loginName", loginName); command.Parameters.AddWithValue("@pass", pass); command.CommandType = CommandType.Text; var reader = command.ExecuteReader(); if (reader.HasRows) return true; else return false; } } } //+public bool Login(string user, string pass)... //Metodo login (Capitulo anterior) }
Public Class UserDao Inherits ConnectionToSql Public Function existsUser(id As Integer, loginName As String, pass As String) As Boolean Using connection = GetConnection() connection.Open() Using command = New SqlCommand() command.Connection = connection command.CommandText = "select *from users where userId=@id and loginName=@loginName and password=@pass" command.Parameters.AddWithValue("@id", id) command.Parameters.AddWithValue("@loginName", loginName) command.Parameters.AddWithValue("@pass", pass) command.CommandType = CommandType.Text Dim reader = command.ExecuteReader() If reader.HasRows Then Return True Else Return False End If End Using End Using End Function "+Public Function Login(user As String, pass As String) As Boolean..." //Método de inicio de sesión, para verlo vea el capítulo anterior End Class
Paso 2: Codificar- Capa de Dominio / Negocio
- En la clase UserModel, creamos un método de nombre Seguridad para inicio de sesión e invocamos el método anterior de UserDao, pero antes debemos de verificar si ID USUARIO del módulo tiene valor, de esta manera evitamos que se realice la consulta a la base de datos con datos vacíos, e inmediatamente retornamos falso para que la aplicación finalice.
UserModel.cs / .vb
public class UserModel { private UserDao userDao = new UserDao(); public bool securityLogin() { if (UserCache.IdUser >= 1) { if (userDao.existsUser(UserCache.IdUser, UserCache.LoginName, UserCache.Password) == true) return true; else return false; } else return false; } //+public bool Login(string user, string pass) //Metodo login (Capitulo anterior) }
Public Class UserModel Dim userDao As New UserDao() Public Function securityLogin() As Boolean If ActiveUser.idUser >= 1 Then If userDao.existsUser(ActiveUser.idUser, ActiveUser.loginName, ActiveUser.password) = True Then Return True Else Return False End If Else Return False End If End Function "+Public Function Login(user As String, pass As String) As Boolean..." //Método de inicio de sesión, para verlo vea el capítulo anterior End Class
Paso 3: Codificar- Capa de Presentación
- En el código del Formulario de Menú Principal, creamos un método para invocar el método anterior creado, donde indicamos que el formulario se muestre siempre en cuando que se haya aprobado la seguridad del modelo de usuario en la capa de dominio, es decir, si este retorna como resultado FALSO, inmediatamente mostramos un error y cerramos el formulario, ya que puede ser un ataque o error de sistema (el método creado security(), invocamos desde el evento load del formulario).
FormMainMenu.cs / .vb
public partial class FormMainMenu : Form { private void FormPrincipal_Load(object sender, EventArgs e) { security(); } private void security() { var userModel = new UserModel(); if (userModel.securityLogin() == false) { MessageBox.Show("Error Fatal, se detectó que está intentando acceder al sistema sin credenciales, por favor inicie sesión e indentifiquese"); Application.Exit(); } } }
Public Class FormPrincipal Private Sub FormPrincipal_Load(sender As Object, e As EventArgs) Handles MyBase.Load security() loadUser() End Sub Private Sub security() Dim user As New UserModel() If user.securityLogin() = False Then 'Codes MessageBox.Show("Error") Application.Exit() End If End Sub //otros métodos de capítulos anteriores "+Private Sub loadUser()..." "+LOGOUT/ APPLICATION EXIT..." End Class
2.- Aplicar seguridad según tipos o cargos de usuario
También está la seguridad en cuanto ejecutar líneas de código de acuerdo al tipo de usuario o cargo del usuario, Por ejemplo, ejecutar cierto código solo para los de cargo recepcionista o contadores, como en este caso, o para los de tipo administrador, invitado. Para optimizar el proceso de la codificación (programación), en la carpeta Cache de la Capa Común, agregamos una estructura de datos para almacenar los cargos del usuario o tipos de usuario (según la aplicación que desarrolles).
Paso 1: Codificar- Capa Común
- En la carpeta cache, agregamos una clase para almacenar los cargos de usuario (Positions.cs), y la convertimos a estructura. En ella, declaramos campos para cada cargo que tenemos en la base de datos e inicializamos los campos manualmente con los valores del campo Position (cargo) de la tabla Usuarios. Si tienes la tabla cargos en la BD, carga los campos con los datos de la tabla mediante una consulta en la capa de acceso a datos.
Positions.cs / .vb
public struct Positions { public const string Administrator = "Administrator"; public const string Receptionist = "Receptionist"; public const string Accounting = "Accounting"; }
Public Structure Positions Public Const administrator = "Administrator" Public Const receptionist = "Receptionist" Public Const accounting = "Accounting" End Structure
Paso 2: Codificar- Capa de Acceso a Datos, Dominio y Presentación
- Supongamos que tenemos cualquier método en cualquier capa donde queremos que tal línea de código solamente se ejecute para tal cargo de usuario. Para ello, simplemente creamos una condición, donde comparamos el cargo de usuario almacenada en el cache de Usuario con el cargo almacenado en la estructura de cargos (Positions.cs) y ejecutamos las líneas o métodos que deseemos que se ejecute para tal cargo, de esta manera:
CualquierClase.cs / .vb
public void anyMethod() { if (UserCache.Position == Positions.Receptionist) { //Lineas o métodos que quieras ejecutar para el cargo recepcionita } if (UserCache.Position == Positions.Accounting) { //Lineas o métodos que quieras ejecutar para el cargo contador } }
Public Sub anyMethod() If ActiveUser.position = Positions.receptionist Then 'Codes End If If ActiveUser.position = Positions.accounting Then 'codes End If End Sub
3.-Aplicar Privilegios/ Permisos de usuario en la Interfaz Gráfica- Formularios
Como indiqué al inicio, la seguridad y privilegios de usuario dependen del tamaño, tipo, y plataforma de un sistema de gestión o aplicación. Pero la lógica es el mismo de los anteriores ejemplos. Aquí os mostraré alguno de ellos.
Caso 1: Sistemas de Gestión o Aplicaciones de menor Tamaño.
Para aplicaciones pequeñas, como sistemas de software para una tienda o minimarket, donde no existen áreas; generalmente se suele usar un solo Formulario de Menú Principal, donde se aplica los privilegios de usuario según el cargo del usuario, al igual en los formularios hijos.
- En el código del Formulario de Menú Principal, creamos un método para administrar los permisos de usuario, donde desactivaremos los botones según los permisos y cargos de usuario (invocamos el método desde el evento load del formulario).
public partial class FormMainMenu : Form { private void FormPrincipal_Load(object sender, EventArgs e) { ManagePermissions(); security(); } private void ManagePermissions() { if (UserCache.Position == Positions.Accounting) { btnPacient.Enabled = false; btnClinicalHistory.Enabled = false; } if (UserCache.Position == Positions.Receptionist) { Button2.Enabled = false; } if (UserCache.Position == Positions.Administrator) { //Codes } } //+private void security()... //Metodo anterior }
Public Class FormPrincipal Private Sub FormPrincipal_Load(sender As Object, e As EventArgs) Handles MyBase.Load managePermissions() security() End Sub Private Sub managePermissions() If ActiveUser.position = Positions.receptionist Then btnChart.Enabled = False btnSetting.Enabled = False End If If ActiveUser.position = Positions.accounting Then btnPacient.Enabled = False btnClinicalHistory.Enabled = False End If 'more End Sub End Class
- Para Formulario Hijos realizamos lo mismo:
public class Form1 { private void Form1_Load(object sender, EventArgs e) { managePermissions(); } private void managePermissions() { if (ActiveUser.position == Positions.receptionist) { btnAdd.Enabled = false; btnEdit.Enabled = false; btnRemove.Enabled = false; } } }
Imports Common Public Class Form1 Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load managePermissions() End Sub Private Sub managePermissions() If ActiveUser.position = Positions.receptionist Then btnAdd.Enabled = False btnEdit.Enabled = False btnRemove.Enabled = False End If 'more End Sub End Class
Caso 2: Sistemas de Gestión o Aplicaciones de tamaño Medio
Para aplicaciones de tamaño medio, como sistemas de software para una clínica, donde existen muchas áreas, como: administración y finanzas, dirección médica, seguridad y salud, servicios generales, recursos humanos, etc., en este tipo de aplicación, administrar los permisos de usuario en un solo formulario de menú principal es imposible. Para estos casos, tenemos que utilizar subsistemas para cada área. Podemos crear los subsistemas solo a nivel de la capa de presentación, y al momento de iniciar sesión, dirigir al formulario de menú principal de un área, de acuerdo al área que pertenece el usuario, también cabe mencionar que en este caso, puedes usar áreas y cargos de usuario, sin embargo, depende mucho del tipo de sistema y la empresa, los ejemplos a partir de aquí, son solo referencias, tú debes aplicar de acuerdo a las políticas y organigrama de la empresa (Recuerden que un sistema de software es un reflejo digital de la empresa en físico). Bien, iniciemos como se haría esto.
- En capa de Presentación, agregamos una carpeta para cada área, cada área tendrá su propio formulario de menú principal y formularios hijos, también puede haber formularios comunes para todas las áreas.
- En el Formulario Login, direccionamos al usuario, al menú principal del área al que pertenece. “Supongamos que el cargo contador pertenece al área comercial, y el cargo administrador pertenece al área de gerencia general”.
FormLogin.cs / .vb
public partial class FormLogin : Form { private void btnlogin_Click(object sender, EventArgs e) { if (txtuser.Text != "Username" && txtuser.TextLength>2) { if (txtpass.Text != "Password") { UserModel user = new UserModel(); var validLogin = user.LoginUser(txtuser.Text, txtpass.Text); if (validLogin == true) { if (UserCache.Position == Positions.Administrator) { FormMainMenu mainMenu = new FormMainMenu(); MessageBox.Show("Welcome " + UserCache.FirstName + ", " + UserCache.LastName); mainMenu.Show(); mainMenu.FormClosed += Logout; this.Hide(); } if (UserCache.Position == Positions.Accounting) { Commercial.MainMenuComer mainMenu = new Commercial.MainMenuComer(); MessageBox.Show("Welcome " + UserCache.FirstName + ", " + UserCache.LastName); mainMenu.Show(); mainMenu.FormClosed += Logout; this.Hide(); } } else { msgError("Incorrect username or password entered. \n Please try again."); txtpass.Text ="Password"; txtpass.UseSystemPasswordChar = false; txtuser.Focus(); } } else msgError("Please enter password."); } else msgError("Please enter username."); } }
Imports Domain Public Class FormLogin Private Sub btnLogin_Click(sender As Object, e As EventArgs) Handles btnLogin.Click Dim userModel As New UserModel() Dim validLogin = userModel.Login(txtUser.Text, txtPass.Text) If validLogin = True Then If ActiveUser.position = Positions.accounting Then Dim frm2 As New MainMenuCommercial() frm2.Show() AddHandler frm2.FormClosed, AddressOf Me.Logout Me.Hide() End If If ActiveUser.position = Positions.administrator Then Dim frm As New MainMenuManagement() frm.Show() AddHandler frm.FormClosed, AddressOf Me.Logout Me.Hide() End If Else MessageBox.Show("Incorrect username or password entered." + vbNewLine + "Please try again.") txtPass.Clear() txtPass.Focus() End If End Sub End Class
Caso 3: Sistemas de gestión o Aplicaciones gran tamaño
Para aplicaciones gran tamaño, el ejemplo anterior, esto es muy difícil de mantener, principalmente en las capas. Entonces podríamos crear grupos de acuerdo a las áreas. Cada área tendría sus propias capas, de la siguiente manera:
Solución MySystem
Gerencia
CapaPresentation
CapaDominio
CapaAccesoDatos
Commercial
CapaPresentation
CapaDominio
CapaAccesoDatos
Logística
CapaPresentation
CapaDominio
CapaAccesoDatos
Al igual que el ejemplo anterior, desde el formulario login, podemos dirigir al usuario al área al que pertenece. Sin embargo, hay ocasiones que el código es difícil de mantener, en ese caso podemos utilizar microServicios orientada a la DDD.
Eso es todo 🙂
Puede ver este tutorial en Youtube:
C#
Visual Basic
DESCARGAS:
Descargar Base de Datos
Descargar Proyecto Login Completo + CRUD
La descarga del proyecto (No es gratis) incluye las funciones realizadas en los 6 capítulos del proyecto LOGIN COMPLETO EN C# o LOGIN COMPLETO EN VB.NET, sin embargo, el proyecto tiene un código fuente totalmente modernizado y optimizado, además de incluir CRUD completo con manejo de imágenes, validación de datos duplicados, entre otros. Todo ello con SQL Server, WinForm, Programación Orientada a Objetos (POO) y Arquitectura en Capas, con buenas prácticas y estrategias, tanto de nivel intermedio o avanzado. Puedes descargar las versiones de demostración y obtener los proyectos desde lo botones a continuación:
C# (C-Sharp)
VB.NET (Visual Basic)
Los comentarios están cerrados.