jueves, 27 de agosto de 2020

Flask 15. Construyendo un Login de usuarios con Flask-login, Flask-WTForms, Flask-SQLALchemy y Flask-Bootstrap

 Login Register Window - Free image on Pixabay


Anteriormente Flask 14. Usando una base de datos en Flask, Flask-SQLAlchemy.


Login de usuarios con Flask-login, Flask-WTForms, Flask-SQLALchemy y Flask-Bootstrap


En este post vamos a centrarnos en construir, a través de un ejemplo práctico, un sistema de login para páginas web. Crearemos una página inicial accesible al todo el mundo. Si no estás registrado lo vas a poder hacer y posteriormente podrás iniciar sesión en el sistema. Habrá determinadas vistas a las que no podrás acceder si no estas logeado.

Prácticamente en casi todo el cápitulo vamos a utilizar cosas ya vistas en temas anteriores y solamente casi al final introduciremos algo no visto aún, como es la librería FLask-Login.

Sin más empezamos.

1.- Construyendo el Core de la Aplicación.


Crearemos un directorio de trabajo en donde vamos a colocar los directorios propios de Flask para plantillas y recursos estáticos y uno más donde irá posteriormente la base de datos que contendrá la información de los usuarios de la aplicación. Creamos también el archivo que contendrá el código de la aplicación principal que llamaré inicio.py.

Directorio de Trabajo
    |_ /static
    |_ /templates
    |_ /base_de_datos
    |_ inicio.py


Como este capítulo va a ser más largo de lo habitual sería conveniente que cada pocos pasos comprobarás que la aplicación va funcionando correctamente. 


Dicho lo cual, continuamos escribiendo el código básico que hará que funcione el programa. Definiremos las funciones para cada una de las rutas que tendrá el programa y una vista final para gestionar los errores provocados cuando el usuario entra en una ruta que no existe.

Cargaremos las opciones de configuración del programa a través de un archivo externo (línea 5).

archivo inicio.py





Vamos a crear ese archivo de configuración (config.py)



Para no perdernos, cada vez que cree un archivo voy a mostrar el aspecto que irá teniendo el directorio de trabajo:

Directorio de Trabajo
    |_ /static
    |_ /templates
    |_ /base_de_datos
    |_ inicio.py                                                                                       
    |_ config.py


Comentamos brevemente el archivo config.py:

- Empezamos cargando el módulo os.path que nos permitirá saber cual es la ruta de nuestro directorio de trabajo independientemente del sistema operativo que estemos utilizando. Algo que luego necesitaremos para la base de datos.

- Tanto los formularios como posteriormente Flask-Login (que va a hacer uso de la sesión para la autentificación de los usuarios) van a necesitar que creemos un token de seguridad. Como ya hemos explicado ese token lo eliges tú, pero asegurate de que sea los suficientemente seguro. (más información aquí)

- Creamos la variable PWD (de personal work directory) que recogerá la ruta de trabajo de nuestra aplicación.

- con DEBUG = True podremos depurar código sin necesidad de reiniciar el servidor.

- SQLALCHEMY_DATABASE_URI, como ya vimos su contenido va a depender de que tipo de base de datos vamos a utilizar y de donde estará el archivo al que conectaremos la base de datos.

- SQLALCHEMY_TRACK_MODIFICATIONS - para que nos nos salgan los avisos de SQLAlchemy cuando haga operaciones en la base de datos.

En este archivo de configuración también podríamos modificar otras opciones como el puerto que usa el servidor (PORT) o cambiar los directorios por defecto de flask (templates -template_folder - o stactic -static_folder-).


Antes de continuar, supongo que si estas siguiendo el curso desde el principio ya tienes instaladas las librerías que necesitaremos (Flask-Bootstrap, Flask-Forms, Flask-SQLAlchemy). La única que no tenemos aun instalada es Flask-Login que lo haremos casi al final de este capítulo. Si no las tienes instaladas deberás hacerlo si quieres que el programa funcione.


2.- Empezando a utilizar Flask-Bootstrap.


Si aún no lo tienes instalado, instala la extensión Flask-Bootstrap. Puedes encontrar más información aquí.

Crearemos la página con la que va a comenzar la aplicación, nuestra página de inicio. (pagina_inicial.html) dentro del directorio templates.

