Skip to content

33. Ejercicio práctico: versión 13

La versión 13 modifica la versión 12 en los siguientes aspectos:

  • se reestructuran ciertas partes del código (refactorización);
  • se gestiona la sesión de manera diferente mediante el módulo [flask_session];
  • se utilizan contraseñas cifradas en el archivo de configuración;

La versión 13 se obtiene inicialmente copiando la versión 12:

Image

Image

  • en [1], se va a refactorizar la configuración. En particular, se saca de la carpeta [flask];
  • en [2], se va a refactorizar el script principal. También se extrae de la carpeta [flask];
  • en [3], la configuración se dividirá en varios archivos;
  • en [4]: se simplificará el script principal [main] trasladando código a otros archivos;
  • en [5], se modificará el controlador de autenticación, ya que a partir de ahora las contraseñas de los usuarios se cifrarán;
  • en [6], el controlador principal incorporará código que antes se encontraba en el script principal [main];

33.1. Reestructuración de la configuración de la aplicación

Image

Aparecen tres nuevos archivos de configuración:

  • [mvc]: para configurar la arquitectura MVC de la aplicación;
  • [parameters]: que reunirá todas las constantes de la aplicación;
  • [syspath]: que configura la ruta de Python de la aplicación;

El archivo [syspath.py] es el siguiente:


def configure(config: dict) -> dict:
    import os

    # carpeta de este archivo
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # ruta raíz
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # dependencias
    absolute_dependencies = [
        # carpetas del proyecto
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        # ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        # Constantes, tramos
        f"{root_dir}/impots/v05/entities",
        # Registrador, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
        # carpeta del script principal
        script_dir,
        # configuraciones [database, layers, parameters, controllers, views]
        f"{script_dir}/../configs",
        # controladores
        f"{script_dir}/../controllers",
        # respuestas HTTP
        f"{script_dir}/../responses",
        # plantillas de vistas
        f"{script_dir}/../models_for_views",
    ]

    # se establece el syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # se hace pública la configuración
    return {
        "root_dir": root_dir,
        "script_dir": script_dir
    }
  • el script [syspath] sirve para configurar la ruta de Python de la aplicación (líneas 40-41);
  • proporciona dos datos útiles para los demás scripts de configuración (líneas 45-46);

El script [mvc] configura la arquitectura MVC de la aplicación web jSON / XML / HTML:


