martes, 9 de febrero de 2010

Comprobar usuario y clave usando una base de datos

Introducción:

En este artículo te muestro cómo verificar si el nombre y la clave de un usuario son correctos, pero comprobando esos datos desde una base de datos de SQL Server.
También te dejo un programa que crea la base de datos y la tabla de ejemplo, además de añadir 4 usuarios de prueba, dos de ellos con las claves guardadas de forma normal y los otros dos en los que las claves se han guardado usando SHA1, para que de esa forma no se guarde en la base de datos el texto "normal".
En este artículo, el código de ejemplo es para la versión 2003 de Visual Studio .NET (tanto para Visual Basic como para Visual C#), aunque también es válido para Visual Studio 2005, pero en el caso de Visual Basic, el código de la versión 2005 es algo más simple.

¿Qué hace este ejemplo?

Primero hay que entrar en situación, así que, te explico qué es lo que hace este ejemplo que te voy a mostrar
aquí.
Solo deja iniciar una aplicación después de comprobar que el usuario y la clave introducidas son correctos.
Pero para saber si la clave introducida es correcta, se busca en una base de datos, en este ejemplo, la base de datos es de SQL Server. Aunque te aviso que el código para una base de datos de Access es muy parecido, solo tienes que cambiar los tipos de objetos usados y la cadena de conexión, así que... si no quieres usar una base de datos de SQL Server, tendrás que "currártelo" más...
Para que te hagas una idea de qué es lo que hace este código de ejemplo, te muestro una captura de la ventana de comprobación (ver la figura 1).

El formulario de login (diseño)
Figura 1. El formulario de login (diseño)

No te voy a explicar "los pormenores" de lo que hace la aplicación de ejemplo, ya que en lo que realmente me quiero concentrar es en explicarte cómo comprobar si el nombre y la clave introducida son correctas. Si quieres saber esos "intríngulis", sigue el link al artículo anterior que te comenté antes y así sabrás porqué funciona como funciona.
Veamos qué es lo que pasa cuando se pulsa en el botón Aceptar, es decir, cuando se ha escrito el nombre del usuario y la clave y se va a a comprobar si son correctos esos datos.

Private Sub btnAceptar_Click( _
            ByVal sender As Object, _
            ByVal e As System.EventArgs) _
            Handles btnAceptar.Click
 
    If comprobarUsuario(Me.txtUsuario.Text, Me.txtClave.Text) Then
        Me.DialogResult = DialogResult.OK
    Else
        ' Permitir varios intentos
        veces = veces + 1
        If veces < NumeroIntentos Then
            Label1.Text = "Quedan " & (NumeroIntentos - veces) & " intentos"
            Exit Sub
        End If
        Me.DialogResult = DialogResult.No
    End If
    Hide()
End Sub

Como puedes ver en el texto resaltado, lo que hago es llamar a una función que es la que se encarga de comprobar si ese nombre de usuario y esa clave son datos correctos.
Veamos que hace esa función, ya que lo que se hace en ella es la parte importante de este artículo.
Primero veamos el código y ahora te explico un poco más de lo que ya explican los comentarios que es lo que se hace en esa función.

' Función para comprobar si el acceso es correcto
Private Function comprobarUsuario( _
            ByVal nombre As String, _
            ByVal clave As String) As Boolean
 
    ' Conectar a la base de datos
    Dim cnn As SqlConnection = Nothing
    '
    Try
        ' Conectar a la base de datos de SQL Server
        ' (la cadena debe estar inicializada previamente)
        cnn = New SqlConnection(cadenaCnn)
        cnn.Open()
 
        ' Definir la cadena que vamos a usar para comprobar
        ' si el usuario y el password son correctos.
        ' Utilizo parámetros para evitar inyección de código.
        Dim sel As New System.Text.StringBuilder
 
        ' Usando COUNT(*) nos devuelve el total que coincide
        ' con lo indicado en el WHERE,
        ' por tanto, si la clave y el usuario son correctos,
        ' devolverá 1, sino, devolverá 0
        sel.Append("SELECT COUNT(*) FROM Usuarios ")
        sel.Append("WHERE Nombre = @Nombre AND Clave = @Clave")
        ' Definir el comando que vamos a ejecutar
        Dim cmd As New SqlCommand(sel.ToString, cnn)
        ' Creamos los parámetros
        cmd.Parameters.Add("@Nombre", SqlDbType.NVarChar, 50)
        cmd.Parameters.Add("@Clave", SqlDbType.NVarChar, 40)
        '
        ' Asignamos los valores recibidos como parámetro
        cmd.Parameters("@Nombre").Value = nombre
        cmd.Parameters("@Clave").Value = clave
        '
        ' Ejecutamos la consulta
        ' ExecuteScalar devuelve la primera columna de la primera fila
        ' por tanto, devolverá el número de coincidencias halladas,
        ' que si es 1, quiere decir que el usuario y el password son correctos.
        Dim t As Integer = CInt(cmd.ExecuteScalar())
        ' Cerramos la conexión
        cnn.Close()
        '
        ' Si el valor devuelto es cero
        ' es que no es correcto.
        If t = 0 Then
            Return False
        End If
 
    Catch ex As Exception
        MessageBox.Show("ERROR al conectar a la base de datos: " & vbCrLf & _
                    ex.Message, "Comprobar usuario", MessageBoxButtons.OK, _
                    MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1)
        Return False
    Finally
        If Not cnn Is Nothing Then
            cnn.Dispose()
        End If
    End Try
    '
    ' Si llega aquí es que todo ha ido bien
    Return True
End Function

Como ya te he comentado, lo que hacemos es comprobar si ese usuario y esa clave son correctas.
Tanto el nombre del usuario como la clave, los pasamos como parámetros de la función. Y se buscarán en la base de datos tal y como los pasemos a esa función.
Para no complicarte porqué te digo esto, sigue leyendo y al final te lo aclaro.
Este código "supone" que accedemos a una base de datos que está indicada en la cadena de conexión cadenaCnn, y que en esa base de datos hay una tabla llamada Usuarios que al menos tiene dos campos, uno llamado Nombre que es del tipo nvarchar y que tiene una longitud de 50 caracteres y el otro llamado Clave que también es del tipo nvarchar y con una longitud de 40 caracteres.
Nota:Por supuesto, para tu caso concreto tendrás que cambiar esos valores por los adecuados, pero eso...
¡debes saber hacerlo tú!

Lo que hago es crear una "consulta" (la cadena select, que es el texto resaltado) en la que le digo a la base de
datos que cuente cuantos datos hay que coincidan con lo que le indico. Es decir, que compruebe cuantos usuarios hay que tengan el nombre y la clave que se indican. Como es de suponer, no debemos tener más de un usuario con la misma clave, si ese es el caso... entonces vamos mal...
Si ese usuario y esa clave son correctos, esa consulta devolverá un uno y si no son correctos, devolverá cero. En caso de que devuelva UNO es que es correcto, y si devuelve CERO es que no es correcto.
Para saber cuantos datos devuelve esa consulta, uso el método ExecuteScalar del objeto SqlCommand, y tal como está en el comentario del código, ese método devuelve la primera columna de la primera fila de lo indicado en la cadena de selección, y como lo que debe devolver esa cadena de selección es el número de "datos" que coincidan con lo que hay en la parte WHERE, pues resulta que ese valor es en realidad el total de datos, y como te he dicho hace un párrafo, si devuelve CERO es que no existe esa combinación de nombre/clave, por tanto esos datos no son correctos, por tanto, devolvemos un valor FALSO.

Lo que se comprueba es lo que está en la base de datos

Pues eso, que lo que se comprueba con el código anterior es lo que haya en la base de datos, es decir, en la base de datos el nombre del usuario está tal y como lo indicamos (esto suele ser así), y la clave también está como la indicamos, esto último NO DEBERÍA SER ASÍ, ¿por qué? pues por seguridad, ya que si la clave está en texto "normal", será más fácil "averiguarla". Para saber cómo "complicar" un poco la cosa, sigue leyendo.

Guardar los datos de la clave de forma encriptada

Una solución para que el valor de la clave no esté en texto normal, es encriptándola.
Yo suelo guardar las claves en formato SHA1 al estilo de como lo hace el propio ASP.NET.
¿Qué consigo con esto?
Pues no dejar las claves como texto normal y corriente, sino como una ristra de 40 valores hexadecimales, que no permitan saber que clave es.
¿Cómo encriptar la clave?
Yo tengo una utilidad para generar la clave SHA1 a partir de una cadena, y lo que hago es convertir la clave en el valor correspondiente de la encriptación SHA1 y eso es lo que guardo en la base de datos.
Si quieres hacer esto mismo con tus claves, el código que te he mostrado antes, al menos el del evento Click del botón, no puedes usarlo así, ya que lo que debes comprobar en la base de datos es el valor SHA1 correspondiente a la clave que hayan escrito en la caja de textos de la clave.
Este es el código modificado del evento Click del botón Aceptar, en el que se llama a la función que genera el
valor SHA1 de la clave introducida.

Private Sub btnAceptar_Click( _
            ByVal sender As Object, _
            ByVal e As System.EventArgs) _
            Handles btnAceptar.Click
    ' Convertir a SHA1 la clave introducida
    Dim claveSHA As String = Me.generarClaveSHA1(Me.txtClave.Text)
    If comprobarUsuario(Me.txtUsuario.Text, claveSHA) Then
        Me.DialogResult = DialogResult.OK
    Else
        ' Permitir varios intentos
        veces = veces + 1
        If veces < NumeroIntentos Then
            Label1.Text = "Quedan " & (NumeroIntentos - veces) & " intentos"
            Exit Sub
        End If
        Me.DialogResult = DialogResult.No
    End If
    Hide()
End Sub

Y este es el código de la función
generarClaveSHA1:
Private Function generarClaveSHA1(ByVal nombre As String) As String
    ' Crear una clave SHA1 como la generada por 
    ' FormsAuthentication.HashPasswordForStoringInConfigFile
    ' Adaptada del ejemplo de la ayuda en la descripción de SHA1 (Clase)
    Dim enc As New UTF8Encoding
    Dim data() As Byte = enc.GetBytes(nombre)
    Dim result() As Byte
 
    Dim sha As New SHA1CryptoServiceProvider
    ' This is one implementation of the abstract class SHA1.
    result = sha.ComputeHash(data)
    '
    ' Convertir los valores en hexadecimal
    ' cuando tiene una cifra hay que rellenarlo con cero
    ' para que siempre ocupen dos dígitos.
    Dim sb As New StringBuilder
    For i As Integer = 0 To result.Length - 1
        If result(i) < 16 Then
            sb.Append("0")
        End If
        sb.Append(result(i).ToString("x"))
    Next
    '
    Return sb.ToString.ToUpper
End Function
Para que ese código te funcione debes tener una importación al espacio de nombres System.Security.Cryptography que es donde se define la clase SHA1CryptoServiceProvider y también a System.Text que es donde se definen las clases StringBuilder y UTF8Encoding.

Espero que todo lo aquí dicho te sea de utilidad y te sirva para hacer eso que querías saber y que tantas veces me han preguntado...

No hay comentarios:

Publicar un comentario