Directorio de Trabajo
    |_ /static
    |_ /templates
    |       |_ pagina_inicial.html 
    |_ /base_de_datos
    |_ inicio.py                                                                             
    |_ config.py


Comenzamos usando la plantilla base que nos proporciona bootstrap con el comando extends.

Usamos {% block title %}  y {% block content %} para colocar el título de la página y su contenido. Utilizamos el código de CSS que nos facilita Bootstrap para crear una barra de navegación y un texto en la pantalla.

En la barra de navegación nos aparecerán enlaces para cada una de las vistas que vamos a utilizar (login, signup y logout).

Para que esto funcione en nuestro archivo principal tendrás que: 

- importar el método render_template para renderizar la plantilla.

- importar Bootstrap y aplicarlo a nuestra aplicación (app).




Ejecuta la aplicación y tendrá que aparecerte algo parecido a esto:


3.- Diseño de los formularios de las vistas "login" y "signup".


La idea en un Login de cualquier página web es doble. Primero tienes que registrarte (vista signup) y luego iniciar sesión (vista login) para poder acceder a cierto contenido que si no estás registrado no vas a poder ver. Para obtener los datos vamos a necesitar construir formularios para ambas vistas. Queremos que nos quede algo así.

Como quedará el Formulario para el login.


Como quedará el Formulario para el registro correspondiente a la vista signup. 





Para diseñar los formularios lo primero que haremos será crear el archivo que contendrá el diseño de los mismos (forms.py) donde importaremos las bibliotecas que vamos a utilizar y crearemos las clases para cada uno de ellos.

Directorio de Trabajo
    |_ /static
    |_ /templates
    |       |_ pagina_inicial.html 
    |_ /base_de_datos
    |_ inicio.py                                                                             
    |_ config.py                           
    |_ forms.py  


El código del archivo forms.py es el que sigue. 



Corrección de errores:        El tamaño máximo de la contraseña será de 50 caracteres y no de 80 como pone la imagen.    

 


Estamos definiendo dos clases, Formulario_de_Login y Formulario_de_Registro. Ambas heredan de FlaskForm.

Los campos de ambas son casi los mismos. 

La clase Formulario_de_Login tiene tres registros: nombre_usuario, contrasena y recuerdame. No hay mucho que comentar sobre los campos, solamente decir que el campo recuerdame lo usaremos para dar la posibilidad al usuario de mantener abierta la sesión incluso después de haber cerrado el navegador. Todos tienen unos validadores para definir si son campos requeridos, la longitud máxima y mínima de los mismos etc.

La clase Formulario_de_Registro tendrá los campos nombre_usuario, correo_electronico, contrasena y contrasena2 necesarios para registrar a nuestros usuarios. El motivo de usar contrasena y contrasena2 es para asegúrarnos de que el usuario introduce la contraseña que desea, al obligarle a introducirla dos veces y comprobar que en ambos campos es la misma (EqualTo).

Además esta clase tiene una serie de peculiaridades.

Para empezar hemos tenido que usar import inicio y también de wtforms.validators ValidationError.

He usado import inicio para poder usar la clase Usuario definida en la misma, con la finalidad de poder realizar consultas a nuestra base de datos. No he usado from inicio import usuario porque utilizando esta estructura se produciría una llamada cíclica entre inicio y forms que pararía el programa.

La razón de importar ValidationError es poder realizar validadores creados por nosotros. Como ves hemos añadido dos nuevos métodos a esta clase que son validate_nombre_usuario() y validate_correo_electronico(). Cuando añades cualquier método que coincida con el patrón validate_<nombre_del_campo>  WTForms los considera como validadores personalizados y los utiliza a mayores de los que hayamos utilizado en la definición de los campos. 

En este caso, nos queremos asegurar de que el nombre de usuario y la dirección de correo electrónico no estén ya en la base de datos, por lo que estos dos métodos emiten consultas a la base de datos esperando que no haya resultados. En el caso de que los haya se generará un error de validación (ValidationError). El mensaje incluido como argumento en la excepción será el mensaje que se mostrará junto al campo para que lo vea el usuario.


Una vez creada las clases, creamos las plantillas de cada uno de los formularios. También vamos a crear un archivo de CSS para poner un fondo al body personalizado.