def configure(config: dict) -> dict:
    # configuración de la aplicación MVC

    # los controladores
    from AfficherCalculImpotController import AfficherCalculImpotController
    from AuthentifierUtilisateurController import AuthentifierUtilisateurController
    from CalculerImpotController import CalculerImpotController
    from CalculerImpotsController import CalculerImpotsController
    from FinSessionController import FinSessionController
    from GetAdminDataController import GetAdminDataController
    from InitSessionController import InitSessionController
    from ListerSimulationsController import ListerSimulationsController
    from MainController import MainController
    from SupprimerSimulationController import SupprimerSimulationController
    # las respuestas HTTP
    from HtmlResponse import HtmlResponse
    from JsonResponse import JsonResponse
    from XmlResponse import XmlResponse
    # las plantillas de las vistas
    from ModelForAuthentificationView import ModelForAuthentificationView
    from ModelForCalculImpotView import ModelForCalculImpotView
    from ModelForErreursView import ModelForErreursView
    from ModelForListeSimulationsView import ModelForListeSimulationsView

    # acciones autorizadas y sus controladores
    controllers = {
        # inicialización de una sesión de cálculo
        "init-session": InitSessionController(),
        # autenticación de un usuario
        "authentifier-utilisateur": AuthentifierUtilisateurController(),
        # cálculo del impuesto en modo individual
        "calculer-impot": CalculerImpotController(),
        # cálculo del impuesto en modo por lotes
        "calculer-impots": CalculerImpotsController(),
        # lista de simulaciones
        "lister-simulations": ListerSimulationsController(),
        # eliminación de una simulación
        "supprimer-simulation": SupprimerSimulationController(),
        # fin de la sesión de cálculo
        "fin-session": FinSessionController(),
        # visualización de la vista de cálculo del impuesto
        "afficher-calcul-impot": AfficherCalculImpotController(),
        # obtención de los datos de la administración tributaria
        "get-admindata": GetAdminDataController(),
        # controlador principal
        "main-controller": MainController()
    }
    # los diferentes tipos de respuesta (json, xml, html)
    responses = {
        "json": JsonResponse(),
        "html": HtmlResponse(),
        "xml": XmlResponse()
    }
    # las vistas HTML y sus plantillas dependen del estado devuelto por el controlador
    views = [
        {
            # vista de autenticación
            "états": [
                700,    # /iniciar-sesión - éxito
                201,    # /autenticar-usuario - error
            ],
            "view_name""views/vue-authentification.html",
            "model_for_view": ModelForAuthentificationView()
        },
        {
            # vista de cálculo del impuesto
            "états"[
                200,    # /autenticar-usuario: éxito
                300,    # /calcular-impuesto: exitoso
                301,    # /calcular-impuesto fallido
                800,    # /mostrar-cálculo-impuesto correcto
            ],
            "view_name""views/vue-calcul-impot.html",
            "model_for_view": ModelForCalculImpotView()
        },
        {
            # vista de la lista de simulaciones
            "états"[
                500,    # /listar-simulaciones (correcto)
                600,    # /eliminar-simulación (correcto)
            ],
            "view_name""views/vue-liste-simulations.html",
            "model_for_view": ModelForListeSimulationsView()
        },

    ]
    # vista de los errores inesperados
    view_erreurs = {
        "view_name""views/vue-erreurs.html",
        "model_for_view": ModelForErreursView()
    }
    # redirecciones
    redirections = [
        {
            "états": [
                400,  # /fin-de-sesión (correcto)
            ],
            # redirección a URL
            "to""/init-session/html",
        }
    ]


    # se restablece la configuración MVC
    return {
        # controladores
        "controllers": controllers,
        # respuestas HTTP
        "responses": responses,
        # vistas y plantillas
        "views": views,
        # lista de redirecciones
        "redirections": redirections,
        # vista de errores inesperados
        "view_erreurs": view_erreurs
    }
  • líneas 1-101: este código es conocido;
  • líneas 105-116: se devuelve la configuración MVC de la aplicación;

El script [parameters] recopila las constantes de la aplicación:


def configure(config: dict) -> dict:
    # configuración de la aplicación

    # script_dir
    script_dir = config['syspath']['script_dir']

    # Configuración de la aplicación
    parameters = {
        # usuarios autorizados a utilizar la aplicación
        "users"[
            {
                "login""admin",
                "password""$pbkdf2-sha256$29000$mPM.h3COkTIGYOzde68VIg$7LH5Q7rN/1hW.Xa.6rcmR6h52PntvVqd5.na7EtgQNw"
            }
        ],
        # archivo de registros
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # configuración del servidor SMTP
        "adminMail": {
            # servidor SMTP
            "smtp-server""localhost",
            # puerto del servidor SMTP
            "smtp-port""25",
            # administrador
            "from""guest@localhost.com",
            "to""guest@localhost.com",
            # asunto del correo
            "subject""plantage du serveur de calcul d'impôts",
            # TLS en «True» si el servidor SMTP requiere autorización, en «False» en caso contrario
            "tls"False
        },
        # duración de la pausa del hilo en segundos
        "sleep_time"0,
        # servidor Redis
        "redis": {
            "host""127.0.0.1",
            "port"6379
        },
    }

    # se devuelven los ajustes de la aplicación
    return parameters
  • línea 13: a partir de ahora, las contraseñas de los usuarios se cifrarán;
  • líneas 34-38: la configuración de un servidor [Redis], sobre el que hablaremos más adelante;

Con estos nuevos archivos de configuración, el script [config] queda así:


def configure(config: dict) -> dict:
    # Configuración de syspath
    import syspath
    config['syspath'] = syspath.configure(config)

    # Configuración de la aplicación
    import parameters
    config['parameters'] = parameters.configure(config)

    # configuración de la base de datos
    import database
    config["database"] = database.configure(config)

    # instanciación de las capas de la aplicación
    import layers
    config['layers'] = layers.configure(config)

    # configuración MVC de la capa [web]
    import mvc
    config['mvc'] = mvc.configure(config)

    # se devuelve la configuración
    return config

