Skip to content

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

Image

A pasta [http-servers/09] da versão 14 é obtida por meio 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. Ela é explicada da seguinte forma na Wikipedia (https://fr.wikipedia.org/wiki/Cross-site_request_forgery):

Suponhamos que Alice seja a administradora de um fórum e que esteja conectada a ele por meio de um sistema de sessões. Malorie é membro desse mesmo fórum e deseja excluir uma das mensagens do fórum. Como não possui os direitos necessários em sua conta, ela utiliza a conta de Alice por meio de um ataque do tipo CSRF.
  1. Malorie consegue descobrir o link que permite excluir a mensagem em questão.
  2. Malorie envia uma mensagem para Alice contendo uma pseudoimagem a ser exibida (que, na verdade, é um script). O URL da imagem é o link para o script que permite excluir a mensagem desejada.
  3. Alice precisa ter uma sessão aberta no navegador para o site visado por Malorie. Essa é uma condição necessária para que o ataque seja bem-sucedido de forma silenciosa, sem exigir uma solicitação de autenticação que alertaria Alice. Essa sessão deve ter os direitos necessários para executar a solicitação destrutiva de Malorie. Não é necessário que uma aba do navegador esteja aberta no site alvo, nem mesmo que o navegador esteja em execução. Basta que a sessão esteja ativa.
  4. Alice lê a mensagem de Malorie; seu navegador utiliza a sessão aberta de Alice e não solicita autenticação interativa. Ele tenta recuperar o conteúdo da imagem. Ao fazer isso, o navegador aciona o link e exclui a mensagem, recuperando uma página da web com texto como conteúdo da imagem. Como não reconhece o tipo de imagem associado, ele não exibe a imagem, e Alice não sabe que Malorie acabou de fazer com que ela apagasse uma mensagem contra sua vontade.

Mesmo explicada dessa forma, a técnica do CSRF é difícil de entender. Vamos fazer um esquema:

Image

  • em [1-2], Alice se conecta ao fórum (Site A). Esse fórum mantém uma sessão para cada usuário. O navegador de Alice armazena localmente esse cookie de sessão e o reenvia sempre que faz uma nova solicitação ao Site A;
  • em [3], Malorie envia uma mensagem para Alice. Ela a lê com 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 de Alice;
  • esse script JavaScript então realiza 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 acessar o site A com os direitos (sessão) de Alice. A partir daí, independentemente do que aconteça, o ataque já ocorreu;

Para se defender contra esse tipo de ataque, o site A pode proceder da seguinte maneira:

  • a cada troca [1-2] com Alice, o site A envia uma chave, doravante chamada de token CSRF, que Alice deve reenviar na próxima solicitação. Assim, Alice deve enviar, a 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 está aí: embora o navegador reenvie automaticamente ao site A o cookie de sessão, ele não faz o mesmo com o token CSRF. Por esse motivo, a troca 6-7 realizada pelo script de ataque será recusada, pois a solicitação 6 não terá enviado o token CSRF;

O site A pode enviar a Alice o token CSRF de várias maneiras para uma solicitação HTML:

  • ele pode, a cada solicitação, enviar uma página HTML em que todos os links tenham o token CSRF, por exemplo, [http://siteA/chemin/csrf_token]. Na próxima solicitação, quando Alice clicar em um desses links, o site A precisará apenas recuperar o token CSRF do URL da solicitação e verificar se ele está correto. É isso que será feito aqui;
  • para as páginas HTML que contenham um formulário, ele pode enviá-lo com um campo oculto [input type=’hidden’] contendo o token CSRF. Esse token será então enviado automaticamente com o formulário quando Alice enviar a página. O site A recuperará o token CSRF no corpo (body) da solicitação;
  • outras técnicas são possíveis;

34.2. Configuração

Image

Incorporamos na configuração [parameters] do aplicativo dois valores booleanos:

  • [with_redissession]: quando definido como True, o aplicativo utiliza uma sessão Redis. Quando definido como False, o aplicativo utiliza uma sessão Flask normal;
  • [with_csrftoken]: quando definido como True, os URL do aplicativo contêm um token CSRF;

        # tempo de 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], o aplicativo envie ao navegador do cliente páginas da 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 em um 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 visualizações

Introduzimos uma nova classe nos modelos:

Image

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 o aplicativo tiver sido configurado para utilizá-los. Dependendo do caso, a função retorna um token precedido pelo sinal /; caso contrário, retorna uma string vazia. A função [generate_csrf] tem a particularidade de sempre gerar o mesmo valor para uma determinada solicitação do cliente. O processamento de uma solicitação envolve a execução de diferentes funções. O uso 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 visualização V incluirão o token CSRF da seguinte maneira:


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)

        # retornamos 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 string vazia, ou uma string do tipo [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c];

34.3.3. As visualizações

Pelo que acabamos de ver, todas as visualizações V terão, em seu modelo M, o token CSRF. Portanto, elas poderão utilizá-lo nos links que contêm. Vejamos alguns exemplos:

O fragmento de autenticação [v_authentification.html]


<!-- formulário HTML — enviamos 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 acabamos de ver, o URL do atributo [action] será:

[/authentifier-utilisateur/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c]

ou

[/authentifier-utilisateur]

dependendo se o aplicativo foi configurado para usar 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 com 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 exibidos) -->
    <tbody>
    <!-- cada simulação é exibida ao percorrer a tabela de simulações -->
    {% for simulation in modèle.simulations %}

    <!-- exibição de uma linha da tabela com 6 colunas — tag <tr> -->
    <!-- coluna 1: cabeçalho da linha (n.º da simulação) — tag <th scope='row' -->
    <!-- coluna 2: valor do parâmetro [marié] - tag <td> -->
    <!-- coluna 3: valor do parâmetro [enfants] - tag <td> -->
    <!-- coluna 4: valor do parâmetro [salaire] - tag <td> -->
    <!-- coluna 5: valor do parâmetro [impôt] (do imposto) - tag <td> -->
    <!-- coluna 6: valor do parâmetro [surcôte] - tag <td> -->
    <!-- coluna 7: valor do parâmetro [décôte] - tag <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 excluir 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 rotas