Directorio de Trabajo
    |_ /static
    |_     |_ mystyle.css  
    |_ /templates
    |       |_ pagina_inicial.html 
    |       |_ signup.html 
    |       |_ login.html
    |_ /base_de_datos
    |_ inicio.py                                                                             
    |_ config.py                           
    |_ form.py 


El archivo css solo va a contener un fondo degradado para la etiqueta <body> para que no sea completamente blanca.

mystyle.css

body {

    background: #D3CCE3;  /* fallback for old browsers */

    background: -webkit-linear-gradient(to right, #E9E4F0, #D3CCE3);  /* Chrome 10-25, Safari 5.1-6 */

    background: linear-gradient(to right, #E9E4F0, #D3CCE3); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */

 
Empezamos con la plantilla donde se van a registrar los datos de los usuarios previamente al login. Como vimos antes esta tendrá cuatro campos: el nombre del usuario, el correo electrónico, la contraseña y el verificador de la contraseña.

signup.html
{% extends "bootstrap/base.html" %}
<!-- importamos la extensión de bootstrap para formularios -->
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Signup{% endblock%}

<!--super es para no sobreescribir los estilos propios de bootstrap y si 
quieres usar un css propio-->
{% block styles %}
{{super()}}
<link rel="stylesheet" href="{{url_for('.static', filename='mystyle.css')}}">
{% endblock %}

{% block content %}
<div class="container" style="width: 27%;">
    <div class="row">
        <form action="{{url_for('signup')}}" method="POST">
            <h2 class="form-signin-heading">Crear Cuenta</h2>
            <br />
            {{form.csrf_token}}
            <!-- Los 4 siguientes líneas se pueden sustituir por una sola instrucción
            {{wtf.quick_form(form)}} pero habria que definir en el formulario un campo del 
            tipo SubmitField para que funcionase en vez del boton de envio -->
            {{wtf.form_field(form.nombre_usuario)}}
            {{wtf.form_field(form.correo_electronico)}}
            {{wtf.form_field(form.contrasena)}}
            {{wtf.form_field(form.contrasena2)}}
            <button class="btn btn-lg btn-primary btn-block" type="submit">Sign up</button>
            <br />
        </form>
        <a href="{{url_for('home')}}">Volver</a>
    </div>
</div>

{% endblock %}

La principal novedad es que vamos a usar la plantilla de Bootstrap/wtf.html que nos va a permitir crear el diseño del formulario de registro de manera muy sencilla. Primeramente importamos la citada plantilla usando la siguiente instrucción:

{% import "bootstrap/wtf.html" as wtf %}

Para luego diseñar el formulario siguiendo la siguiente estructura de los campos en el orden que queramos ponerlos:
<form action="/dirección ruta" method="post">
  {{ form.hidden_tag() o form.csrf_token()}}
  {{wtf.form_field(form.nombre_usuario)}}
  {{wtf.form_field(form.correo_electronico)}}
  {{wtf.form_field(form.contrasena)}}
  {{wtf.form_field(form.contrasena2)}}
  <button class="btn btn-lg btn-primary btn-block" type="submit">Sign up</button>}
</form>
O de forma más simplificada podrías, como pone el manual, teclear:
<form action="/dirección ruta" method="post">
    {{wtf.quick_form(form)}}
</form>

Con lo que ahorras aún más código ya que sustituyes todas las declaraciones de los campos por una sola línea que hace lo mismo. Lo único que con este sistema los campos aparecerán uno debajo de otro en el orden que los hayas definido en la clase e importante para que funcione, tienes que crear un campo del tipo SubmitField en vez de usar un botón para enviar los datos como he hecho en el ejemplo.


Solamente con estas pocas líneas de código ya tenemos creado el formulario. No hace falta especificar ni etiquetas ni campos, ni diseño css de los mismos, ya se encarga bootstrap por nosotros. Los campos aparecen en el orden que los hayamos puesto, con su etiqueta encima  y abajo el botón de enviar, tal como puedes ver en las imágenes que hay un poco más arriba. Todo ya con el diseño típico de cualquier formulario de registro que puedes ver en internet. 

También ponemos un link al final para volver a la página principal en caso de que el usuario no quiera registrase.

Lo mismo hacemos con el formulario que nos va a permitir hacer el login. 

login.html

{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}
Login
{% endblock %}

<!--super es para no sobreescribir los estilos propios de bootstrap y si 
quieres usar un css propio-->
{% block styles %}
{{super()}}
<link rel="stylesheet"
      href="{{url_for('.static', filename='mystyle.css')}}">
{% endblock %}

{% block content %}
<div class="container" style="width:27%">
  <div class="row">
    <form class="form-signin" method="POST" action="{{url_for('login')}}">
      <h2 class="form-signin-heading">Iniciar Sesión</h2>
      <br />
      {{ form.csrf_token}}
      {{ wtf.form_field(form.nombre_usuario) }}
      {{ wtf.form_field(form.contrasena) }}
      {{ wtf.form_field(form.recuerdame) }}
      <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
    </form><br />
    <div>¿No tienes cuenta? <a href="{{ url_for('signup') }}">Regístrate</a></div>
    <br />
    <a href="{{url_for('home')}}">Volver</a>
  </div>
</div> <!-- /container -->

{% endblock %}

Añadimos un enlace para que si el usuario no esta aún registrado lo pueda hacer y otro para volver a la pantalla de inicio si no quiere realizar el proceso.

Puedes repasar como se creaban formularios con FLask-WTF aqui.

Para que esto funcione tenemos que realizar algunas modificaciones en el archivo inicio.py





Implicitamente importamos las clases Formulario_de_Login y Formulario_de_Registro al importar forms

Modificamos los decoradores para que acepten tanto el método GET como el POST. Cuando se entre por primera vez, al pulsar el enlace al Login o Sign Up de la página principal, lo haremos con el método GET con lo que se renderizará la plantilla login.html o signup.html y le pasaremos el diseño del formulario (clase form). Si cumplimentas el formulario y lo envías, de momento nuestro programa te dará un error. Pero luego lo complementaremos, no te preocupes.

4.- Creación de la base de datos de los Usuarios.


Ahora nos toca crear la base de datos en la que almacenaremos los datos introducidos por cada usuario. Su nombre de usuario, correo electrónico y contraseña.

Solamente vamos a hacer unas pequeñas modificaciones al archivo inicio.py

from flask import Flask, render_template
from flask_bootstrap import Bootstrap
import forms
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config.from_object('config')
Bootstrap(app)
db = SQLAlchemy(app)

# Base de datos sqlite de usuarios

class Usuario(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    nombre_usuario = db.Column(db.String(15), unique=True)
    correo_electronico = db.Column(db.String(50), unique=True)
    contrasena = db.Column(db.String(80))
Correción de errores - la contraseña tendrá un tamaño de 94 caracteres y no de 80 como pone la imagen anterior.


Importamos SQLAlchemy desde la biblioteca flask-sqlalchemy y lo integramos en nuestra aplicación con db = SQLAlchemy(app).

Definimos la base de datos que vamos a usar a través de nuestra clase Usuario. Definimos las diferentes columnas o campos de que estará compuesta:
  1. id. Valor numérico que asignará la base de datos automáticamente a cada registro o usuario en este caso.
  2. nombre_usuario. Campo de tipo String con un tamaño máximo de 15 caracteres y que será único. No podrá haber dos usuarios con el mismo nombre.
  3. correo_electronico. Campo de tipo String con un tamaño maximo de 50 caracteres y que también será único. Se supone que dos usuarios distintos no pueden tener el mismo correo electrónico.
  4. contrasena. Sin ñ para que no haya problema con el código. De tipo String como los anteriores y con un tamaño máximo de 94 caracteres. Lo he puesto así ya que la contraseña posteriormente estará codificada con un algoritmo sha-256 que ocupará 80 caracteres más 14 de la cabecera, lo que nos da 94 caracteres a guardar en total.
Recuerda que cuando creamos las clases de formulario en el archivo form.py ya especificamos que todos estos campo serían obligatorios de introducir (InputRequired)

Ahora viene el momento mágico. Date cuenta que hasta el momento no tenemos ningún archivo de base de datos como tal. Si miras en el directorio base_de_datos este, ¡Esta vacío!. Únicamente habíamos definido donde iba a estar cuando en el archivo config.py definimos su dirección de conexión o URI con:

SQLALCHEMY_DATABASE_URI = "sqlite:///{}/base_de_datos/dbase.db".format(PWD)

Para crearlo "físicamente". Entramos en nuestro directorio de trabajo desde el terminal, iniciamos python y como ya explique en el capítulo anterior tecleamos:
hongoCh@ubuntu:~/Escritorio/nFlask/15$ python
Python 3.6.9 (default, Jul 17 2020, 12:50:27) 
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from inicio import db
>>> db.create_all()
¡Voilá! Si miras en el directorio ya esta ahí nuestra base de datos con su tabla "usuario" y sus campos respectivos "nombre_usuario", "correo_electronico" y "contrasena". Ahora ya la podemos utilizar en las vistas signup y login para guardar y consultar los datos de cada usuario.

Directorio de Trabajo
    |_ /static
    |_     |_ mystyle.css  
    |_ /templates
    |       |_ pagina_inicial.html 
    |       |_ signup.html 
    |       |_ login.html
    |_ /base_de_datos
    |_      |_ dbase.db
    |_ form.py 
    |_ inicio.py                                                                             
    |_ config.py                           
    

Siguiendo una lógica vamos primero a ver lo que pasa cuando se envía un formulario ya validado a la vista /signup. 

Lo que tenemos que hacer es usar los datos que nos pasa el usuario en el formulario y guardarlos en la base de datos para su posterior uso. 

Ve al archivo inicio.py y busca el código de la vista signup. Ahora busca la instrucción "pass" y vamos a sustituirla por un nuevo código.  Este creará un nuevo objeto, llamado nuevo_usuario, que pertenece a la clase Usuario y al que pasamos las variables nombre_usuario, correo_electronico y contrasena con los datos obtenidos del formulario de registro).

Intentamos guardarlo en la base de datos y si tenemos éxito se mostrará un mensaje de confirmación y si hay algún error al crear el nuevo registro el programa nos avisará.

Este es el código de la vista:

@app.route('/signup/', methods=['GET', 'POST'])
def signup():
    form = forms.Formulario_de_Registro()
    if form.validate_on_submit():
        nuevo_usuario = Usuario(
            nombre_usuario=form.nombre_usuario.data,
            correo_electronico=form.correo_electronico.data,
            contrasena=form.contrasena.data)      
        # No añadimos id porque lo creara automáticamente la base de datos
        try:
            db.session.add(nuevo_usuario)
            db.session.commit()
            return '''<h2>Nuevo usuario creado con éxito</h2>
            <a href="/">Volver<a/>'''
        except:
            return "<h2>Ha habido un problema en la creación del registro</h2>"
    else:
        return render_template('signup.html', form=form)


Ahora ejecuta el programa y comprueba si la vista para registrar los datos del usuario funciona. Debería hacerlo correctamente y en la base de datos haber un nuevo usuario creado.

Hagamos lo mismo para la vista del "login". 

Vamos a pensar lo que debería hacer el programa dejando a parte la opción recuérdame. Una vez que el usuario registre su nombre de usuario y contraseña en el formulario, esta información se enviará a la vista "login". Ahí el programa verá, primero si hay un usuario en la base de datos que hemos creado que tenga ese nombre y si es así, comprobará si la contraseña introducida por ese usuario en concreto es igual a la contraseña que tenemos almacenada de él, en la base de datos de cuando este se registro.

Si el usuario no existe o la contraseña no es correcta devolveremos una pantalla de error. Si el usuario existe y la contraseña es correcta nos redirigirá a la vista "dashboard" que será, cuando terminemos el capítulo, a la que puedan acceder los usuarios solamente si están registrados.

Necesitamos importar de flask, los métodos "redirect"y "url_for" ya que sino al modificar la vista nos dará un error.

        1    from flask import Flask, render_template, redirect, url_for
El código de la vista login quedaría de la siguiente forma, susituye "pass" por lo que sigue:

@app.route('/login/', methods=['GET', 'POST'])
def login():
    form = forms.Formulario_de_Login()
    if form.validate_on_submit():
        usuario = Usuario.query.filter_by(nombre_usuario=form.nombre_usuario.data).first()
        if usuario:
            if usuario.contrasena == form.contrasena.data:
                return redirect(url_for('dashboard'))
        return '''<h2>Usuario o contraseña incorrecta</h2>
            <a href="/">Volver</a>'''     
            
    else:
        return render_template('login.html', form=form)

Ejecuta de nuevo el programa para ver si puedes logearte en el sistema.


Ya hemos avanzado mucho en el diseño pero tenemos un punto que resolver. Si miras la base de datos verás que las contraseñas se guardan en crudo, tal cual se teclean. Esto no es recomendable ya que todo el mundo puede verlas. Para solventar esto, por temas de seguridad, vamos a ver como podemos guardarlas codificadas en la base de datos y luego poder utilizarlas de nuevo sin que nadie más que el usuario que la introdujo sepa cual es. 


"Las contraseñas no deben ni pueden guardarse tal cual en la base de datos."


5.- Codificando las contraseñas con werkzeug.security


Codificar y decodificar las contraseñas se convierte en algo muy fácil con esta librería. Si has seguido el proyecto hasta aquí seguramente ya la tengas instalada de alguna de las dependencias, pero si te da error instálala primero con:

pip3 install Werkzeug

Importamos las funciones generate_password_hash y check_passwod_hash desde werkzeug.security al principio de inicio.py
from werkzeug.security import generate_password_hash, check_password_hash
Como su nombre indica generate_password_hash codificará el texto que le pasemos como contraseña. El sistema que utiliza para ello por defecto es SHA-256.

En el lado contrario check_password_hash comparará la contraseña codificada con la que le pasemos en un formulario y nos dirá si es la misma o no.

Para que la vista de registro (signup) guarde las contraseñas codificadas solo tenemos que hacer unos pequeños cambios. 

Primero le decimos que la contraseña codificada será igual a la que le pase el usuario en el formulario de registro pero pasándola a través de la función generate_password_hash

Y ahora en el modelo Usuario le decimos que guarde la contraseña codificada y no la original que nos proporciona el formulario. Y ya está, si ejecutas el programa y añades un nuevo usuario y luego consultas la base de datos verás que ya aparece cifrada.





Archivo de base de datos con la contraseña codificada:







Ahora programemos el proceso inverso. 

Cuando el usuario inicie sesión, a través de la vista login, tenemos que comprobar si la contraseña introducida es la misma que tenemos codificada en la base de datos. Esto lo haremos con check_password_hash. Nos vamos a la vista login y modificamos la línea que compara las contraseñas para que incluyan esta instrucción, simplemente:




En este punto volvemos a probar el programa registrando un nuevo usuario y luego iniciando sesión. Si todo ha ido bien deberías ver:

¡Éxito!


Y con esto tenemos el 95% del proceso de creación de un Login de usuario hecho. Nos queda el final y retocarlo un poco.

6.- Sesiones de usuario con Flask-Login.


Ya nos queda la última parte que es la más novedosa, puesto que todo lo que hemos hecho hasta ahora lo habíamos desglosado en post anteriores. Con Flask-Login podremos gestionar las sesiones de nuestros usuarios; se va a ocupar de las tareas comunes tales como:
  •  El inicio de sesión, el logout y recordar las sesiones de usuarios durante periodos de tiempo personalizados.
  • Restringir el acceso a ciertas vistas únicamente a los usuarios autenticados.
  • Gestionar la funcionalidad Recuérdame para mantener la sesión incluso después de que el usuario cierre el navegador.
  • Proteger el acceso a las cookies de sesión frente a terceros.

El primer paso para usar Flask-login en nuestra aplicación será instalarla. Para ello, ejecutaremos en la consola lo siguiente, dentro de nuestro entorno virtual, si lo estas utilizando:

pip3 install flask-login

1) En el programa principal, inicio.py,  configuramos la extensión:

from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
...

gestor_inicio_sesion  = LoginManager(app)
gestor_inicio_sesion.login_view = 'login'

Con gestor_inicio_sesion.login_view = 'login' le estamos diciendo a la aplicación que si un usuario entra en una vista que necesita autentificación para verse, sino está logueado le redirija a esta vista, en nuestro caso, a la vista "login". Lo vamos a ver en  breve.


Al importar Flask-Login se nos provee de diversas funciones muy interesantes para el uso de sesiones de usuario:

    • UserMixin: es una clase que proporciona implementaciones por defecto para los métodos que Flask-Login espera que tengan los objetos de usuario de la clase Flask-Login.

    login_user: Esta función permite crear la sesión de un usuario.

    • logout_user: Esta función permite terminar la sesión actual. 

    • login_required: Es un decorador que nos permite restringir la ejecución de una vista sólo a los usuarios logueados. 

    • current_user: Es un objeto con la información del usuario autentificado. 


2) Función user_loader. Flask-Login realiza un seguimiento del usuario que inicio sesión, almacenando su identificador único en la sesión de usuario de Flask, un espacio de almacenamiento asignado a cada usuario que se conecta a la aplicación. Cada vez que el usuario que ha iniciado sesión, navega a una página nueva, flask-login recupera el ID del usuario de la sesión y luego carga ese usuario en la memoria.