33.2. Reestructuración del script principal [main]

Image

El script principal [main.py] simplemente inicia el servidor:


# se espera un parámetro mysql o pgres
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# se configura la aplicación
import config
config = config.configure({'sgbd': sgbd})

# dependencias
from SendAdminMail import SendAdminMail
from Logger import Logger
from ImpôtsError import ImpôtsError
import redis

# envío de un correo electrónico al administrador
def send_adminmail(config: dict, message: str):
    # se envía un correo electrónico al administrador de la aplicación
    config_mail = config['parameters']['adminMail']
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)

# verificación del archivo de registros
logger = None
erreur = False
message_erreur = None
try:
    # registrador
    logger = Logger(config['parameters']['logsFilename'])
except BaseException as exception:
    # registro de consola
    print(f"L'erreur suivante s'est produite : {exception}")
    # se anota el error
    erreur = True
    message_erreur = f"{exception}"
# se guarda el registrador en la configuración
config['logger'] = logger
# manejo del error
if erreur:
    # correo electrónico al administrador
    send_adminmail(config, message_erreur)
    # cierre de la aplicación
    sys.exit(1)

# registro de inicio
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")

# Se verifica la disponibilidad del servidor Redis
redis_client = redis.Redis(host=config["parameters"]["redis"]["host"],
                           port=config["parameters"]["redis"]["port"])
# Se realiza un ping al servidor Redis
try:
    redis_client.ping()
except BaseException as exception:
    # Redis no está disponible
    log = f"[serveur] Le serveur Redis n'est pas disponible : {exception}"
    # consola
    print(log)
    # registro
    logger.write(f"{log}\n")
    # fin
    sys.exit(1)

# se guarda el cliente [redis] en la configuración
config['redis_client'] = redis_client

# recuperación de datos de la administración tributaria
erreur = False
try:
    # admindata será un dato de alcance de aplicación de solo lectura
    config["admindata"] = config["layers"]["dao"].get_admindata()
    # registro de éxito
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    # se registra el error
    erreur = True
    # registro de error
    log = f"L'erreur suivante s'est produite : {ex}"
    # consola
    print(log)
    # archivo de registros
    logger.write(f"{log}\n")
    # correo al administrador
    send_adminmail(config, log)

# el hilo principal ya no necesita el registrador
logger.close()

# si hubo un error, se detiene
if erreur:
    sys.exit(2)

# importación de las rutas de la aplicación web
import routes
routes.config=config
routes.execute(__name__)
  • líneas 56-73: se introduce un servidor Redis y su cliente;
  • líneas 102-104: las rutas de la aplicación se han externalizado en el script [routes];

33.2.1. Los módulos [flask_session] y [redis]

El servidor [Redis] se utilizará para almacenar las sesiones de los usuarios. Usaremos el módulo [flask_session] para administrar estas sesiones. Este módulo puede almacenar las sesiones de los usuarios en varios lugares. Redis es uno de ellos y lo utilizaremos.

El módulo [flask_session] debe instalarse en un terminal PyCharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install flask-session
Collecting flask-session

Para comunicarnos con el servidor Redis, necesitamos un cliente Redis. Este nos lo proporcionará el módulo [redis], que también instalaremos:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install redis
Collecting redis

33.2.2. El servidor Redis

El servidor Redis servirá para almacenar las sesiones de los usuarios. El módulo [flask_session] funciona de la siguiente manera:

  • cada usuario tiene un identificador de sesión y es precisamente eso lo que se envía al cliente, y solo eso. El cliente recibe una cookie de sesión únicamente una vez, al finalizar su primera solicitud. Esta cookie contiene el identificador de sesión del usuario, el cual no cambiará a lo largo de las solicitudes del cliente. Por eso, el servidor no tiene que volver a enviar una nueva cookie de sesión;
  • anteriormente, el contenido de la sesión se enviaba al cliente. Esto ya no será así. El contenido de la sesión del usuario se almacenará en el servidor Redis;

