34. Exercício prático: versão 14

A pasta [http-servers/09] da versão 14 é obtida através da cópia da pasta [http-servers/08] da versão 13.
34.1. Introdução
CSRF (Cross Site Request Forgery) é uma técnica de roubo de sessão. É explicada da seguinte forma na Wikipédia (https://fr.wikipedia.org/wiki/Cross-site_request_forgery):
- Malorie consegue descobrir o link que permite eliminar a mensagem em questão.
- Malorie envia uma mensagem a Alice contendo uma pseudo-imagem para visualizar (que, na verdade, é um script). O URL da imagem é o link para o script que permite eliminar a mensagem pretendida.
- A Alice tem de ter uma sessão aberta no seu navegador para o site visado pela Malorie. Esta é uma condição necessária para que o ataque seja bem-sucedido de forma silenciosa, sem exigir um pedido de autenticação que alertasse a Alice. Esta sessão tem de dispor dos direitos necessários para executar a solicitação destrutiva da Malorie. Não é necessário que esteja aberta uma aba do navegador no site alvo, nem sequer que o navegador esteja em execução. Basta que a sessão esteja ativa.
- A Alice lê a mensagem da Malorie; o seu navegador utiliza a sessão aberta da Alice e não solicita autenticação interativa. Tenta recuperar o conteúdo da imagem. Ao fazê-lo, o navegador aciona o link e apaga a mensagem, recuperando uma página web de texto como conteúdo para a imagem. Como não reconhece o tipo de imagem associado, não exibe nenhuma imagem e a Alice não sabe que a Malorie acabou de a obrigar a apagar uma mensagem contra a sua vontade.
Mesmo explicada desta forma, a técnica do CSRF é difícil de compreender. Vamos fazer um esquema:

- em [1-2], a Alice comunica com o fórum (Site A). Este fórum mantém uma sessão para cada utilizador. O navegador da Alice armazena localmente este cookie de sessão e reenvia-o sempre que faz uma nova solicitação ao Site A;
- em [3], a Malorie envia uma mensagem à Alice. Esta lê-a com o seu navegador. A mensagem lida está no formato HTML e contém um link para uma imagem do site B. Na verdade, esse link é um link para um script JavaScript que é executado assim que chega ao navegador da Alice;
- esse script JavaScript efetua então uma solicitação ao site A. O navegador de Alice envia automaticamente a solicitação com o cookie de sessão armazenado localmente. É aqui que ocorre o ataque: Malorie conseguiu consultar o site A com os direitos (sessão) de Alice. Depois disso, independentemente do que aconteça, o ataque já ocorreu;
Para contrariar este tipo de ataque, o site A pode proceder da seguinte forma:
- a cada troca [1-2] com a Alice, o site A envia uma chave, doravante designada por token CSRF, que a Alice deve reenviar na solicitação seguinte. Assim, a Alice deve enviar, em cada solicitação, duas informações:
- o cookie de sessão;
- o token CSRF recebido na resposta à sua última solicitação ao site A;
A proteção reside aqui: embora o navegador reenvie automaticamente ao site A o cookie de sessão, não faz o mesmo com o token CSRF. Por este motivo, a troca 6-7 efetuada pelo script de ataque será recusada, uma vez que a solicitação 6 não terá enviado o token CSRF;
O site A pode enviar a Alice o token CSRF de várias formas para uma aplicação HTML:
- pode, em cada pedido, enviar uma página HTML em que todos os links tenham o token CSRF, por exemplo, [http://siteA/chemin/csrf_token]. Na solicitação seguinte, quando a Alice clicar num desses links, o site A terá apenas de recuperar o token CSRF do URL da solicitação e verificar se está correto. É isso que será feito aqui;
- pode, no caso das páginas HTML que contenham um formulário, enviar este último com um campo oculto [input type=’hidden’] contendo o token CSRF. Este será então enviado automaticamente com o formulário quando a Alice submeter a página. O site A irá recuperar o token CSRF no corpo (body) da solicitação;
- outras técnicas são possíveis;
34.2. Configuração

Introduzimos na configuração [parameters] da aplicação dois valores booleanos:
- [with_redissession]: quando definido como True, a aplicação utiliza uma sessão Redis. Quando definido como False, a aplicação utiliza uma sessão Flask normal;
- [with_csrftoken]: quando definido como True, os URL da aplicação contêm um token CSRF;
# duração da pausa do thread em segundos
"sleep_time": 0,
# servidor Redis
"with_redissession": True,
"redis": {
"host": "127.0.0.1",
"port": 6379
},
# token CSRF
"with_csrftoken": False,
34.3. Implementação CSRF
Vamos garantir que, quando:
config['parameters']['with_csrftoken']
for igual a [True], a aplicação envie ao navegador do cliente páginas web cujos links contenham um token CSRF.
34.3.1. O módulo [flask_wtf]
A implementação do token CSRF será feita com o módulo [flask_wtf], que instalamos num 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. Os modelos das vistas
Introduzimos uma nova classe nos modelos:

A classe [AbstractBaseModelForView] é a seguinte:
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 ""
- linha 9: a classe [AbstractBaseModelForView] implementa a interface [InterfaceModelForView] implementada pelas classes dos modelos;
- linhas 11-13: o método [get_model_for_view] não está implementado;
- linhas 15-20: o método [get_csrftoken] gera o token CSRF se a aplicação tiver sido configurada para os utilizar. Consoante o caso, a função devolve um token precedido pelo sinal /; caso contrário, devolve uma cadeia vazia. A função [generate_csrf] tem a particularidade de gerar sempre o mesmo valor para uma determinada solicitação do cliente. O processamento de uma solicitação implica a execução de várias funções. A utilização de [generate_csrf] nessas funções gera sempre o mesmo valor. Na solicitação seguinte, por outro lado, é gerado um novo token CSRF;
Todos os modelos M da vista V incluirão o token CSRF da seguinte forma:
class ModelForAuthentificationView(AbstractBaseModelForView):
def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
# encapsulamos os dados da página no modelo
modèle = {}
…
# token CSRF
modèle['csrf_token'] = super().get_csrftoken(config)
# retorna-se o modelo
return modèle
- cada classe de modelo estende a classe base [AbstractBaseModelForView];
- linha 8: o token CSRF é solicitado à classe pai. Obtém-se ou a cadeia vazia, ou uma cadeia do tipo [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c];
34.3.3. As visualizações
Pelo que acabámos de ver, todas as vistas V terão no seu modelo M o token CSRF. Poderão, portanto, utilizá-lo nos links que contêm. Vejamos alguns exemplos:
O fragmento de autenticação [v_authentification.html]
<!-- formulário HTML - enviamos os seus valores com a ação [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>
- linha 2: de acordo com o que acabámos de ver, o URL do atributo [action] será:
[/authentifier-utilisateur/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c]
ou
dependendo de a aplicação ter sido configurada para utilizar ou não tokens CSRF;
O fragmento de cálculo do imposto [v-calcul-impot.html]
<!-- formulário HTML enviado -->
<form method="post" action="/calculer-impot{{modèle.csrf_token}}">
<!-- mensagem em 12 colunas sobre fundo 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>
O fragmento das simulações [v-liste-simulations.html]
{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- mensagem sobre fundo 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 %}
<!-- mensagem sobre fundo azul -->
<div class="alert alert-primary" role="alert">
<h4>Liste de vos simulations</h4>
</div>
<!-- tabela de simulações -->
<table class="table table-sm table-hover table-striped">
…
<!-- corpo da tabela (dados apresentados) -->
<tbody>
<!-- cada simulação é apresentada ao percorrer a tabela de simulações -->
{% for simulation in modèle.simulations %}
<!-- exibição de uma linha da tabela com 6 colunas - baliza <tr> -->
<!-- coluna 1: cabeçalho da linha (n.º da simulação) — baliza <th scope='row' -->
<!-- coluna 2: valor do parâmetro [marié] - baliza <td> -->
<!-- coluna 3: valor do parâmetro [enfants] - baliza <td> -->
<!-- coluna 4: valor do parâmetro [salaire] - tag <td> -->
<!-- coluna 5: valor do parâmetro [impôt] (do imposto) - baliza <td> -->
<!-- coluna 6: valor do parâmetro [surcôte] - baliza <td> -->
<!-- coluna 7: valor do parâmetro [décôte] - baliza <td> -->
<!-- coluna 8: valor do parâmetro [réduction] - tag <td> -->
<!-- coluna 9: valor do parâmetro [taux] (do imposto) - tag <td> -->
<!-- coluna 10: link para eliminar a simulação - tag <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 %}
O fragmento do menu [v-menu.html]
<!-- menu Bootstrap -->
<nav class="nav flex-column">
<!-- exibição de uma lista de links 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. As estradas
Existem agora dois tipos de estradas, consoante utilizem ou não um token CSRF:

- [routes_without_csrftoken] são as rotas sem token CSRF. Estas são as rotas da versão anterior;
- [routes_with_csrftoken] são as rotas com o token CSRF.
Em [routes_with_csrftoken], as rotas têm agora um parâmetro adicional, o token CSRF:
# o controlador frontal
def front_controller() -> tuple:
# a solicitação é encaminhada para o controlador principal
main_controller = config['mvc']['controllers']['main-controller']
return main_controller.execute(request, session, config)
@app.route('/', methods=['GET'])
def index() -> tuple:
# redirecionamento para /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:
# é executado o controlador associado à ação
return front_controller()
# autenticar-utilizador
@app.route('/authentifier-utilisateur/<string:csrf_token>', methods=['POST'])
def authentifier_utilisateur(csrf_token: str) -> tuple:
# executa-se o controlador associado à ação
return front_controller()
# calcular-imposto
@app.route('/calculer-impot/<string:csrf_token>', methods=['POST'])
def calculer_impot(csrf_token: str) -> tuple:
# executa-se o controlador associado à ação
return front_controller()
# cálculo do imposto em lotes
@app.route('/calculer-impots/<string:csrf_token>', methods=['POST'])
def calculer_impots(csrf_token: str):
# executa-se o controlador associado à ação
return front_controller()
# listar simulações
@app.route('/lister-simulations/<string:csrf_token>', methods=['GET'])
def lister_simulations(csrf_token: str) -> tuple:
# executa-se o controlador associado à ação
return front_controller()
# eliminar-simulação
@app.route('/supprimer-simulation/<int:numero>/<string:csrf_token>', methods=['GET'])
def supprimer_simulation(numero: int, csrf_token: str) -> tuple:
# executa-se o controlador associado à ação
return front_controller()
# fim-sessão
@app.route('/fin-session/<string:csrf_token>', methods=['GET'])
def fin_session(csrf_token: str) -> tuple:
# executa-se o controlador associado à ação
return front_controller()
# exibir-cálculo-impostos
@app.route('/afficher-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_calcul_impot(csrf_token: str) -> tuple:
# executa-se o controlador associado à ação
return front_controller()
# obter-dados-administrativos
@app.route('/get-admindata/<string:csrf_token>', methods=['GET'])
def get_admindata(csrf_token: str) -> tuple:
# é executado o controlador associado à ação
return front_controller()
Todas as rotas têm agora o token CSRF nos seus parâmetros, incluindo a rota [/init-session]. Isto significa que o cliente não pode iniciar a aplicação digitando diretamente o URL [/init-session/html], pois faltará o token CSRF. Agora, tem de passar obrigatoriamente pelas linhas 7 a 10: URL e [/].
A escolha das rotas é feita no script principal [main]:
…
# o thread principal já não necessita do logger
logger.close()
# se tiver ocorrido um erro, o processo é interrompido
if erreur:
sys.exit(2)
# importação das rotas da aplicação web
if config['parameters']['with_csrftoken']:
import routes_with_csrftoken as routes
else:
import routes_without_csrftoken as routes
# configuração das rotas
routes.config = config
# inicialização da aplicação Flask
routes.execute(__name__)
- linhas 9-13: escolha das rotas consoante a aplicação utilize ou não tokens CSRF;
34.3.5. O controlador [MainController]
Em cada pedido, o servidor deve verificar a presença do token CSRF. Faremos isso no controlador principal [MainController], que recebe todos os pedidos:
from flask_wtf.csrf import generate_csrf, validate_csrf
…
# processar o pedido
try:
# registo
logger = Logger(config['parameters']['logsFilename'])
…
# recuperação dos elementos do caminho
params = request.path.split('/')
# a ação é o primeiro elemento
action = params[1]
…
if config['parameters']['with_csrftoken']:
# o csrf_token é o último elemento do caminho
csrf_token = params.pop()
# verifica-se a validade do token
# será lançada uma exceção se o csrf_token não for o esperado
validate_csrf(csrf_token)
…
except ValidationError as exception:
# token CSRF inválido
résultat = {"action": action, "état": 121, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
except BaseException as exception:
# outras exceções (inesperadas)
résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
finally:
pass
# adiciona-se o csrf_token ao resultado
résultat['csrf_token'] = generate_csrf()
# regista-se o resultado enviado ao cliente
log = f"[MainController] {résultat}\n"
logger.write(log)
- linha 20: recuperamos o token CSRF do URL da solicitação do tipo [http://machine :port/chemin/action/param1/param2/…/csrf_token]. O token de sessão é sempre o último elemento do URL;
- linha 23: verifica-se a validade do token CSRF recuperado do URL com o token CSRF da sessão. Se não for válido, a função [validate_csrf] lança uma exceção do tipo [ValidationError] (linha 27);
- linha 41: o token CSRF é incluído no resultado enviado ao cliente. Os clientes jSON e XML irão precisar dele. Com efeito, estes clientes não recebem páginas HTML com o token CSRF nos links contidos nas páginas. Receberão, portanto, esse token no resultado jSON ou XML enviado pelo servidor;
Nota: a função [validate_csrf] da linha 23 não verifica uma correspondência exata. O token CSRF é armazenado na sessão com a chave [csrf_token]. Os testes parecem indicar que um token CSRF é válido se tiver sido gerado durante a sessão. Assim, se manualmente, no URL apresentado no navegador, por exemplo (/lister-simulations/xyz), substituir o token [xyz] CSRF por outro, [abc], já recebido numa ação anterior, a ação [/lister-simulations] será bem-sucedida;
34.4. Testes com um navegador
Proceda da seguinte forma:
- inicie o servidor com o parâmetro [with_csrftoken] a [True];
- solicite o URL [http://localhost:5000] com um navegador;

- em [1], o token CSRF;
Vamos realizar algumas operações até obtermos uma lista de simulações:

Agora, digitemos manualmente o URL [http://localhost:5000/supprimer-simulation/1/x] para eliminar a simulação com id=1. Introduzimos deliberadamente um token CSRF incorreto para ver o que acontece. A resposta do servidor é a seguinte:

Nota 1: não é certo que o método aqui utilizado seja sempre suficiente para combater os ataques CSRF. Voltemos ao esquema do ataque:

Se o script JavaScript descarregado em [5] for capaz de ler o histórico do navegador utilizado pela Alice, será capaz de recuperar as URL executadas pelo navegador, bem como as URL, tais como a [/cible/csrf_token]. Poderá então recuperar o token de sessão [csrf_token] e lançar o seu ataque em [6-7]. No entanto, o navegador permite apenas a exploração do histórico da janela do navegador na qual o script está a ser executado. Portanto, se a Alice não utilizar a mesma janela para aceder ao site A ([1-2]) e ler a mensagem da Malorie ([3]), o ataque (CSRF) não será possível.
34.5. Clientes de consola
Outra forma de testar a versão 14 da aplicação é retomar os testes da versão 12 e adaptá-los ao novo servidor.

A pasta [impots/http-clients/09] é obtida inicialmente através da cópia da pasta [impots/http-clients/07]. Em seguida, é modificada.
Voltemos às rotas que inicializam uma sessão:
# raiz da aplicação
@app.route('/', methods=['GET'])
def index() -> tuple:
# redirecionamento para /init-session/html
return redirect(url_for("init_session", type_response="html", csrf_token=generate_csrf()), status.HTTP_302_FOUND)
# inicialização da sessão com token CSRF
@app.route('/init-session/<string:type_response>/<string:csrf_token>', methods=['GET'])
def init_session(type_response: str, csrf_token: str) -> tuple:
# executa-se o controlador associado à ação
return front_controller()
Nenhuma destas rotas é adequada para inicializar uma sessão jSON ou XML:
- linhas 2-5: a rota [/] inicializa uma sessão HTML;
- linhas 8-11: a rota [/init-session] requer um token CSRF que desconhecemos;
Decidimos adicionar uma nova rota ao servidor:
# inicialização da sessão sem token CSRF
@app.route('/init-session-without-csrftoken/<string:type_response>', methods=['GET'])
def init_session_without_csrftoken(type_response: str) -> tuple:
# redirecionamento para /init-session/type_response
return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()), status.HTTP_302_FOUND)
- linha 2: a nova rota. Esta não requer o token CSRF. Assim, voltámos à rota [/init-session] da versão anterior;
- linhas 4-5: redirecionamos o cliente (jSON, XML, HTML) para a rota [/init-session], que tem o token CSRF nos seus parâmetros;
É possível experimentar esta nova rota com um navegador:

A resposta do servidor (configurado com [with_csrftoken=True]) é a seguinte:

- em [1], o servidor foi redirecionado para a rota [/init-session] com o token CSRF no URL;
- em [2], o token CSRF encontra-se no dicionário jSON enviado pelo servidor associado à chave [csrf_token];
Voltemos ao código do cliente:

Alteramos a configuração [config] da seguinte forma:
config.update({
# ficheiro dos contribuintes
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
# ficheiro de resultados
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
# ficheiro de erros
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
# ficheiro de registos
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# servidor de cálculo de impostos
"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",
}
},
# modo de depuração
"debug": True,
# csrf_token
"with_csrftoken": True,
}
)
…
# rota 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'
- linha 31: um valor booleano indicará ao cliente se o servidor a que se dirige trabalha ou não com tokens CSRF;
- linhas 37-40: definimos o URL de serviço da ação [init-session]:
- se o servidor utilizar tokens CSRF, então o token de serviço URL é [/init-session-without-csrftoken];
- caso contrário, o serviço URL é o [/init-session];
Foi apresentada a rota [/init-session-without-csrftoken]. Esta permite que um cliente jSON / XML inicie uma sessão com o servidor sem possuir um token CSRF. Encontrará esse token na resposta do servidor.
Em seguida, alteramos a classe [ImpôtsDaoWithHttpSession] que implementa a camada [dao] do cliente:

# importações
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):
# construtor
def __init__(self, config: dict):
# inicialização do pai
AbstractImpôtsDao.__init__(self, config)
# armazenamento dos elementos de configuração
# configuração geral
self.__config = config
# servidor
self.__config_server = config["server"]
# serviços
self.__config_services = config["server"]['url_services']
# modo de depuração
self.__debug = config["debug"]
# registo
self.__logger = None
# cookies
self.__cookies = None
# tipo de sessão (json, xml)
self.__session_type = None
# token CSRF
self.__csrf_token = None
# etapa de pedido/resposta
def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
# [method]: método HTTP, GET ou POST
# [url_service]: URL de serviço
# [data]: parâmetros do POST em x-www-form-urlencoded
# [json]: parâmetros do POST em JSON
# [cookies]: cookies a incluir na solicitação
# é necessário ter uma sessão XML ou JSON; caso contrário, não será possível processar a resposta
if self.__session_type not in ['json', 'xml']:
raise ImpôtsError(73, "il n'y a pas de session valide en cours")
# adiciona-se o token CSRF ao URL de serviço
if self.__csrf_token:
url_service = f"{url_service}/{self.__csrf_token}"
# execução do pedido
response = requests.request(method,
url_service,
data=data_value,
json=json_value,
cookies=self.__cookies,
allow_redirects=True)
# modo de depuração?
if self.__debug:
# registo
if not self.__logger:
self.__logger = self.__config['logger']
# a registar
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']
# recuperam-se os cookies da resposta, caso existam
if response.cookies:
self.__cookies = response.cookies
# recuperam-se o 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
# se o código de estado for diferente de 200, OK
if status_code != status.HTTP_200_OK:
raise ImpôtsError(35, résultat['réponse'])
# retornamos o resultado
return résultat['réponse']
def init_session(self, session_type: str):
# regista-se o tipo de sessão
self.__session_type = session_type
# elimina-se o token CSRF das chamadas anteriores
self.__csrf_token = None
# solicita-se o URL da ação init-session
url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# execução da solicitação
self.get_response("GET", url_service)
…
- linhas 38-92: a gestão do token CSRF ocorre principalmente no método [get_response];
- linha 60: o ponto importante é o parâmetro [allow_redirects=True]. Trata-se do seu valor por predefinição, mas fizemos questão de o destacar;
Quando se está no modo [with_csrftoken=True]:
- os clientes iniciam o seu diálogo com o servidor através da chamada à rota [/init-session_without_csftoken/type_response];
- o servidor responde a este pedido com um redirecionamento para a rota [/init-session/type_response/csrf_token];
- devido ao parâmetro [allow_redirects=True], este redirecionamento será seguido pelo cliente [requests];
- o token CSRF será encontrado no resultado obtido nas linhas 72 e 74, associado à chave [csrf_token];
Quando se está no modo [with_csrftoken=False]:
- (continuação)
- os clientes iniciam o seu diálogo com o servidor através da chamada à rota [/init-session /type_response];
- o servidor responde a este pedido com um redirecionamento para a rota [/init-session/type_response];
- devido ao parâmetro [allow_redirects=True], este redirecionamento será seguido pelo cliente [requests];
- não há nenhum token CSRF para recuperar nas linhas 81-82. A propriedade [self.__csrf_token] permanece, portanto, sempre como None (linha 36);
- linhas 51-52: para todas as solicitações seguintes, o token CSRF, caso exista, é adicionado à rota inicial;
- linhas 81-82: o novo token gerado pelo servidor a cada nova solicitação do cliente é armazenado localmente para ser reenviado na linha 52 na solicitação seguinte;
Além disso, o método [init_session] sofre uma ligeira alteração:
def init_session(self, session_type: str):
# regista-se o tipo de sessão
self.__session_type = session_type
# elimina-se o token CSRF das chamadas anteriores
self.__csrf_token = None
# solicita-se o URL da ação init-session
url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# execução da consulta
self.get_response("GET", url_service)
É importante recordar aqui que criámos uma rota [/init-session-without-csrftoken/<type-response>] para inicializar o diálogo cliente/servidor sem o token CSRF. Ora, vimos que o método [get_response], chamado na linha 12 do código, adiciona sistematicamente ao final do serviço URL o token CSRF armazenado em [self.__csrf_token]. É por isso que, na linha 6 do código, se elimina este token CSRF, caso exista.
É tudo. Para os testes, executaremos:
- os clientes de consola [main, main2, main3];
- as classes de teste [Test1HttpClientDaoWithSession] e [Test2HttpClientDaoWithSession];
definindo sucessivamente como True e depois como False o parâmetro de configuração [with_csrftoken].

Eis, a título de exemplo, os registos obtidos aquando da execução do cliente [main json] com o [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
Se observarmos os tokens CSRF recebidos sucessivamente, verificamos que são todos diferentes.