Agora existem dois tipos de rotas, dependendo se elas utilizam ou não um token CSRF:

Image

  • [routes_without_csrftoken] são as rotas sem token CSRF. 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 agora possuem um parâmetro adicional, o token CSRF:


# o front controller
def front_controller() -> tuple:
    # a solicitação é encaminhada ao 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-usuário
@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()

# excluir-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-imposto
@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 agora possuem o token CSRF em seus parâmetros, inclusive a rota [/init-session]. Isso significa que o cliente não pode iniciar o aplicativo digitando diretamente URL [/init-session/html], pois faltará o token CSRF. Agora, ele deve obrigatoriamente passar pelas linhas 7 a 10: URL e [/].

A escolha das rotas é feita no script principal [main]:



# a thread principal não precisa mais do logger
logger.close()

# se houver erro, a execução é interrompida
if erreur:
    sys.exit(2)

# importação das rotas do aplicativo 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 do aplicativo Flask
routes.execute(__name__)
  • linhas 9 a 13: escolha das rotas dependendo se o aplicativo utiliza ou não tokens CSRF;

34.3.5. O controlador [MainController]

A cada solicitação, o servidor deve verificar a presença do token CSRF. Faremos isso no controlador principal [MainController], que processa todas as solicitações:


from flask_wtf.csrf import generate_csrf, validate_csrf

       # processando a solicitação
        try:
            # registro em log
            logger = Logger(config['parameters']['logsFilename'])

            …

            # recuperando os 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
                # uma exceção será lançada 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()

        # registra-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 ele 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 precisarão dele. De fato, esses clientes não recebem páginas HTML com o token CSRF nos links contidos nas páginas. Portanto, eles o receberão no resultado jSON ou XML enviado pelo servidor;

Observação: 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 exibido no navegador, por exemplo (/lister-simulations/xyz), você substituir o token [xyz] CSRF por outro, [abc], já recebido em uma ação anterior, a ação [/lister-simulations] será bem-sucedida;

34.4. Testes com um navegador