Laragon viene con un servidor Redis desactivado por defecto. Por lo tanto, primero hay que activarlo:

Image

  • en [3], activa el servidor [Redis];
  • en [4], dejar el puerto [6379] que los clientes de Redis utilizan por defecto;

Los servicios de Laragon se reinician automáticamente tras la activación de Redis:

Image

Se puede consultar el servidor Redis en modo de comandos. Abrimos un terminal de Laragon [6]:

Image

  • en [1], el comando [redis-cli] inicia el cliente en modo de comando del servidor Redis;

En julio de 2019, el cliente Redis puede utilizar 172 comandos para comunicarse con el servidor [https://redis.io/commands#list]. Uno de ellos, [command count] [2], muestra este número: [3].

La escritura en [Redis] se realiza con el comando de Redis [set attribut valeur] [4]. El valor se puede recuperar luego con el comando [get attribut] [5].

Puede ser necesario vaciar la memoria de Redis. Esto se hace con el comando [flushdb] [6]. Luego, si se solicita el valor del atributo [titre] [7], se obtiene una referencia [nil] [8] que indica que no se encontró el atributo. También se puede utilizar el comando [exists] [9-10] para verificar la existencia de un atributo.

Para salir del cliente Redis, escribe el comando [quit] [11].

También se puede utilizar una interfaz web para administrar las claves presentes en el servidor Redis. Para ello, es necesario que el servidor Apache de Laragon esté en ejecución:

Image

Aparecerá la siguiente interfaz:

Image

  • en [1-4], una de las sesiones almacenadas en el servidor Redis;

33.2.3. Administración del servidor Redis en el script principal [main]

El script [main] verifica la presencia del servidor Redis de la siguiente manera:


import redis

# Se verifica la disponibilidad del servidor Redis
redis_client = redis.Redis(host=config["parameters"]["redis"]["host"],
                           port=config["parameters"]["redis"]["port"])
# se hace un ping al servidor Redis
try:
    redis_client.ping()
except BaseException as exception:
    # Redis no está disponible
    log = f"[serveur] Le serveur Redis n'est pas disponible : {exception}"
    # consola
    print(log)
    # registro
    logger.write(f"{log}\n")
    # fin
    sys.exit(1)

# se guarda el cliente [redis] en la configuración
config['redis_client'] = redis_client
  • línea 4: el constructor de la clase [redis.Redis] crea un cliente del servidor Redis. Las características de este (dirección, puerto) se encuentran en el script [parameters];
  • línea 8: el método [ping] permite verificar si el servidor Redis está presente;
  • líneas 9-17: si el ping no se realiza con éxito, se registra el error y se detiene el servidor;
  • línea 20: la referencia del cliente Redis se incluye en la configuración;

33.2.4. Gestión de rutas en el script principal [main]

La gestión de rutas en [main] se limita a las siguientes líneas:


# importación de las rutas de la aplicación web
import routes
routes.config=config
routes.execute(__name__)
  • línea 1: las rutas se han externalizado al módulo [routes];
  • línea 3: las rutas necesitan conocer la configuración de ejecución;
  • línea 4: se inicia la aplicación Flask pasándole el nombre del script ejecutado (__main__);

El script de las rutas es el siguiente:


# dependencias
import os

from flask import Flask, redirect, request, session, url_for
from flask_api import status
from flask_session import Session

# Aplicación Flask
app = Flask(__name__, template_folder="../flask/templates", static_folder="../flask/static")

# configuración de la aplicación
config = {}

# el controlador principal
def front_controller() -> tuple:
    # se reenvía la solicitud al controlador principal
    main_controller = config['mvc']['controllers']['main-controller']
    return main_controller.execute(request, session, config)

@app.route('/', methods=['GET'])
def index() -> tuple:
    # redirección a /init-session/html
    return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)

# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# autenticar-usuario
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# calcular-impuesto
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# cálculo del impuesto por lotes
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# listar simulaciones
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# eliminar-simulación
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# fin-sesión
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# mostrar-cálculo-impuestos
@app.route('/afficher-calcul-impot', methods=['GET'])
def afficher_calcul_impot() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# obtener-datos-de-administración
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

