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:


- 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

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]

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:

- 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:

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

- 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:

Aparecerá la siguiente interfaz:

- 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:

# 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(0, 1)
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:

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:

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