Skip to content

33. Ejercicio práctico: version 13

La version 13 modifica la version 12 en los siguientes puntos:

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

La version 13 se obtiene inicialmente mediante la copia de la version 12:

Image

Image

  • en [1], se va a refactorizar la configuración. En concreto, 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 estarán encriptadas;
  • en [6], el controlador principal integrará 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 el Python Path 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 la ruta del sistema
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # se aplica la configuración
    return {
        "root_dir": root_dir,
        "script_dir": script_dir
    }
  • el script [syspath] sirve para configurar el Python Path 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 permitidas 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 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,    # /init-session - éxito
                201,    # /autenticar-usuario - error
            ],
            "view_name""views/vue-authentification.html",
            "model_for_view": ModelForAuthentificationView()
        },
        {
            # vista del cálculo del impuesto
            "états"[
                200,    # /autenticar-usuario éxito
                300,    # /calcular-impuesto correcto
                301,    # /calcular-impuesto error
                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()
    }
    # redireccionamientos
    redirections = [
        {
            "états": [
                400,  # /fin-sesión correcto
            ],
            # redirección a URL
            "to""/init-session/html",
        }
    ]


    # se devuelve la configuración MVC
    return {
        # controladores
        "controllers": controllers,
        # respuestas HTTP
        "responses": responses,
        # vistas y plantillas
        "views": views,
        # lista de redireccionamientos
        "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",
        # config 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 devuelve la configuración 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 volveremos 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] se limita a iniciar 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 almacena el registrador en el config
config['logger'] = logger
# gestión del error
if erreur:
    # correo electrónico al administrador
    send_adminmail(config, message_erreur)
    # fin de la aplicación
    sys.exit(1)

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

# se comprueba la disponibilidad del servidor Redis
redis_client = redis.Redis(host=config["parameters"]["redis"]["host"],
                           port=config["parameters"]["redis"]["port"])
# conectando ping al servidor Redis
try:
    redis_client.ping()
except BaseException as exception:
    # Redis no 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 almacena el cliente [redis] en el config
config['redis_client'] = redis_client

# recuperación de datos de la administración tributaria
erreur = False
try:
    # admindata será un dato de ámbito 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 se ha producido 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. Utilizaremos el módulo [flask_session] para gestionar 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 eso lo que se envía al cliente, y solo eso. El cliente solo recibe una cookie de sesión una vez, al finalizar su primera solicitud. Esta cookie contiene el identificador de sesión del usuario, que 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;
  • antes, 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 incluye un servidor Redis que no está activado por defecto. Por lo tanto, hay que empezar por activarlo:

Image

  • en [3], active el servidor [Redis];
  • en [4], dejar el puerto [6379] que los clients Redis utilizan por defecto;

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

Image

El servidor Redis puede consultarse en modo de comandos. Abrimos un terminal 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 Redis [set attribut valeur] [4]. A continuación, el valor se puede recuperar con el comando [get attribut] [5].

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

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

También se puede utilizar una interfaz web para gestionar 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. Gestión del servidor Redis en el script principal [main]

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


import redis

# se comprueba la disponibilidad del servidor Redis
redis_client = redis.Redis(host=config["parameters"]["redis"]["host"],
                           port=config["parameters"]["redis"]["port"])
# en ping el servidor Redis
try:
    redis_client.ping()
except BaseException as exception:
    # Redis no 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 almacena el cliente [redis] en el config
config['redis_client'] = redis_client
  • línea 4: el constructor de la clase [redis.Redis] crea un cliente del servidor Redis. Sus características (dirección, puerto) se encuentran en el script [parameters];
  • línea 8: el método [ping] permite verificar la presencia del servidor Redis;
  • líneas 9-17: si ping no se ejecuta correctamente, 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 la 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()

# get-admindata
@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 escribir el script. Solo se conoce en el momento de su ejecución;
  • líneas 20-77: las rutas de la aplicación tal y como se definieron en el anterior version. No cambia;
  • 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 se limita a llamar al controlador principal de la aplicación web;
  • líneas 79-89: [execute] es la función llamada por 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 añadir 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 este motivo, 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. Refactorizació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 path
            params = request.path.split('/')

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

            # sin errores al inicio
            erreur = False

            # el tipo de sesión debe conocerse antes de 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 config 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 subproceso si así se ha solicitado
            sleep_time = config['parameters']['sleep_time']
            if sleep_time != 0:
                # la pausa es aleatoria para que algunos subprocesos 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 algunas acciones hay que estar autenticado
            user = session.get('user')
            if not erreur and user is None and action not in ["init-session""authentifier-utilisateur"]:
                # se anota el error
                résultat = {"action": action, "état"101,
                            "réponse"[f"action [{action}] demandée par utilisateur non authentifié"]}
                erreur = True

            # ¿hay errores?
            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)

        # ¿Se ha producido 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:
            # el tipo de sesión aún no se ha establecido; será jSON
            type_response = 'json'
        elif type_response2 is not None:
            # se conoce el tipo de respuesta y está en la sesión
            type_response = type_response2
        else:
            # de lo contrario, se sigue utilizando type_response1
            type_response = type_response1

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

        # cerramos el archivo de registros si se ha abierto
        if logger:
            logger.close()

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

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

33.4. Gestión 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 cifra 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 version ([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 cadena cifrada version a partir 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 comprueba 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 clients de la carpeta [http-servers/07] escritos para el version 12. También deberían «funcionar» para la version 13:

Image

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