def execute(name: str):
    # clave secreta de la sesión
    app.secret_key = os.urandom(12).hex()
    # Flask-Session
    app.config.update(SESSION_TYPE='redis',
                      SESSION_REDIS=config['redis_client'])
    Session(app)
    # caso en el que se inicia la aplicación Flask mediante un script de consola
    if name == '__main__':
        app.config.update(ENV="development", DEBUG=True)
        app.run(threaded=True)
  • línea 9: se instancia la aplicación Flask;
  • línea 12: la configuración de la aplicación aún no se conoce al momento de escribir el script. Solo se conoce en el momento de su ejecución;
  • líneas 20-77: las rutas de la aplicación tal como estaban definidas en la versión anterior. No cambian;
  • líneas 14-18: todas las rutas se limitan a llamar a la función [front_controller]. Hemos eliminado de esta su código inicial. Ahora solo llama al controlador principal de la aplicación web;
  • líneas 79-89: [execute] es la función que llama el script [main] para iniciar la aplicación web;
  • línea 81: el módulo [flask_session] utiliza la clave secreta de Flask;
  • líneas 82-84: configuración del módulo [flask_session]. Esta consiste en agregar las claves [SESSION_TYPE, SESSION_REDIS] a la configuración [app.config] de la aplicación Flask [app]:
  • [SESSION_TYPE]: el tipo de sesión. Existen varios. El tipo [redis] indica que [flask_session] utiliza un servidor [redis] para almacenar las sesiones de los usuarios. Por eso, hay que definir la clave [SESSION_REDIS], que debe ser la referencia de un cliente Redis;
  • línea 85: la sesión [Flask-Session] está asociada a la aplicación Flask;
  • líneas 86-89: si el parámetro [name] de la línea 79 es la cadena [__main__], entonces se inicia la aplicación Flask;

33.3. Reestructuración del controlador principal

El código que antes se encontraba en la función [front_controller] del script [main] se ha trasladado al controlador principal:

Image


# importación de dependencias
import threading
import time
from random import randint

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController
from Logger import Logger
from SendAdminMail import SendAdminMail

def send_adminmail(config: dict, message: str):
    # envío de un correo electrónico al administrador de la aplicación
    config_mail = config['parameters']['adminMail']
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)

