34. Ćwiczenie praktyczne: wersja 14

Folder [http-servers/09] z wersji 14 powstaje poprzez skopiowanie folderu [http-servers/08] z wersji 13.
34.1. Wprowadzenie
CSRF (Cross Site Request Forgery) to technika kradzieży sesji. Została ona wyjaśniona w Wikipedii (https://fr.wikipedia.org/wiki/Cross-site_request_forgery) w następujący sposób:
- Malorie udaje się zdobyć link umożliwiający usunięcie danego wpisu.
- Malorie wysyła do Alice wiadomość zawierającą rzekomy obrazek do wyświetlenia (który w rzeczywistości jest skryptem). Kod URL zawarty w tym obrazku stanowi link do skryptu umożliwiającego usunięcie wybranego wpisu.
- Alice musi mieć otwartą sesję w przeglądarce na stronie, na którą celuje Malorie. Jest to warunek konieczny, aby atak przebiegł w sposób niezauważalny, bez konieczności wysyłania żądania uwierzytelnienia, które mogłoby zaalarmować Alice. Sesja ta musi posiadać uprawnienia niezbędne do wykonania destrukcyjnego żądania Malorie. Nie jest konieczne, aby w przeglądarce była otwarta karta z docelową stroną, ani nawet aby przeglądarka była uruchomiona. Wystarczy, że sesja jest aktywna.
- Alice czyta wiadomość od Malorie, a jej przeglądarka korzysta z otwartej sesji Alice i nie wymaga interaktywnego uwierzytelniania. Próbuje pobrać zawartość obrazu. W ten sposób przeglądarka uruchamia link i usuwa wiadomość, pobierając tekstową stronę internetową jako zawartość obrazu. Nie rozpoznając powiązanego typu obrazu, nie wyświetla obrazu, a Alice nie wie, że Malorie właśnie sprawiła, iż usunęła wiadomość wbrew swojej woli.
Nawet wyjaśniona w ten sposób technika CSRF jest trudna do zrozumienia. Sporządźmy schemat:

- w [1-2] Alicja łączy się z forum (strona A). Forum to utrzymuje sesję dla każdego użytkownika. Przeglądarka Alicji przechowuje lokalnie ten plik cookie sesji i odsyła go za każdym razem, gdy wysyła nowe żądanie do strony A;
- w [3] Malorie wysyła wiadomość do Alicji. Ta czyta ją w swojej przeglądarce. Przeczytana wiadomość ma format HTML i zawiera link do obrazka na stronie B. W rzeczywistości link ten prowadzi do skryptu JavaScript, który uruchamia się po dotarciu do przeglądarki Alicji;
- skrypt JavaScript wysyła następnie żądanie do strony A. Przeglądarka Alice automatycznie wysyła to żądanie wraz z lokalnie zapisanym plikiem cookie sesji. W tym momencie dochodzi do ataku: Malorie zdołała wysłać żądanie do strony A, korzystając z uprawnień (sesji) Alice. Następnie, niezależnie od dalszego przebiegu wydarzeń, atak został przeprowadzony;
Aby zapobiec tego typu atakom, strona A może postępować w następujący sposób:
- przy każdej wymianie danych [1-2] z Alicją strona A wysyła klucz, zwany dalej tokenem (żetonem) CSRF, który Alicja musi odesłać przy następnym żądaniu. W ten sposób Alicja musi przy każdym żądaniu przesłać dwie informacje:
- plik cookie sesji;
- token CSRF otrzymany w odpowiedzi na jej ostatnie żądanie skierowane do serwisu A;
Na tym polega zabezpieczenie: jeśli przeglądarka automatycznie odsyła do serwisu A plik cookie sesji, nie robi tego w przypadku tokenu CSRF. Z tego powodu wymiana 6-7 przeprowadzona przez skrypt atakujący zostanie odrzucona, ponieważ żądanie 6 nie będzie zawierało tokenu CSRF;
Witryna A może wysłać Alicji token CSRF na różne sposoby dla aplikacji HTML:
- może przy każdym żądaniu wysyłać stronę o nazwie HTML, na której wszystkie linki będą zawierały token CSRF, na przykład [http://siteA/chemin/csrf_token]. Podczas kolejnego żądania, gdy Alice kliknie jeden z tych linków, strona A będzie musiała jedynie pobrać token CSRF z pola URL w żądaniu i sprawdzić, czy jest on poprawny. Tak właśnie zostanie to zrobione w tym przypadku;
- w przypadku stron HTML zawierających formularz strona A może wysłać ten formularz wraz z ukrytym polem [input type=’hidden’] zawierającym token CSRF. Token ten zostanie wówczas automatycznie przesłany wraz z formularzem, gdy Alice zatwierdzi stronę. Witryna A pobierze token CSRF z treści (body) żądania;
- możliwe są również inne techniki;
34.2. Konfiguracja

W konfiguracji aplikacji [parameters] wprowadzamy dwie wartości logiczne:
- [with_redissession]: przy wartości True aplikacja korzysta z sesji Redis. Przy wartości False aplikacja korzysta ze zwykłej sesji Flask;
- [with_csrftoken]: przy wartości True parametry URL aplikacji zawierają token CSRF;
# czas przerwy wątku w sekundach
"sleep_time": 0,
# serwer Redis
"with_redissession": True,
"redis": {
"host": "127.0.0.1",
"port": 6379
},
# token CSRF
"with_csrftoken": False,
34.3. Implementacja CSRF
Zapewnimy, że gdy:
config['parameters']['with_csrftoken']
ma wartość [True], aplikacja wysyła do przeglądarki klienta strony internetowe, których linki będą zawierały token CSRF.
34.3.1. Moduł [flask_wtf]
Implementacja tokenu CSRF zostanie przeprowadzona za pomocą modułu [flask_wtf], który instalujemy w terminalu PyCharm:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install flask_wtf
Collecting flask_wtf
…
34.3.2. Szablony widoków
Wprowadzamy nową klasę do szablonów:

Klasa [AbstractBaseModelForView] ma następującą postać:
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 ""
- wiersz 9: klasa [AbstractBaseModelForView] implementuje interfejs [InterfaceModelForView], który jest implementowany przez klasy szablonów;
- wiersze 11–13: metoda [get_model_for_view] nie jest zaimplementowana;
- wiersze 15–20: metoda [get_csrftoken] generuje token CSRF, jeśli aplikacja została skonfigurowana do korzystania z nich. W zależności od sytuacji funkcja zwraca token poprzedzony znakiem /, w przeciwnym razie – pusty ciąg znaków. Cechą charakterystyczną funkcji [generate_csrf] jest to, że zawsze generuje tę samą wartość dla danego żądania klienta. Przetwarzanie żądania wiąże się z wykonaniem różnych funkcji. Użycie funkcji [generate_csrf] w tych funkcjach zawsze generuje tę samą wartość. Natomiast przy kolejnym żądaniu generowany jest nowy token CSRF;
Wszystkie szablony M widoku V będą zawierały token CSRF w następujący sposób:
class ModelForAuthentificationView(AbstractBaseModelForView):
def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
# dane strony są umieszczane w szablonie
modèle = {}
…
# token CSRF
modèle['csrf_token'] = super().get_csrftoken(config)
# zwracamy szablon
return modèle
- każda klasa szablonu dziedziczy po klasie bazowej [AbstractBaseModelForView];
- wiersz 8: token CSRF jest pobierany z klasy nadrzędnej. Otrzymujemy albo pusty ciąg znaków, albo ciąg znaków typu [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c];
34.3.3. Widoki
Z tego, co właśnie widzieliśmy, wszystkie widoki V będą miały w swoim modelu M token CSRF. Będą więc mogły go używać w zawartych w nich linkach. Rozważmy kilka przykładów:
Fragment uwierzytelniający [v_authentification.html]
<!-- formularz HTML – wysyłamy jego wartości za pomocą akcji [authentifier-utilisateur] -->
<form method="post" action="/authentifier-utilisateur{{modèle.csrf_token}}">
<!-- tytuł -->
<div class="alert alert-primary" role="alert">
<h4>Veuillez vous authentifier</h4>
</div>
…
</form>
- wiersz 2: zgodnie z tym, co właśnie omówiliśmy, URL z atrybutu [action] będzie wyglądał następująco:
[/authentifier-utilisateur/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c]
lub
w zależności od tego, czy aplikacja została skonfigurowana do korzystania z tokenów CSRF;
Fragment obliczeń podatku [v-calcul-impot.html]
<!-- formularz HTML wysłany -->
<form method="post" action="/calculer-impot{{modèle.csrf_token}}">
<!-- wiadomość w 12 kolumnach na niebieskim tle -->
<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>
Fragment symulacji [v-liste-simulations.html]
{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- komunikat na niebieskim tle -->
<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 %}
<!-- komunikat na niebieskim tle -->
<div class="alert alert-primary" role="alert">
<h4>Liste de vos simulations</h4>
</div>
<!-- tabela symulacji -->
<table class="table table-sm table-hover table-striped">
…
<!-- treść tabeli (wyświetlane dane) -->
<tbody>
<!-- wyświetlanie poszczególnych symulacji poprzez przeglądanie tabeli symulacji -->
{% for simulation in modèle.simulations %}
<!-- wyświetlanie wiersza tabeli z 6 kolumnami – tag <tr> -->
<!-- kolumna 1: nagłówek wiersza (nr symulacji) – tag <th scope='row' -->
<!-- kolumna 2: wartość parametru [marié] – tag <td> -->
<!-- kolumna 3: wartość parametru [enfants] – tag <td> -->
<!-- kolumna 4: wartość parametru [salaire] – tag <td> -->
<!-- kolumna 5: wartość parametru [impôt] (podatku) – tag <td> -->
<!-- kolumna 6: wartość parametru [surcôte] – tag <td> -->
<!-- kolumna 7: wartość parametru [décôte] – tag <td> -->
<!-- kolumna 8: wartość parametru [réduction] – tag <td> -->
<!-- kolumna 9: wartość parametru [taux] (podatku) – tag <td> -->
<!-- kolumna 10: link do usunięcia symulacji – 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 %}
Fragment menu [v-menu.html]
<!-- menu Bootstrap -->
<nav class="nav flex-column">
<!-- wyświetlanie listy linków 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. Trasy
Obecnie istnieją dwa rodzaje tras, w zależności od tego, czy wykorzystują one token, czy nie: CSRF:

- [routes_without_csrftoken] to trasy bez tokenu CSRF. Są to trasy z poprzedniej wersji;
- [routes_with_csrftoken] to trasy z tokenem CSRF.
W [routes_with_csrftoken] trasy mają teraz dodatkowy parametr – token CSRF:
# kontroler front-end
def front_controller() -> tuple:
# przekazujemy żądanie do kontrolera głównego
main_controller = config['mvc']['controllers']['main-controller']
return main_controller.execute(request, session, config)
@app.route('/', methods=['GET'])
def index() -> tuple:
# przekierowanie do /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:
# uruchamiany jest kontroler powiązany z akcją
return front_controller()
# uwierzytelnianie użytkownika
@app.route('/authentifier-utilisateur/<string:csrf_token>', methods=['POST'])
def authentifier_utilisateur(csrf_token: str) -> tuple:
# uruchamiany jest kontroler powiązany z akcją
return front_controller()
# oblicz-podatek
@app.route('/calculer-impot/<string:csrf_token>', methods=['POST'])
def calculer_impot(csrf_token: str) -> tuple:
# uruchamiany jest kontroler powiązany z akcją
return front_controller()
# obliczanie podatku partiami
@app.route('/calculer-impots/<string:csrf_token>', methods=['POST'])
def calculer_impots(csrf_token: str):
# uruchamia się kontroler powiązany z akcją
return front_controller()
# wyświetlanie listy symulacji
@app.route('/lister-simulations/<string:csrf_token>', methods=['GET'])
def lister_simulations(csrf_token: str) -> tuple:
# uruchamiany jest kontroler powiązany z akcją
return front_controller()
# usuń-symulację
@app.route('/supprimer-simulation/<int:numero>/<string:csrf_token>', methods=['GET'])
def supprimer_simulation(numero: int, csrf_token: str) -> tuple:
# uruchamia się kontroler powiązany z akcją
return front_controller()
# koniec sesji
@app.route('/fin-session/<string:csrf_token>', methods=['GET'])
def fin_session(csrf_token: str) -> tuple:
# uruchamia się kontroler powiązany z akcją
return front_controller()
# wyświetl-obliczenia-podatku
@app.route('/afficher-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_calcul_impot(csrf_token: str) -> tuple:
# uruchamiany jest kontroler powiązany z akcją
return front_controller()
# pobierz-dane-administracyjne
@app.route('/get-admindata/<string:csrf_token>', methods=['GET'])
def get_admindata(csrf_token: str) -> tuple:
# uruchamia się kontroler powiązany z akcją
return front_controller()
Wszystkie trasy mają teraz w swoich parametrach token CSRF, nawet trasa [/init-session]. Oznacza to, że klient nie może uruchomić aplikacji, wpisując bezpośrednio URL [/init-session/html], ponieważ zabraknie tokenu CSRF. Teraz musi koniecznie przejść przez ścieżki URL i [/] z wierszy 7–10.
Wybór tras odbywa się w głównym skrypcie [main]:
…
# główny wątek nie potrzebuje już modułu logującego
logger.close()
# jeśli wystąpił błąd, proces zostaje zatrzymany
if erreur:
sys.exit(2)
# import tras aplikacji internetowej
if config['parameters']['with_csrftoken']:
import routes_with_csrftoken as routes
else:
import routes_without_csrftoken as routes
# konfiguracja tras
routes.config = config
# uruchomienie aplikacji Flask
routes.execute(__name__)
- wiersze 9–13: wybór tras w zależności od tego, czy aplikacja korzysta z tokenów CSRF, czy nie;
34.3.5. Kontroler [MainController]
Przy każdym żądaniu serwer musi sprawdzić obecność tokenu CSRF. Zrobimy to w głównym kontrolerze [MainController], przez który przechodzą wszystkie żądania:
from flask_wtf.csrf import generate_csrf, validate_csrf
…
# przetwarzanie żądania
try:
# rejestrowanie
logger = Logger(config['parameters']['logsFilename'])
…
# pobieranie elementów ścieżki
params = request.path.split('/')
# akcja jest pierwszym elementem
action = params[1]
…
if config['parameters']['with_csrftoken']:
# csrf_token jest ostatnim elementem ścieżki
csrf_token = params.pop()
# sprawdzana jest poprawność tokenu
# zostanie zgłoszony wyjątek, jeśli csrf_token nie jest tokenem oczekiwanym
validate_csrf(csrf_token)
…
except ValidationError as exception:
# nieważny token CSRF
résultat = {"action": action, "état": 121, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
except BaseException as exception:
# inne (nieoczekiwane) wyjątki
résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
finally:
pass
# do wyniku dodawany jest csrf_token
résultat['csrf_token'] = generate_csrf()
# rezultat wysłany do klienta jest rejestrowany
log = f"[MainController] {résultat}\n"
logger.write(log)
- wiersz 20: pobieramy token CSRF z URL zawartego w żądaniu typu [http://machine :port/chemin/action/param1/param2/…/csrf_token]. Token sesji jest zawsze ostatnim elementem URL;
- wiersz 23: sprawdzana jest ważność tokenu CSRF pobranego z URL w połączeniu z tokenem CSRF z sesji. Jeśli token jest nieprawidłowy, funkcja [validate_csrf] zgłasza wyjątek typu [ValidationError] (wiersz 27);
- wiersz 41: token CSRF jest umieszczany w wyniku wysyłanym do klienta. Klienci jSON i XML będą go potrzebować. Klienci ci nie otrzymują bowiem stron HTML z tokenem CSRF w linkach zawartych na tych stronach. Otrzymają go zatem w wyniku jSON lub XML wysłanym przez serwer;
Uwaga: funkcja [validate_csrf] w wierszu 23 nie sprawdza ścisłej zgodności. Token CSRF jest zapisywany w sesji wraz z kluczem [csrf_token]. Testy wydają się wskazywać, że token CSRF jest ważny, jeśli został wygenerowany w trakcie sesji. Jeśli więc ręcznie, na przykład w adresie URL wyświetlanym w przeglądarce (/lister-simulations/xyz), zastąpisz token [xyz] lub CSRF innym tokenem, np. [abc], otrzymanym już podczas poprzedniej akcji, akcja [/lister-simulations] zakończy się powodzeniem;
34.4. Testy w przeglądarce
W:
- uruchamia serwer z parametrem od [with_csrftoken] do [True];
- wywołujemy URL i [http://localhost:5000] za pomocą przeglądarki;

- w [1] token CSRF;
Wykonajmy te operacje, aż uzyskamy listę symulacji:

Teraz ręcznie wpiszmy URL [http://localhost:5000/supprimer-simulation/1/x], aby usunąć symulację o id=1. Celowo podajemy nieprawidłowy token CSRF, aby zobaczyć, co się stanie. Odpowiedź serwera jest następująca:

Uwaga 1: nie ma pewności, czy zastosowana tutaj metoda zawsze wystarczy, by przeciwdziałać atakom typu CSRF. Wróćmy do schematu ataku:

Jeśli skrypt JavaScript pobrany jako [5] jest w stanie odczytać historię przeglądarki używanej przez Alicję, będzie w stanie odzyskać adresy URL wykonane przez przeglądarkę, a także adresy URL, takie jak [/cible/csrf_token]. Będzie mógł wówczas odzyskać token sesji [csrf_token] i przeprowadzić atak w [6-7]. Niemniej jednak przeglądarka zezwala wyłącznie na wykorzystanie historii okna przeglądarki, w którym skrypt jest uruchamiany. Jeśli więc Alice nie korzysta z tego samego okna do pracy ze stroną A o adresie [1-2] i do czytania wiadomości od Malorie o adresie [3], atak o adresie CSRF nie będzie możliwy.
34.5. Klienci konsoli
Innym sposobem przetestowania wersji 14 aplikacji jest wykorzystanie testów z wersji 12 i dostosowanie ich do nowego serwera.

Folder [impots/http-clients/09] uzyskuje się początkowo poprzez skopiowanie folderu [impots/http-clients/07]. Następnie jest on modyfikowany.
Wróćmy do ścieżek inicjujących sesję:
# katalog główny aplikacji
@app.route('/', methods=['GET'])
def index() -> tuple:
# przekierowanie do /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:
# uruchamiany jest kontroler powiązany z akcją
return front_controller()
Żadna z tych tras nie nadaje się do zainicjowania sesji jSON ani XML:
- wiersze 2–5: trasa [/] inicjuje sesję HTML;
- wiersze 8–11: trasa [/init-session] wymaga tokenu CSRF, którego nie znamy;
Postanawiamy dodać nową trasę do serwera:
# init-session-without-csrftoken
@app.route('/init-session-without-csrftoken/<string:type_response>', methods=['GET'])
def init_session_without_csrftoken(type_response: str) -> tuple:
# przekierowanie do /init-session/type_response
return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()), status.HTTP_302_FOUND)
- wiersz 2: nowa trasa. Nie wymaga ona tokenu CSRF. W ten sposób powróciliśmy do trasy [/init-session] z poprzedniej wersji;
- wiersze 4–5: przekierowujemy klienta (jSON, XML, HTML) na trasę [/init-session], która ma w swoich parametrach token CSRF;
Można wypróbować tę nową trasę w przeglądarce:

Odpowiedź serwera (skonfigurowanego z [with_csrftoken=True]) jest następująca:

- w przypadku [1] serwer został przekierowany na trasę [/init-session] z tokenem CSRF w URL;
- w [2] token CSRF znajduje się w słowniku jSON wysłanym przez serwer powiązany z kluczem [csrf_token];
Wróćmy do kodu klienta:

Modyfikujemy konfigurację [config] w następujący sposób:
config.update({
# plik podatników
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
# plik wyników
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
# plik błędów
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
# plik logów
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# serwer obliczający podatek
"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",
}
},
# tryb debugowania
"debug": True,
# csrf_token
"with_csrftoken": True,
}
)
…
# trasa 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'
- wiersz 31: zmienna logiczna wskaże klientowi, czy serwer, do którego się zwraca, obsługuje tokeny CSRF;
- wiersze 37–40: ustalamy URL usługi dla akcji [init-session]:
- jeśli serwer korzysta z tokenów CSRF, wówczas token usługi URL to [/init-session-without-csrftoken];
- w przeciwnym razie identyfikator usługi URL to [/init-session];
Przedstawiono trasę [/init-session-without-csrftoken]. Umożliwia ona klientowi o tokenie jSON / XML rozpoczęcie sesji z serwerem bez posiadania tokenu CSRF. Token ten znajdzie on w odpowiedzi serwera.
Następnie modyfikujemy klasę [ImpôtsDaoWithHttpSession], która implementuje warstwę klienta [dao]:

# importy
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):
# konstruktor
def __init__(self, config: dict):
# inicjalizacja elementu nadrzędnego
AbstractImpôtsDao.__init__(self, config)
# zapisywanie elementów konfiguracji
# ogólna konfiguracja
self.__config = config
# serwer
self.__config_server = config["server"]
# usługi
self.__config_services = config["server"]['url_services']
# tryb debugowania
self.__debug = config["debug"]
# rejestrator
self.__logger = None
# pliki cookie
self.__cookies = None
# typ sesji (json, xml)
self.__session_type = None
# token CSRF
self.__csrf_token = None
# etap żądania / odpowiedzi
def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
# [method]: metoda HTTP, GET lub POST
# [url_service]: URL serwisowe
# [data]: parametry POST w formacie x-www-form-urlencoded
# [json]: parametry POST w formacie JSON
# [cookies]: pliki cookie, które należy dołączyć do żądania
# należy posiadać sesję XML lub JSON, w przeciwnym razie nie będzie można obsłużyć odpowiedzi
if self.__session_type not in ['json', 'xml']:
raise ImpôtsError(73, "il n'y a pas de session valide en cours")
# do żądania serwisowego URL dodaje się token CSRF
if self.__csrf_token:
url_service = f"{url_service}/{self.__csrf_token}"
# wykonanie żądania
response = requests.request(method,
url_service,
data=data_value,
json=json_value,
cookies=self.__cookies,
allow_redirects=True)
# tryb debugowania?
if self.__debug:
# moduł logowania
if not self.__logger:
self.__logger = self.__config['logger']
# rejestruje się
self.__logger.write(f"{response.text}\n")
# wynik
if self.__session_type == "json":
résultat = json.loads(response.text)
else: # xml
résultat = xmltodict.parse(response.text[39:])['root']
# pobieramy pliki cookie z odpowiedzi, jeśli takie istnieją
if response.cookies:
self.__cookies = response.cookies
# pobieramy token CSRF
if self.__config['with_csrftoken']:
self.__csrf_token = résultat.get('csrf_token', None)
# kod statusu
status_code = response.status_code
# jeśli kod statusu jest inny niż 200, to OK
if status_code != status.HTTP_200_OK:
raise ImpôtsError(35, résultat['réponse'])
# zwracamy wynik
return résultat['réponse']
def init_session(self, session_type: str):
# odnotowuje się typ sesji
self.__session_type = session_type
# usuwamy token CSRF z poprzednich wywołań
self.__csrf_token = None
# żądamy URL dla akcji init-session
url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# wykonanie żądania
self.get_response("GET", url_service)
…
- wiersze 38–92: obsługa tokenu CSRF odbywa się głównie w metodzie [get_response];
- wiersz 60: istotnym elementem jest parametr [allow_redirects=True]. Jest to jego wartość domyślna, ale postanowiliśmy ją podkreślić;
W trybie [with_csrftoken=True]:
- klienci rozpoczynają komunikację z serwerem od wywołania trasy [/init-session_without_csftoken/type_response];
- serwer odpowiada na to żądanie przekierowaniem do trasy [/init-session/type_response/csrf_token];
- z powodu parametru [allow_redirects=True] przekierowanie to zostanie zrealizowane przez klienta [requests];
- token CSRF zostanie znaleziony w pobranym wyniku w wierszach 72 i 74, powiązany z kluczem [csrf_token];
W trybie [with_csrftoken=False]:
- (ciąg dalszy)
- klienci rozpoczynają dialog z serwerem od wywołania trasy [/init-session /type_response];
- serwer odpowiada na to żądanie przekierowaniem do trasy [/init-session/type_response];
- z powodu parametru [allow_redirects=True] przekierowanie to zostanie zrealizowane przez klienta [requests];
- w wierszach 81–82 nie ma tokenu CSRF do pobrania. Właściwość [self.__csrf_token] pozostaje zatem nadal ustawiona na None (wiersz 36);
- wiersze 51–52: w przypadku wszystkich kolejnych żądań token CSRF, o ile istnieje, jest dodawany do początkowej trasy;
- wiersze 81–82: nowy token generowany przez serwer przy każdym nowym żądaniu klienta jest zapisywany lokalnie, aby zostać odesłany w wierszu 52 przy następnym żądaniu;
Ponadto metoda [init_session] ulega niewielkiej zmianie:
def init_session(self, session_type: str):
# odnotowuje się typ sesji
self.__session_type = session_type
# usuwany jest token CSRF z poprzednich wywołań
self.__csrf_token = None
# żądanie URL dla akcji init-session
url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# wykonanie żądania
self.get_response("GET", url_service)
Należy tutaj pamiętać, że utworzono trasę [/init-session-without-csrftoken/<type-response>] w celu zainicjowania dialogu klient–serwer bez tokenu CSRF. Widzieliśmy jednak, że metoda [get_response], wywoływana w linii 12 kodu, systematycznie dodaje na końcu kodu usługi URL token CSRF zapisany w [self.__csrf_token]. Dlatego w wierszu 6 kodu usuwa się ten token CSRF, jeśli istniał.
To wszystko. W celu przeprowadzenia testów należy wykonać:
- klienty konsoli [main, main2, main3];
- klasy testowe [Test1HttpClientDaoWithSession] i [Test2HttpClientDaoWithSession];
ustawiając kolejno na True, a następnie na False parametr konfiguracyjny [with_csrftoken].

Oto przykładowe logi uzyskane podczas uruchomienia klienta [main json] wraz z [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
Jeśli przyjrzymy się kolejno otrzymanym tokenom CSRF, zauważymy, że wszystkie są różne.