Nós:

  • inicie o servidor com o parâmetro [with_csrftoken] até [True];
  • solicite o URL [http://localhost:5000] com um navegador;

Image

  • no [1], o token CSRF;

Vamos realizar algumas operações até obtermos uma lista de simulações:

Image

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

Image

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

Image

Se o script JavaScript baixado em [5] for capaz de ler o histórico do navegador usado por Alice, ele será capaz de recuperar as URL executadas pelo navegador, bem como as URL, como a [/cible/csrf_token]. Assim, ele poderá recuperar o token de sessão [csrf_token] e realizar 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á sendo executado. Portanto, se Alice não usar a mesma janela para acessar o site A ([1-2]) e ler a mensagem de Malorie ([3]), o ataque (CSRF) não será possível.

34.5. Clientes de console

Outra maneira de testar a versão 14 do aplicativo é retomar os testes da versão 12 e adaptá-los ao novo servidor.

Image

A pasta [impots/http-clients/09] é obtida inicialmente por meio da cópia da pasta [impots/http-clients/07]. Em seguida, ela é modificada.

Voltemos às rotas que inicializam uma sessão:


# raiz do aplicativo
@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-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:
    # executa-se o controlador associado à ação
    return front_controller()

Nenhuma dessas 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 não conhecemos;

Decidimos adicionar uma nova rota ao servidor:


# init-session-without-csrftoken
@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. Ela não exige o token CSRF. Assim, voltamos à rota [/init-session] da versão anterior;
  • linhas 4-5: redirecionamos o cliente (jSON, XML, HTML) para a rota [/init-session], que possui o token CSRF em seus parâmetros;

É possível testar essa nova rota com um navegador:

Image

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

Image

  • em [1], o servidor foi redirecionado para a rota [/init-session] com o token CSRF no URL;
  • em [2], o token CSRF está no dicionário jSON enviado pelo servidor associado à chave [csrf_token];

Voltemos ao código do cliente:

Image

Alteramos a configuração [config] da seguinte maneira:


   config.update({
        # arquivo de contribuintes
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        # arquivo de resultados
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        # arquivo de erros
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        # arquivo de logs
        "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 ao qual ele está se conectando trabalha ou não com tokens CSRF;
  • linhas 37-40: definimos o URL de serviço da ação [init-session]:
    • se o servidor utiliza tokens CSRF, então o token de serviço URL é [/init-session-without-csrftoken];
    • caso contrário, o serviço URL é o [/init-session];

A rota [/init-session-without-csrftoken] foi apresentada. Ela permite que um cliente jSON / XML inicie uma sessão com o servidor sem possuir um token CSRF. Ele encontrará esse token na resposta do servidor.

Em seguida, modificamos a classe [ImpôtsDaoWithHttpSession] que implementa a camada [dao] do cliente:

Image


# 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 da 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"]
        # registrador
        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 solicitação/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 no formato x-www-form-urlencoded
        # [json]: parâmetros do POST em JSON
        # [cookies]: cookies a serem incluídos 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 da solicitação
        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:
            # registrador
            if not self.__logger:
                self.__logger = self.__config['logger']
            # registrando
            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']

        # recuperamos os cookies da resposta, se houver
        if response.cookies:
            self.__cookies = response.cookies

        # recupera-se o token CSRF
        if self.__config['with_csrftoken']:
            self.__csrf_token = résultat.get('csrf_token'None)

        # código de status
        status_code = response.status_code

        # se o código de status 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):
        # registra-se o tipo da 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: o gerenciamento do token CSRF ocorre principalmente no método [get_response];
  • linha 60: o ponto importante é o parâmetro [allow_redirects=True]. Esse é seu valor padrão, mas fizemos questão de destacá-lo;

Quando se está no modo [with_csrftoken=True]:

  • os clientes iniciam sua comunicação com o servidor chamando a rota [/init-session_without_csftoken/type_response];
  • o servidor responde a essa solicitação com um redirecionamento para a rota [/init-session/type_response/csrf_token];
  • devido ao parâmetro [allow_redirects=True], esse redirecionamento será seguido pelo cliente [requests];
  • o token CSRF será encontrado no resultado recuperado nas linhas 72 e 74, associado à chave [csrf_token];

Quando se está no modo [with_csrftoken=False]:

  • (continuação)
    • os clientes iniciam seu diálogo com o servidor chamando a rota [/init-session /type_response];
    • o servidor responde a essa solicitação com um redirecionamento para a rota [/init-session/type_response];
    • devido ao parâmetro [allow_redirects=True], esse redirecionamento será seguido pelo cliente [requests];
    • não há nenhum token CSRF a ser recuperado 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, se existir, é 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 próxima solicitação;

Além disso, o método [init_session] sofre uma pequena alteração:


    def init_session(self, session_type: str):
        # registra-se o tipo da sessão
        self.__session_type = session_type

        # o token CSRF das chamadas anteriores é removido
        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 lembrar aqui que criamos uma rota [/init-session-without-csrftoken/<type-response>] para inicializar o diálogo cliente/servidor sem o token CSRF. No entanto, vimos que o método [get_response], chamado na linha 12 do código, adiciona sistematicamente ao final do URL de serviço o token CSRF armazenado em [self.__csrf_token]. É por isso que, na linha 6 do código, esse token CSRF é removido, caso exista.

É isso. Para os testes, executaremos:

  • os clientes de console [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].

Image

Aqui está, a título de exemplo, os logs obtidos ao executar o cliente [main json] com [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, vemos que todos são diferentes.