# controlador principal de la aplicación
class MainController(InterfaceController):
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # se procesa la solicitud
        logger = None
        action = None
        type_response1 = None
        try:
            # se recuperan los elementos de la ruta
            params = request.path.split('/')

            # la acción es el primer elemento
            action = params[1]

            # sin errores al inicio
            erreur = False

            # se debe conocer el tipo de sesión antes de realizar ciertas acciones
            type_response1 = session.get('typeResponse')
            if type_response1 is None and action != "init-session":
                # se registra el error
                résultat = {"action": action, "état"101,
                            "réponse"["pas de session en cours. Commencer par action [init-session]"]}
                erreur = True

            # registrar
            logger = Logger(config['parameters']['logsFilename'])

            # se almacena en una configuración asociada al hilo
            thread_config = {"logger": logger}
            thread_name = threading.current_thread().name
            config[thread_name] = {"config": thread_config}

            # se registra la solicitud
            logger.write(f"[MainController] requête : {request}\n")

            # se interrumpe el hilo si así se ha solicitado
            sleep_time = config['parameters']['sleep_time']
            if sleep_time != 0:
                # la pausa es aleatoria para que algunos hilos se interrumpan y otros no
                aléa = randint(01)
                if aléa == 1:
                    # registro antes de la pausa
                    logger.write(f"[front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                    # pausa
                    time.sleep(sleep_time)

            # Para ciertas acciones es necesario estar autenticado
            user = session.get('user')
            if not erreur and user is None and action not in ["init-session""authentifier-utilisateur"]:
                # se registra el error
                résultat = {"action": action, "état"101,
                            "réponse"[f"action [{action}] demandée par utilisateur non authentifié"]}
                erreur = True

            # ¿Hay algún error?
            if erreur:
                # la solicitud no es válida
                status_code = status.HTTP_400_BAD_REQUEST
            else:
                # se ejecuta el controlador asociado a la acción
                controller = config['mvc']['controllers'][action]
                résultat, status_code = controller.execute(request, session, config)

        except BaseException as exception:
            # Otras excepciones (inesperadas)
            résultat = {"action": action, "état"131"réponse"[f"{exception}"]}
            status_code = status.HTTP_400_BAD_REQUEST

        finally:
            pass

        # se registra el resultado enviado al cliente
        log = f"[MainController] {résultat}\n"
        logger.write(log)

        # ¿Hubo un error fatal?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # se envía un correo electrónico al administrador de la aplicación
            send_adminmail(config, log)

        # se determina el tipo deseado para la respuesta
        type_response2 = session.get('typeResponse')
        if type_response2 is None and type_response1 is None:
            # aún no se ha establecido el tipo de sesión; será jSON
            type_response = 'json'
        elif type_response2 is not None:
            # el tipo de respuesta se conoce y está en la sesión
            type_response = type_response2
        else:
            # de lo contrario, se sigue utilizando type_response1
            type_response = type_response1

        # se genera la respuesta que se enviará
        response_builder = config['mvc']['responses'][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)

        # se cierra el archivo de registros si se había abierto
        if logger:
            logger.close()

        # se envía la respuesta HTTP
        return response, status_code

Todo este código ya se ha visto en algún momento.

33.4. Manejo de contraseñas cifradas

Para gestionar contraseñas cifradas, utilizaremos el módulo [passlib], que se instala desde un terminal de PyCharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install passlib
Collecting passlib

A continuación, se muestra un ejemplo de script que encripta la contraseña que se le pasa como parámetro:

Image

El script [create_hashed_password] es el siguiente (https://passlib.readthedocs.io/en/stable/):


import sys

# función de cifrado
from passlib.hash import pbkdf2_sha256

# se espera la contraseña a cifrar
syntaxe = f"{sys.argv[0]} password"
erreur = len(sys.argv) != 2
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()
else:
    password = sys.argv[1]

# se cifra la contraseña
hash = pbkdf2_sha256.hash(password)
print(f"version cryptée de [{password}] = {hash}")

# verificación
correct = pbkdf2_sha256.verify(password, hash)
print(correct)
  • línea 16: se cifra la contraseña pasada como parámetro;
  • línea 20: se compara la contraseña [password] pasada como parámetro con su versión cifrada [hash]. La función [verify] cifra la contraseña [password] y compara la cadena cifrada obtenida con [hash]. Devuelve True si ambas cadenas son iguales;

El script anterior nos permite obtener la versión cifrada de la contraseña [admin]:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/08/passlib/create_hashed_password.py admin
version cryptée de [admin] = $pbkdf2-sha256$29000$fU9pTendO6c0ZoyR8r5Xqg$5ZXywIUnbMfN2hPnBaefiuqWjEbmAY.Lu06i4dwcnek
True

Línea 2, el valor que introducimos en el script [parameters]:


        "users"[
            {
                "login""admin",
                "password""$pbkdf2-sha256$29000$fU9pTendO6c0ZoyR8r5Xqg$5ZXywIUnbMfN2hPnBaefiuqWjEbmAY.Lu06i4dwcnek"
            }
        ],

El controlador de autenticación [AuthentifierUtilisateurController] evoluciona de la siguiente manera:


from passlib.handlers.pbkdf2 import pbkdf2_sha256

            # se verifica la validez del par (usuario, contraseña)
            users = config['parameters']['users']
            i = 0
            nbusers = len(users)
            trouvé = False
            while not trouvé and i < nbusers:
                trouvé = user == users[i]["login"] and pbkdf2_sha256.verify(password, users[i]["password"])
                i += 1
            # ¿Encontrado?
            if not trouvé:

33.5. Pruebas

Además de las pruebas con un navegador, también se pueden utilizar los clientes de la carpeta [http-servers/07] escritos para la versión 12. Estos también deberían «funcionar» para la versión 13:

Image

  • en [1], los tres clientes deben funcionar;
  • en [2], las dos pruebas deben funcionar;