Sin embargo la extensión Flask-Login no sabe nada de bases de datos, necesita que nuestra aplicación le ayude para cargar un usuario. Por esa razón, la extensión espera que la aplicación configure una función para cargar usuarios, que se pueda llamar para cargar un usuario dado el ID. Por lo tanto, en el programa principal tenemos que escribir obligatoriamente una función para ello, que va a utilizar Flask-Login:

# cargador de Usuario - Flask login & clase Usuario
@gestor_inicio_sesion.user_loader
def load_user(user_id):
    return Usuario.query.get(int(user_id))

El cargador de usuarios lo registramos en flask-login con el decorador @gestor_inicio_sesion.user_loader. La identificación que flask-login pasa a la función como argumento será un string, por lo que las bases de datos que usan identificaciones numéricas, como es nuestro caso, deben convertir el string o cadena en un número entero como se ve más arriba. Para ello usaremos la función int de python.


3) Para que todo funcione necesitamos añadir "UserMixin" a nuestra clase Usuario. Así con ella, no solo tendremos acceso a la base de datos sino que también podremos usar las funciones de flask-login. Es decir con la misma clase conectaremos flask-login y la base de datos de los usuarios. Para hacerlo simplemente tenemos que decir que la clase usuario además de db.Model herede de UserMixin:

