34. Ejercicio de aplicación: version 14
El archivo [http-servers/09] de la version 14 se obtiene copiando el archivo [http-servers/08] de la version 13.
34.1. Introducción
CSRF (Cross Site Request Forgery) es una técnica de robo de sesión. Se explica así en Wikipedia (https://fr.wikipedia.org/wiki/Cross-site_request_forgery):
- Malorie consigue averiguar el enlace que permite eliminar el mensaje en cuestión.
- Malorie envía un mensaje a Alice con una pseudoimagen para mostrar (que en realidad es un script). El URL de la imagen es el enlace al script que permite eliminar el mensaje deseado.
- Alice debe tener una sesión abierta en su navegador para el sitio web al que apunta Malorie. Es un requisito imprescindible para que el ataque tenga éxito de forma silenciosa, sin requerir una solicitud de autenticación que alertara a Alice. Esta sesión debe disponer de los derechos necesarios para ejecutar la solicitud destructiva de Malorie. No es necesario que haya una pestaña del navegador abierta en el sitio web de destino ni siquiera que el navegador esté iniciado. Basta con que la sesión esté activa.
- Alice lee el mensaje de Malorie; su navegador utiliza la sesión abierta de Alice y no solicita autenticación interactiva. Intenta recuperar el contenido de la imagen. Al hacerlo, el navegador activa el enlace y borra el mensaje, y recupera una página web de texto como contenido de la imagen. Al no reconocer el tipo de imagen asociado, no muestra ninguna imagen y Alice no sabe que Malorie acaba de hacer que borre un mensaje en contra de su voluntad.
Incluso explicada así, la técnica del CSRF es difícil de entender. Hagamos un esquema:

- en [1-2], Alice se conecta al foro (Sitio A). Este foro mantiene una sesión para cada usuario. El navegador de Alice almacena localmente esta cookie de sesión y la reenvía cada vez que realiza una nueva solicitud al sitio A;
- en [3], Malorie envía un mensaje a Alice. Esta lo lee con su navegador. El mensaje leído está en formato HTML y contiene un enlace a una imagen del sitio B. De hecho, este enlace es un enlace a un script Javascript que se ejecuta una vez que llega al navegador de Alice;
- este script Javascript realiza entonces una solicitud al sitio A. El navegador de Alice envía automáticamente la solicitud con la cookie de sesión almacenada localmente. Aquí es donde se produce el ataque: Malorie ha conseguido consultar el sitio A con los derechos (sesión) de Alice. A partir de ahí, pase lo que pase, el ataque se ha producido;
Para contrarrestar este tipo de ataque, el sitio A puede proceder de la siguiente manera:
- en cada intercambio [1-2] con Alice, el sitio A envía una clave, denominada a partir de entonces token (ficha) CSRF, que Alice debe devolverle en la siguiente solicitud. Así, Alice debe enviar en cada solicitud dos datos:
- la cookie de sesión;
- el token CSRF recibido en la respuesta a su última solicitud al sitio A;
Ahí está la protección: si el navegador reenvía automáticamente al sitio A la cookie de sesión, no lo hace con el token CSRF. Por este motivo, el intercambio 6-7 realizado por el script de ataque será rechazado, ya que la solicitud 6 no habrá enviado el token CSRF;
El sitio A puede enviar a Alice el token CSRF de diversas formas para una aplicación HTML:
- puede enviar en cada solicitud una página HTML en la que todos los enlaces tendrán el token CSRF, por ejemplo, [http://siteA/chemin/csrf_token]. En la siguiente solicitud, cuando Alice haga clic en uno de estos enlaces, el sitio A solo tendrá que recuperar el token CSRF del URL de la solicitud y comprobar que es correcto. Esto es lo que se hará aquí;
- en el caso de las páginas HTML que contengan un formulario, puede enviar este último con un campo oculto [input type=’hidden’] que contenga el token CSRF. Este se enviará automáticamente con el formulario cuando Alice valide la página. El sitio A recuperará el token CSRF en el cuerpo (body) de la solicitud;
- se pueden utilizar otras técnicas;
34.2. Configuración

Introducimos en la configuración [parameters] de la aplicación dos valores booleanos:
- [with_redissession]: si es True, la aplicación utiliza una sesión Redis. Si es False, la aplicación utiliza una sesión Flask normal;
- [with_csrftoken]: si es True, los URL de la aplicación contienen un token CSRF;
# tiempo de pausa del hilo en segundos
"sleep_time": 0,
# servidor Redis
"with_redissession": True,
"redis": {
"host": "127.0.0.1",
"port": 6379
},
# token CSRF
"with_csrftoken": False,
34.3. Implementación CSRF
Nos aseguraremos de que cuando:
config['parameters']['with_csrftoken']
sea igual a [True], la aplicación envíe al navegador del cliente páginas web cuyos enlaces contengan un token CSRF.
34.3.1. El módulo [flask_wtf]
La implementación del token CSRF se realizará con el módulo [flask_wtf] que instalamos en un terminal PyCharm:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install flask_wtf
Collecting flask_wtf
…
34.3.2. Las plantillas de las vistas
Introducimos una nueva clase en las plantillas:

La clase [AbstractBaseModelForView] es la siguiente:
from abc import abstractmethod
from flask import Request
from flask_wtf.csrf import generate_csrf
from werkzeug.local import LocalProxy
from InterfaceModelForView import InterfaceModelForView
class AbstractBaseModelForView(InterfaceModelForView):
@abstractmethod
def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
pass
def get_csrftoken(self, config: dict):
# csrf_token
if config['parameters']['with_csrftoken']:
return f"/{generate_csrf()}"
else:
return ""
- línea 9: la clase [AbstractBaseModelForView] implementa la interfaz [InterfaceModelForView] implementada por las clases de los modelos;
- líneas 11-13: el método [get_model_for_view] no está implementado;
- líneas 15-20: el método [get_csrftoken] genera el token CSRF si la aplicación se ha configurado para utilizarlos. Según el caso, la función devuelve un token precedido por el signo /; de lo contrario, una cadena vacía. La función [generate_csrf] tiene la particularidad de generar siempre el mismo valor para una solicitud de cliente determinada. El procesamiento de una solicitud implica la ejecución de diferentes funciones. El uso de [generate_csrf] en estas funciones genera siempre el mismo valor. En la siguiente solicitud, sin embargo, se genera un nuevo token CSRF;
Todas las plantillas M de la vista V incluirán el token CSRF de la siguiente manera:
class ModelForAuthentificationView(AbstractBaseModelForView):
def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
# on encapsule les données de la pagé dans modèle
modèle = {}
…
# jeton csrf
modèle['csrf_token'] = super().get_csrftoken(config)
# on rend le modèle
return modèle
- cada clase de modelo extiende la clase base [AbstractBaseModelForView];
- línea 8: se solicita el token CSRF a la clase padre. Se obtiene o bien la cadena vacía, o bien una cadena del tipo [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c];
34.3.3. Las vistas
Por lo que acabamos de ver, todas las vistas V tendrán en su modelo M el token CSRF. Por lo tanto, podrán utilizarlo en los enlaces que contengan. Veamos algunos ejemplos:
El fragmento de autenticación [v_authentification.html]
<!-- formulario HTML - se envían sus valores con la acción [authentifier-utilisateur] -->
<form method="post" action="/authentifier-utilisateur{{modèle.csrf_token}}">
<!-- título -->
<div class="alert alert-primary" role="alert">
<h4>Veuillez vous authentifier</h4>
</div>
…
</form>
- línea 2: según lo que acabamos de ver, el URL del atributo [action] será:
[/authentifier-utilisateur/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c]
o
dependiendo de si la aplicación se ha configurado para utilizar o no tokens CSRF;
El fragmento de cálculo del impuesto [v-calcul-impot.html]
<!-- formulario HTML enviado -->
<form method="post" action="/calculer-impot{{modèle.csrf_token}}">
<!-- mensaje en 12 columnas sobre fondo azul -->
<div class="col-md-12">
<div class="alert alert-primary" role="alert">
<h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
</div>
</div>
…
</form>
El fragmento de simulaciones [v-liste-simulations.html]
{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- mensaje sobre fondo azul -->
<div class="alert alert-primary" role="alert">
<h4>Votre liste de simulations est vide</h4>
</div>
{% endif %}
{% if modèle.simulations is defined and modèle.simulations|length!=0 %}
<!-- mensaje sobre fondo azul -->
<div class="alert alert-primary" role="alert">
<h4>Liste de vos simulations</h4>
</div>
<!-- tabla de simulaciones -->
<table class="table table-sm table-hover table-striped">
…
<!-- cuerpo de la tabla (datos mostrados) -->
<tbody>
<!-- se muestra cada simulación recorriendo la tabla de simulaciones -->
{% for simulation in modèle.simulations %}
<!-- visualización de una fila de la tabla con 6 columnas - etiqueta <tr> -->
<!-- columna 1: encabezado de fila (n.º de simulación) - etiqueta <th scope='row' -->
<!-- columna 2: valor del parámetro [marié] - etiqueta <td> -->
<!-- columna 3: valor del parámetro [enfants] - etiqueta <td> -->
<!-- columna 4: valor del parámetro [salaire] - etiqueta <td> -->
<!-- columna 5: valor del parámetro [impôt] (del impuesto) - etiqueta <td> -->
<!-- columna 6: valor del parámetro [surcôte] - etiqueta <td> -->
<!-- columna 7: valor del parámetro [décôte] - etiqueta <td> -->
<!-- columna 8: valor del parámetro [réduction] - etiqueta <td> -->
<!-- columna 9: valor del parámetro [taux] (del impuesto) - etiqueta <td> -->
<!-- columna 10: enlace para eliminar la simulación - etiqueta <td> -->
<tr>
<th scope="row">{{simulation.id}}</th>
<td>{{simulation.marié}}</td>
<td>{{simulation.enfants}}</td>
<td>{{simulation.salaire}}</td>
<td>{{simulation.impôt}}</td>
<td>{{simulation.surcôte}}</td>
<td>{{simulation.décôte}}</td>
<td>{{simulation.réduction}}</td>
<td>{{simulation.taux}}</td>
<td><a href="/supprimer-simulation/{{simulation.id}}{{modèle.csrf_token}}">Supprimer</a></td>
</tr>
{% endfor %}
</tr>
</tbody>
</table>
{% endif %}
El fragmento del menú [v-menu.html]
<!-- menú Bootstrap -->
<nav class="nav flex-column">
<!-- visualización de una lista de enlaces HTML -->
{% for optionMenu in modèle.optionsMenu %}
<a class="nav-link" href="{{optionMenu.url}}{{modèle.csrf_token}}">{{optionMenu.text}}</a>
{% endfor %}
</nav>
34.3.4. Las rutas
Ahora hay dos tipos de rutas, dependiendo de si utilizan o no un token CSRF:

- [routes_without_csrftoken] son las rutas sin token CSRF. Son las rutas de la version anterior;
- [routes_with_csrftoken] son las rutas con el token CSRF.
En [routes_with_csrftoken], las rutas tienen ahora un parámetro adicional, el token CSRF:
# el controlador frontal
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", csrf_token=generate_csrf()), status.HTTP_302_FOUND)
# init-session
@app.route('/init-session/<string:type_response>/<string:csrf_token>', methods=['GET'])
def init_session(type_response: str, csrf_token: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# autenticar-usuario
@app.route('/authentifier-utilisateur/<string:csrf_token>', methods=['POST'])
def authentifier_utilisateur(csrf_token: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# calcular-impuesto
@app.route('/calculer-impot/<string:csrf_token>', methods=['POST'])
def calculer_impot(csrf_token: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# cálculo del impuesto por lotes
@app.route('/calculer-impots/<string:csrf_token>', methods=['POST'])
def calculer_impots(csrf_token: str):
# se ejecuta el controlador asociado a la acción
return front_controller()
# listar simulaciones
@app.route('/lister-simulations/<string:csrf_token>', methods=['GET'])
def lister_simulations(csrf_token: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# eliminar-simulación
@app.route('/supprimer-simulation/<int:numero>/<string:csrf_token>', methods=['GET'])
def supprimer_simulation(numero: int, csrf_token: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# fin-sesión
@app.route('/fin-session/<string:csrf_token>', methods=['GET'])
def fin_session(csrf_token: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# mostrar-cálculo-impuestos
@app.route('/afficher-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_calcul_impot(csrf_token: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# get-admindata
@app.route('/get-admindata/<string:csrf_token>', methods=['GET'])
def get_admindata(csrf_token: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
Todas las rutas tienen ahora el token CSRF en sus parámetros, incluso la ruta [/init-session]. Esto significa que el cliente no puede iniciar la aplicación escribiendo directamente URL [/init-session/html], ya que le faltará el token CSRF. Ahora debe pasar obligatoriamente por URL y [/] de las líneas 7-10.
La elección de las rutas se realiza en el script principal [main]:
…
# 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
if config['parameters']['with_csrftoken']:
import routes_with_csrftoken as routes
else:
import routes_without_csrftoken as routes
# Configuración de las rutas
routes.config = config
# inicio de la aplicación Flask
routes.execute(__name__)
- líneas 9-13: selección de rutas según si la aplicación utiliza o no tokens CSRF;
34.3.5. El controlador [MainController]
En cada solicitud, el servidor debe comprobar la presencia del token CSRF. Lo haremos en el controlador principal [MainController], por el que pasan todas las solicitudes:
from flask_wtf.csrf import generate_csrf, validate_csrf
…
# se procesa la solicitud
try:
# registrador
logger = Logger(config['parameters']['logsFilename'])
…
# se recuperan los elementos de path
params = request.path.split('/')
# la acción es el primer elemento
action = params[1]
…
if config['parameters']['with_csrftoken']:
# el csrf_token es el último elemento del path
csrf_token = params.pop()
# se comprueba la validez del token
# se lanzará una excepción si el csrf_token no es el esperado
validate_csrf(csrf_token)
…
except ValidationError as exception:
# token CSRF no válido
résultat = {"action": action, "état": 121, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
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 añade csrf_token al resultado
résultat['csrf_token'] = generate_csrf()
# se registra el resultado enviado al cliente
log = f"[MainController] {résultat}\n"
logger.write(log)
- línea 20: recuperamos el token CSRF del URL de la solicitud de tipo [http://machine :port/chemin/action/param1/param2/…/csrf_token]. El token de sesión es siempre el último elemento del URL;
- línea 23: se comprueba la validez del token CSRF recuperado en el URL con el token CSRF de la sesión. Si no es válido, la función [validate_csrf] lanza una excepción de tipo [ValidationError] (línea 27);
- línea 41: el token CSRF se incluye en el resultado enviado al cliente. Los clients, jSON y XML lo necesitarán. De hecho, estos clients no reciben páginas HTML con el token CSRF en los enlaces contenidos en las páginas. Por lo tanto, lo recibirán en el resultado jSON o XML enviado por el servidor;
Nota: la función [validate_csrf] de la línea 23 no comprueba una coincidencia exacta. El token CSRF se almacena en la sesión con la clave [csrf_token]. Las pruebas parecen indicar que un token CSRF es válido si se ha generado durante la sesión. Así, si manualmente, en el URL mostrado en el navegador, por ejemplo (/lister-simulations/xyz), sustituye el token [xyz] CSRF por otro [abc] ya recibido en una acción anterior, la acción [/lister-simulations] se ejecutará con éxito;
34.4. Pruebas con un navegador
Se:
- inicie el servidor con el parámetro [with_csrftoken] a [True];
- solicita el URL [http://localhost:5000] con un navegador;

- en [1], el token CSRF;
Realicemos las operaciones necesarias hasta obtener una lista de simulaciones:

Ahora, escribamos a mano URL [http://localhost:5000/supprimer-simulation/1/x] para eliminar la simulación de id=1. Introducimos deliberadamente un token CSRF incorrecto para ver qué ocurre. La respuesta del servidor es la siguiente:

Nota 1: no es seguro que el método utilizado aquí sea siempre suficiente para contrarrestar los ataques CSRF. Volvamos al esquema del ataque:

Si el script Javascript descargado en [5] es capaz de leer el historial del navegador utilizado por Alice, podrá recuperar los URL ejecutados por el navegador, los URL como [/cible/csrf_token]. Entonces podrá recuperar el token de sesión [csrf_token] y llevar a cabo su ataque en [6-7]. No obstante, el navegador solo permite el uso del historial de la ventana del navegador en la que se ejecuta el script. Por lo tanto, si Alice no utiliza la misma ventana para trabajar con el sitio A [1-2] y leer el mensaje de Malorie [3], el ataque CSRF no será posible.
34.5. Clients consola
Otra forma de probar la versión 14 de la aplicación es retomar las pruebas de la versión 12 y adaptarlas al nuevo servidor.

La carpeta [impots/http-clients/09] se obtiene inicialmente copiando la carpeta [impots/http-clients/07]. A continuación, se modifica.
Volvamos a las rutas que inicializan una sesión:
# raíz de la aplicación
@app.route('/', methods=['GET'])
def index() -> tuple:
# redirección a /init-session/html
return redirect(url_for("init_session", type_response="html", csrf_token=generate_csrf()), status.HTTP_302_FOUND)
# init-session-with-csrf-token
@app.route('/init-session/<string:type_response>/<string:csrf_token>', methods=['GET'])
def init_session(type_response: str, csrf_token: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
Ninguna de estas rutas es adecuada para inicializar una sesión jSON o XML:
- líneas 2-5: la ruta [/] inicializa una sesión HTML;
- líneas 8-11: la ruta [/init-session] requiere un token CSRF que desconocemos;
Decidimos añadir una nueva ruta al servidor:
# inicio de sesión sin token CSRF
@app.route('/init-session-without-csrftoken/<string:type_response>', methods=['GET'])
def init_session_without_csrftoken(type_response: str) -> tuple:
# redirección a /init-session/type_response
return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()), status.HTTP_302_FOUND)
- línea 2: la nueva ruta. No espera ningún token CSRF. Así, hemos vuelto a la ruta [/init-session] de la anterior version;
- líneas 4-5: se redirige al cliente (jSON, XML, HTML) a la ruta [/init-session] que tiene el token CSRF en sus parámetros;
Se puede probar esta nueva ruta con un navegador:

La respuesta del servidor (configurado con [with_csrftoken=True]) es la siguiente:

- en [1], el servidor ha sido redirigido a la ruta [/init-session] con el token CSRF en el URL;
- en [2], el token CSRF se encuentra en el diccionario jSON enviado por el servidor asociado a la clave [csrf_token];
Volvamos al código del cliente:

Modificamos la configuración [config] de la siguiente manera:
config.update({
# fichier des contribuables
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
# fichier des résultats
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
# fichier des erreurs
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
# fichier de logs
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# le serveur de calcul de l'impôt
"server": {
"urlServer": "http://127.0.0.1:5000",
"user": {
"login": "admin",
"password": "admin"
},
"url_services": {
"calculate-tax": "/calculer-impot",
"get-admindata": "/get-admindata",
"calculate-tax-in-bulk-mode": "/calculer-impots",
"init-session": "/init-session-without-csrftoken",
"end-session": "/fin-session",
"authenticate-user": "/authentifier-utilisateur",
"get-simulations": "/lister-simulations",
"delete-simulation": "/supprimer-simulation",
}
},
# mode debug
"debug": True,
# csrf_token
"with_csrftoken": True,
}
)
…
# route init-session
url_services = config['server']['url_services']
if config['with_csrftoken']:
url_services['init-session'] = '/init-session-without-csrftoken'
else:
url_services['init-session'] = '/init-session'
- línea 31: un valor booleano indicará al cliente si el servidor al que se dirige trabaja o no con tokens CSRF;
- líneas 37-40: se establece el URL de servicio de la acción [init-session]:
- si el servidor utiliza tokens CSRF, entonces el URL de servicio es [/init-session-without-csrftoken];
- de lo contrario, el servicio URL es [/init-session];
Se ha presentado la ruta [/init-session-without-csrftoken]. Permite a un cliente jSON / XML iniciar una sesión con el servidor sin poseer un token CSRF. Encontrará este token en la respuesta del servidor.
A continuación, modificamos la clase [ImpôtsDaoWithHttpSession] que implementa la capa [dao] del cliente:

# importaciones
import json
import requests
import xmltodict
from flask_api import status
from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDaoWithHttpSession import InterfaceImpôtsDaoWithHttpSession
from TaxPayer import TaxPayer
class ImpôtsDaoWithHttpSession(InterfaceImpôtsDaoWithHttpSession):
# constructor
def __init__(self, config: dict):
# inicialización del padre
AbstractImpôtsDao.__init__(self, config)
# almacenamiento de elementos de configuración
# config general
self.__config = config
# servidor
self.__config_server = config["server"]
# servicios
self.__config_services = config["server"]['url_services']
# modo debug
self.__debug = config["debug"]
# registrador
self.__logger = None
# cookies
self.__cookies = None
# tipo de sesión (json, xml)
self.__session_type = None
# token CSRF
self.__csrf_token = None
# etapa de solicitud/respuesta
def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
# [method]: método HTTP GET o POST
# [url_service]: URL de servicio
# [data]: parámetros del POST en x-www-form-urlencoded
# [json]: parámetros de POST en json
# [cookies]: cookies que se deben incluir en la solicitud
#: debe haber una sesión XML o JSON; de lo contrario, no se podrá gestionar la respuesta
if self.__session_type not in ['json', 'xml']:
raise ImpôtsError(73, "il n'y a pas de session valide en cours")
# se añade el token CSRF al URL de servicio
if self.__csrf_token:
url_service = f"{url_service}/{self.__csrf_token}"
# ejecución de la solicitud
response = requests.request(method,
url_service,
data=data_value,
json=json_value,
cookies=self.__cookies,
allow_redirects=True)
# ¿modo debug?
if self.__debug:
# registrador
if not self.__logger:
self.__logger = self.__config['logger']
# iniciando sesión
self.__logger.write(f"{response.text}\n")
# resultado
if self.__session_type == "json":
résultat = json.loads(response.text)
else: # xml
résultat = xmltodict.parse(response.text[39:])['root']
# se recuperan las cookies de la respuesta si las hay
if response.cookies:
self.__cookies = response.cookies
# se recupera el token CSRF
if self.__config['with_csrftoken']:
self.__csrf_token = résultat.get('csrf_token', None)
# código de estado
status_code = response.status_code
# si el código de estado es distinto de 200 OK
if status_code != status.HTTP_200_OK:
raise ImpôtsError(35, résultat['réponse'])
# se devuelve el resultado
return résultat['réponse']
def init_session(self, session_type: str):
# se anota el tipo de sesión
self.__session_type = session_type
# se elimina el token CSRF de las llamadas anteriores
self.__csrf_token = None
# se solicita el URL de la acción init-session
url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# ejecución de la solicitud
self.get_response("GET", url_service)
…
- líneas 38-92: la gestión del token CSRF se lleva a cabo principalmente en el método [get_response];
- línea 60: el punto importante es el parámetro [allow_redirects=True]. Es su valor por defecto, pero hemos querido resaltarlo;
Cuando se está en modo [with_csrftoken=True]:
- los clients inician su diálogo con el servidor mediante la llamada a la ruta [/init-session_without_csftoken/type_response];
- El servidor responde a esta solicitud con una redirección a la ruta [/init-session/type_response/csrf_token];
- debido al parámetro [allow_redirects=True], el cliente [requests] seguirá esta redirección;
- el token CSRF se encontrará en el resultado recuperado en las líneas 72 y 74 asociado a la clave [csrf_token];
Cuando se está en modo [with_csrftoken=False]:
- (continuación)
- los clients inician su diálogo con el servidor mediante la llamada a la ruta [/init-session /type_response];
- el servidor responde a esta solicitud con una redirección a la ruta [/init-session/type_response];
- debido al parámetro [allow_redirects=True], esta redirección será seguida por el cliente [requests];
- no hay ningún token CSRF que recuperar en las líneas 81-82. La propiedad [self.__csrf_token] sigue entonces siendo None (línea 36);
- líneas 51-52: para todas las solicitudes siguientes, el token CSRF, si existe, se añade a la ruta inicial;
- líneas 81-82: el nuevo token que genera el servidor con cada nueva solicitud del cliente se almacena localmente para ser devuelto en la línea 52 a la siguiente solicitud;
Por otra parte, el método [init_session] evoluciona ligeramente:
def init_session(self, session_type: str):
# se anota el tipo de sesión
self.__session_type = session_type
# se elimina el token CSRF de las llamadas anteriores
self.__csrf_token = None
# se solicita el URL de la acción init-session
url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# ejecución de la consulta
self.get_response("GET", url_service)
Hay que recordar aquí que se ha creado una ruta [/init-session-without-csrftoken/<type-response>] para inicializar el diálogo cliente/servidor sin el token CSRF. Ahora bien, hemos visto que el método [get_response], llamado en la línea 12 del código, añade sistemáticamente al final del servicio URL el token CSRF almacenado en [self.__csrf_token]. Por eso, en la línea 6 del código se elimina este token CSRF si existía.
Eso es todo. Para las pruebas, ejecutaremos:
- la consola clients y [main, main2, main3];
- las clases de prueba [Test1HttpClientDaoWithSession] y [Test2HttpClientDaoWithSession];
estableciendo sucesivamente en True y luego en False el parámetro de configuración [with_csrftoken].

A continuación se muestran, a modo de ejemplo, los registros obtenidos al ejecutar el cliente [main json] con [with_csrftoken=True]:
2020-08-08 16:33:23.317903, MainThread : début du calcul de l'impôt des contribuables
2020-08-08 16:33:23.317903, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.317903, Thread-2 : début du calcul de l'impôt des 2 contribuables
2020-08-08 16:33:23.317903, Thread-3 : début du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.317903, Thread-4 : début du calcul de l'impôt des 1 contribuables
2020-08-08 16:33:23.379221, Thread-2 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.381073, Thread-4 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.386982, Thread-3 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.390269, Thread-1 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"], "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.413206, Thread-2 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.422877, Thread-2 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 2}], "csrf_token": "ImFiZmZkYjZmMzFkZDc2YWRjNWYwOGM0NTBmMGM4ODJjYzViOWI4NGEi.Xy63sw.H5L0--yWsvfaWvggrGw78z5VnN0"}
2020-08-08 16:33:23.428622, Thread-4 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.429127, Thread-3 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.429127, Thread-1 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie", "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.429127, Thread-2 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "IjU1YjlmZDA0OWRhNTJlODFmYjgyYjlhM2ExYWNhZmUzNTk2NjA5NGIi.Xy63sw.nyNSvkcG6iG0oIMBjtYPo8ySgdw"}
2020-08-08 16:33:23.438519, Thread-2 : fin du calcul de l'impôt des 2 contribuables
2020-08-08 16:33:23.443033, Thread-4 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}], "csrf_token": "ImY5YzQyMjlkYzcyYmM4YmZiMGI0NWY5MjE4MzIzNDExZjc0MGQ3MWQi.Xy63sw.q6olg7IP_g2ro_RBFRCX1BX90g8"}
2020-08-08 16:33:23.446510, Thread-3 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 4}], "csrf_token": "IjkxZGNlN2YyMmUxMjQ0M2Y0MTdjNDQ4ZmQ1MDMxZjkwNjBhNzAzZjMi.Xy63sw.-6buL11No3UJBlElpW4tX4B-lp0"}
2020-08-08 16:33:23.453477, Thread-1 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}], "csrf_token": "IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
2020-08-08 16:33:23.457912, Thread-4 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "IjQ0ZDQxODgzN2M5NjRiYWI0NjA2MTk5YWFkNGFhMzY1M2IxNWMyNDIi.Xy63sw.mOa5MKXvJ-EXf_qEok-OqC5j_mg"}
2020-08-08 16:33:23.458442, Thread-4 : fin du calcul de l'impôt des 1 contribuables
2020-08-08 16:33:23.459045, Thread-3 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "ImQ0NDZlYmViYjY1ZDUxYzJhMTNmM2JiZTRkMjBjZGJkYzE0OGVkYzMi.Xy63sw.fviTJz4zFDqVLlVlkrosT_JRPww"}
2020-08-08 16:33:23.459700, Thread-3 : fin du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.460492, Thread-1 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée", "csrf_token": "Ijg3MjQ1NGUyYTUyOGEyNTdmZmNmYWZkMmU2OTgyMzUwNjI1YTlhZjIi.Xy63sw.I0xBl9Q8DzsuXPSgOdeARc_VKBA"}
2020-08-08 16:33:23.460492, Thread-1 : fin du calcul de l'impôt des 4 contribuables
2020-08-08 16:33:23.460492, MainThread : fin du calcul de l'impôt des contribuables
Si observamos los tokens CSRF recibidos sucesivamente, vemos que todos son diferentes.