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:


- 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

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]

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:

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

El servidor Redis puede consultarse en modo de comandos. Abrimos un terminal 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 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:

Aparecerá la siguiente interfaz:

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

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

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:

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