...

class Usuario(db.Model, UserMixin):

inicio.py después de las modificaciones:



Y con esto prácticamente ya tenemos creado nuestro login de usuarios 💪😊. Solo queda afinarlo un poco.

Control de acceso a las diferentes vistas.


Hasta ahora todas las vista que tiene el programa son públicas. ¿Que quiere esto decir? Pues que si entras en por ejemplo la vista "dashboard", que se supone que tienes que estar logueado para verla, te va a dejar entrar sin ningún problema. Es más, vamos a verlo. Ejecuta el programa y entra en la siguiente dirección:

127.0.0.1:5000/dashboard



¡Pues vaya login que nos deja acceder sin más a todas las páginas! (Estarás pensando). 

Vamos a arreglar esto. Pensemos primero en cual son las vistas que queremos proteger. En este proyecto protegeremos las vistas dashboard, que es la vista final a la que queremos que se acceda después de loguearse y también la vista logout. No tiene sentido poder salir si aún no te has logueado.


El proceso es tan sencillo como utilizar el decorador @login_required a continuación del decorador que define la vista.

Por ejemplo:

@app.route('/dashboard/')
@login_required
def dashboard():
    return '<h2>pagina a la que solo se podra aceder una vez registrado</h2><a href="/">Volver</a>'


@app.route('/logout/')
@login_required
def logout():
    return '<h2>pagina para que el usuario se desconecte</h2><a href="/">Volver</a>'

      


Modifica el código de inicio.py para incluir el decorador @login_required en la vista 'dashboard' y 'login' tal como te indico arriba. Si ahora vuelves a ejecutar el programa y entras en el navegador en la dirección 127.0.0.1:5000/dashboard verás como ya no puedes entrar en esa vista, sino que ahora el programa te redirige a la vista de login. 

Esto es asi porque hemos incluido el decorador @login_required y porque al principio del programa con "gestor_inicio_sesion .login_view = 'login' " le dijimos a que página tenía que ir cuando se entrará en vistas en las que fuera necesario loguearse para verlas.

Bien, ahora vamos a afinar el modelo y definitivamente conseguir que nuestro usuario comience su sesión.

En la vista login tenemos que decir que si el usuario existe en la base de datos y la contraseña guardada de forma codificada coincide con la introducida loguee al usuario. Para ello usamos la instrucción de FLask-Login:

login_user(usuario, remember=form.recuerdame.data)

* El primer parámetro "usuario" es le objeto que habíamos obtenido con la consulta a la base de datos en base al nombre de usuario introducido en el formulario. Acto seguido de registrarlo el programa nos redirige a la vista dashboard. El segundo parámetro "remember" es si el usuario debe seguir logueado al cerrar el navegador (true) o no (False). 




Pues con esto ya podemos iniciar sesión con un usuario. Estaría bien que si el usuario ya ha iniciado sesión y acede a las vista login o signup le redirija directamente a la página "dashboard", puesto que ya está autenticado.

Para ello vamos a utilizar el método current_user.is_authenticated. 

Existen varios métodos que se pueden utilizar:

   is_authenticated: Devuelve True si el usuario se autentifica, es decir, ha proporcionado unas credenciales válidas o False en caso contrario. 

    • is_active: Devuelve True si la cuenta del usuario está activa y False en caso contrario. Además de ser autenticado, también ha activado su cuenta, no se ha suspendido, o cualquier condición que su aplicación requiera para rechazar una cuenta. Esto no lo hemos tenido en cuenta en nuestro modelo de datos. 

    • is_anonymous: es una propiedad que retorna False para los usuarios habituales y True para un usuario anónimo especial.

    • get_id(): es un método que devuelve un identificador único para el usuario como una cadena o string (unicode si usas python 2)

    • is_admin: devuelve True si el usuario logueado es administrador y False en caso contrario. 




Para que el usuario se desconecte es igual de sencillo. En la vista "logout" añadimos, logout_user() y le redirigimos una vez desconectado la vista "home":


# Para terminar creamos un sistema de log-out
@app.route('/logout/')
@login_required
def logout():
    logout_user()
    return redirect(url_for('home'))

Y también modificaremos la plantilla pagina_inicial.html para que en la barra de navegación, si el usuario está logueado, en la vista "home" solo se muestre el nombre del usuario y la opción de salir. Si no esta logueado se mostrarán los botones de registro y login.


<ul class="nav navbar-nav">
  {% if current_user.is_authenticated %}
    <li class="active"><a href="#">{{current_user.nombre_usuario}}</a></li>
    <li><a href="{{ url_for('logout') }}">Logout</a></li>
  {% else %}
    <li class="active"><a href="#">Home</a></li>
    <li><a href="{{ url_for('login') }}">Login</a></li>
    <li><a href="{{ url_for('signup') }}">Sign Up</a></li>
   {% endif %}
</ul>



Reto. ¿Podrías modificar el código para que cuando el usuario se registre automáticamente quede logueado?


Por último, por temas de seguirdad, comprobamos si recibimos el parámetro next. Esto sucederá cuando el usuario ha intentado acceder a una página protegida pero no estaba autenticado. Además solo tendremos en cuenta dicho parámetro si la ruta es relativa. Una URL relativa tiene una ruta pero no tiene un nombre de host (y por lo tanto no tiene netloc). Por ejemplo:

http://usuario:contraseña@www.ejemplo.com/index?search=src

Aqui, http://usuario:contraseña@www.ejemplo.com es el netloc, todo lo que viene antes de la "/".


De este modo evitamos que un atacante con malas intenciones pueda insertar una URL que dirija al usuario a un sitio externo malicioso, fuera de nuestro dominio, utilizando el argumento next. Si no se recibe el argumento next o este no contiene una ruta relativa (lo que garantiza que la redirección permanezca en el mismo sitio que la aplicación) redirigimos al usuario a la página de nuestro dashboard.

Tenemos que importar primeramente:

a) el método request de la librería de Flask

from flask import Flask, render_template, redirect, url_for, request

b) el método url_parse de la librería de werkzeug.urls

# Por temas de seguridad en la ruta
from werkzeug.urls import url_parse



Luego en la vista "login", que es a la que se redirigen todos los intentos de acceder a una página en la que el usuario debería logearse y no lo está, sustituimos:

return redirect(url_for('dashboard'))

por:

next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
   next_page = url_for('dashboard')
return redirect(next_page)

Y con esto, por fin tenemos creada toda la aplicación.