34. Uygulama alıştırması: sürüm 14

Sürüm 14'teki [http-servers/09] dosyası, sürüm 13'teki [http-servers/08] dosyasının kopyalanmasıyla elde edilir.
34.1. Giriş
CSRF (Cross Site Request Forgery), bir oturum çalma tekniğidir. Wikipedia'da (https://fr.wikipedia.org/wiki/Cross-site_request_forgery) şu şekilde açıklanmaktadır:
- Malorie, söz konusu mesajı silmeye yarayan bağlantıyı öğrenmeyi başarır.
- Malorie, Alice’e görüntülenmesi gereken sahte bir resim (aslında bir komut dosyası) içeren bir mesaj gönderir. Resimdeki URL, istenen mesajı silmeye yarayan komut dosyasına giden bağlantıdır.
- Alice’in tarayıcısında Malorie’nin hedeflediği siteye ait açık bir oturum bulunmalıdır. Bu, Alice’i uyandıracak bir kimlik doğrulama isteği gerektirmeden saldırının sessizce başarılı olması için gerekli bir koşuldur. Bu oturum, Malorie’nin yıkıcı isteğini yürütmek için gerekli yetkilere sahip olmalıdır. Hedef sitede bir tarayıcı sekmesinin açık olması veya hatta tarayıcının başlatılmış olması gerekmez. Oturumun aktif olması yeterlidir.
- Alice, Malorie’nin mesajını okur; tarayıcısı, Alice’in açık oturumunu kullanır ve etkileşimli kimlik doğrulama istemez. Tarayıcı, resmin içeriğini almaya çalışır. Bunu yaparken tarayıcı, bağlantıyı etkinleştirir ve mesajı siler; resmin içeriği olarak metin içeren bir web sayfası alır. İlişkili resim türünü tanımadığı için resim görüntülenmez ve Alice, Malorie’nin az önce kendi rızası dışında bir mesajı sildirdiğinden haberdar olmaz.
Böyle açıklansa bile, CSRF tekniğini anlamak zordur. Bir şema çizelim:

- [1-2]'te Alice, forumla (Site A) iletişim kurar. Bu forum, her kullanıcı için bir oturum tutar. Alice’in tarayıcısı bu oturum çerezini yerel olarak depolar ve Site A’ya her yeni istek gönderdiğinde onu geri gönderir;
- [3] örneğinde, Malorie Alice’e bir mesaj gönderir. Alice bu mesajı tarayıcısıyla okur. Okunan mesaj HTML biçimindedir ve Site B’deki bir resme yönlendiren bir bağlantı içerir. Aslında bu bağlantı, Alice’in tarayıcısına ulaştığında çalışan bir JavaScript betiğine yönlendiren bir bağlantıdır;
- bu JavaScript betiği daha sonra A sitesine bir istek gönderir. Alice’in tarayıcısı, yerel olarak depolanan oturum çerezi ile birlikte bu isteği otomatik olarak gönderir. Saldırı işte bu noktada gerçekleşir: Malorie, Alice’in haklarıyla (oturumuyla) A sitesine erişmeyi başarmıştır. Bundan sonra ne olursa olsun, saldırı gerçekleşmiştir;
Bu tür saldırıları önlemek için A sitesi şu şekilde hareket edebilir:
- Alice ile her [1-2] alışverişinde, A sitesi daha sonra CSRF tokeni olarak adlandırılan bir anahtar gönderir ve Alice'in bir sonraki isteğinde bunu geri göndermesi gerekir. Böylece Alice, her istekte iki bilgiyi göndermelidir:
- oturum çerezi;
- A sitesine yaptığı son isteğe verilen yanıtta aldığı CSRF tokeni;
Güvenlik önlemi şurada yatmaktadır: Tarayıcı, oturum çerezini Site A’ya otomatik olarak geri gönderir, ancak CSRF jetonunu göndermez. Bu nedenle, saldırı komut dosyası tarafından gerçekleştirilen 6-7 numaralı iletişim reddedilecektir, çünkü 6 numaralı istek CSRF jetonunu göndermeyecektir;
A sitesi, HTML uygulaması için Alice’e CSRF jetonunu çeşitli şekillerde gönderebilir:
- her istekte, tüm bağlantıların CSRF jetonunu içerdiği bir HTML sayfası gönderebilir; örneğin [http://siteA/chemin/csrf_token]. Bir sonraki istek sırasında, Alice bu bağlantılardan birine tıkladığında, A sitesi isteğin URL'sinden CSRF jetonunu alıp bunun doğru olup olmadığını kontrol etmekle yetinecektir. Burada da bu şekilde yapılacaktır;
- Form içeren HTML sayfaları için, CSRF jetonunu içeren [input type=’hidden’] adlı gizli bir alanla birlikte bu formu gönderebilir. Alice sayfayı onayladığında bu jeton, formla birlikte otomatik olarak gönderilecektir. A sitesi, isteğin gövdesinde (body) CSRF jetonunu alacaktır;
- başka teknikler de düşünülebilir;
34.2. Yapılandırma

Uygulamanın [parameters] yapılandırmasına iki boole değeri ekliyoruz:
- [with_redissession]: True olarak ayarlandığında, uygulama bir Redis oturumu kullanır. False olarak ayarlandığında, uygulama normal bir Flask oturumu kullanır;
- [with_csrftoken]: True olarak ayarlandığında, uygulamanın URL'leri bir CSRF jetonu içerir;
# iş parçacığı bekleme süresi (saniye cinsinden)
"sleep_time": 0,
# Redis sunucusu
"with_redissession": True,
"redis": {
"host": "127.0.0.1",
"port": 6379
},
# CSRF jetonu
"with_csrftoken": False,
34.3. CSRF Uygulaması
Aşağıdaki durumlarda:
config['parameters']['with_csrftoken']
değeri [True] olduğunda, uygulamanın istemci tarayıcısına, bağlantılarında CSRF jetonu bulunan web sayfaları göndermesini sağlayacağız.
34.3.1. [flask_wtf] modülü
CSRF jetonunun uygulaması, bir PyCharm terminaline kurduğumuz [flask_wtf] modülü ile gerçekleştirilecektir:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install flask_wtf
Collecting flask_wtf
…
34.3.2. Görünüm şablonları
Şablonlara yeni bir sınıf ekliyoruz:

[AbstractBaseModelForView] sınıfı şu şekildedir:
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 ""
- 9. satır: [AbstractBaseModelForView] sınıfı, şablon sınıfları tarafından uygulanan [InterfaceModelForView] arayüzünü uygular;
- 11-13. satırlar: [get_model_for_view] yöntemi uygulanmamıştır;
- 15-20. satırlar: Uygulama bu jetonları kullanmak üzere yapılandırılmışsa, [get_csrftoken] yöntemi CSRF jetonunu oluşturur. Duruma göre, işlev jetonun önüne / işareti ekleyerek bir jeton döndürür; aksi takdirde boş bir dize döndürür. [generate_csrf] işlevinin özelliği, belirli bir istemci isteği için her zaman aynı değeri üretmesidir. Bir isteğin işlenmesi, farklı işlevlerin yürütülmesini gerektirir. Bu işlevlerde [generate_csrf] kullanıldığında her zaman aynı değer üretilir. Buna karşın, bir sonraki istekte yeni bir CSRF jetonu üretilir;
V görünümünün tüm M şablonları, CSRF jetonunu aşağıdaki şekilde içerecektir:
class ModelForAuthentificationView(AbstractBaseModelForView):
def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
# sayfa verilerini şablona yerleştiriyoruz
modèle = {}
…
# CSRF jetonu
modèle['csrf_token'] = super().get_csrftoken(config)
# şablon gönderilir
return modèle
- her şablon sınıfı, [AbstractBaseModelForView] temel sınıfını genişletir;
- 8. satır: üst sınıftan CSRF jetonu istenir. Sonuç olarak ya boş bir dize ya da [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c] gibi bir dize elde edilir;
34.3.3. Görünümler
Az önce gördüğümüz gibi, tüm V görünümlerinin M şablonlarında CSRF jetonu bulunacaktır. Dolayısıyla, içerdikleri bağlantılarda bu jetonu kullanabilirler. Birkaç örnek verelim:
[v_authentification.html] kimlik doğrulama parçası
<!-- HTML formu - değerleri [authentifier-utilisateur] eylemiyle gönderilir -->
<form method="post" action="/authentifier-utilisateur{{modèle.csrf_token}}">
<!-- başlık -->
<div class="alert alert-primary" role="alert">
<h4>Veuillez vous authentifier</h4>
</div>
…
</form>
- 2. satır: az önce gördüğümüz gibi, [action] özniteliğinin URL değeri şu şekilde olacaktır:
[/authentifier-utilisateur/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c]
veya
uygulamanın CSRF jetonlarını kullanacak şekilde yapılandırılıp yapılandırılmadığına bağlı olarak;
[v-calcul-impot.html] vergi hesaplama parçası
<!-- gönderilen HTML formu -->
<form method="post" action="/calculer-impot{{modèle.csrf_token}}">
<!-- mavi arka planlı 12 sütunlu mesaj -->
<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>
Simülasyon parçası [v-liste-simulations.html]
{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- mavi arka planlı mesaj -->
<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 %}
<!-- mavi arka planlı mesaj -->
<div class="alert alert-primary" role="alert">
<h4>Liste de vos simulations</h4>
</div>
<!-- simülasyon tablosu -->
<table class="table table-sm table-hover table-striped">
…
<!-- tablonun gövdesi (görüntülenen veriler) -->
<tbody>
<!-- simülasyon tablosunu tarayarak her bir simülasyon görüntülenir -->
{% for simulation in modèle.simulations %}
<!-- 6 sütunlu tablonun bir satırının görüntülenmesi - <tr> etiketi -->
<!-- 1. sütun: satır başlığı (simülasyon numarası) - <th scope='row'> etiketi -->
<!-- 2. sütun: parametre değeri [marié] - <td> etiketi -->
<!-- 3. sütun: [enfants] parametre değeri - <td> etiketi -->
<!-- 4. sütun: [salaire] parametre değeri - <td> etiketi -->
<!-- 5. sütun: [impôt] parametre değeri (vergi) - <td> etiketi -->
<!-- 6. sütun: [surcôte] parametre değeri - <td> etiketi -->
<!-- 7. sütun: [décôte] parametre değeri - <td> etiketi -->
<!-- 8. sütun: [réduction] parametre değeri - <td> etiketi -->
<!-- 9. sütun: [taux] parametre değeri (vergi) - <td> etiketi -->
<!-- 10. sütun: simülasyonu silme bağlantısı - <td> etiketi -->
<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 %}
Menü parçası [v-menu.html]
<!-- Bootstrap menüsü -->
<nav class="nav flex-column">
<!-- bağlantı listesinin görüntülenmesi 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. Yollar
Artık, CSRF jetonu kullanıp kullanmadıklarına göre iki tür yol bulunmaktadır:

- [routes_without_csrftoken], CSRF jetonu kullanılmayan yollardır. Bunlar önceki sürümdeki yollardır;
- [routes_with_csrftoken], CSRF jetonunu kullanan yollardır.
[routes_with_csrftoken]'te rotalar artık ek bir parametreye sahiptir: CSRF jetonu:
# ön denetleyici
def front_controller() -> tuple:
# istek ana denetleyiciye iletilir
main_controller = config['mvc']['controllers']['main-controller']
return main_controller.execute(request, session, config)
@app.route('/', methods=['GET'])
def index() -> tuple:
# /init-session/html adresine yönlendirme
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:
# eylemle ilişkili denetleyici çalıştırılıyor
return front_controller()
# kullanıcı kimlik doğrulaması
@app.route('/authentifier-utilisateur/<string:csrf_token>', methods=['POST'])
def authentifier_utilisateur(csrf_token: str) -> tuple:
# eylemle ilişkili denetleyici çalıştırılıyor
return front_controller()
# vergi-hesapla
@app.route('/calculer-impot/<string:csrf_token>', methods=['POST'])
def calculer_impot(csrf_token: str) -> tuple:
# eyleme ilişkin denetleyici çalıştırılır
return front_controller()
# toplu vergi hesaplaması
@app.route('/calculer-impots/<string:csrf_token>', methods=['POST'])
def calculer_impots(csrf_token: str):
# eyleme bağlı denetleyici çalıştırılıyor
return front_controller()
# simülasyonları listeleme
@app.route('/lister-simulations/<string:csrf_token>', methods=['GET'])
def lister_simulations(csrf_token: str) -> tuple:
# eyleme bağlı denetleyici çalıştırılır
return front_controller()
# simülasyonu sil
@app.route('/supprimer-simulation/<int:numero>/<string:csrf_token>', methods=['GET'])
def supprimer_simulation(numero: int, csrf_token: str) -> tuple:
# eyleme ilişkin denetleyici çalıştırılır
return front_controller()
# oturum sonu
@app.route('/fin-session/<string:csrf_token>', methods=['GET'])
def fin_session(csrf_token: str) -> tuple:
# eyleme ilişkin denetleyici çalıştırılıyor
return front_controller()
# vergi-hesaplamasını-göster
@app.route('/afficher-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_calcul_impot(csrf_token: str) -> tuple:
# eyleme ilişkin denetleyici çalıştırılıyor
return front_controller()
# yönetici verilerini al
@app.route('/get-admindata/<string:csrf_token>', methods=['GET'])
def get_admindata(csrf_token: str) -> tuple:
# eyleme bağlı denetleyici çalıştırılıyor
return front_controller()
Artık tüm rotaların parametrelerinde CSRF jetonu bulunmaktadır; [/init-session] rotası da dahil. Bu, müşterinin URL veya [/init-session/html]'i doğrudan yazarak uygulamayı başlatamayacağı anlamına gelir, çünkü CSRF jetonu eksik olacaktır. Artık mutlaka 7-10. satırlardaki URL ve [/] yolunu kullanması gerekir.
Rota seçimi, ana komut dosyası [main] içinde yapılır:
…
# ana iş parçacığı artık logger'a ihtiyaç duymuyor
logger.close()
# hata oluşursa işlem durdurulur
if erreur:
sys.exit(2)
# web uygulamasının rotaları içe aktarılır
if config['parameters']['with_csrftoken']:
import routes_with_csrftoken as routes
else:
import routes_without_csrftoken as routes
# rotaların yapılandırılması
routes.config = config
# Flask uygulamasını başlat
routes.execute(__name__)
- 9-13. satırlar: Uygulamanın CSRF jetonlarını kullanıp kullanmadığına göre rota seçimi;
34.3.5. [MainController] denetleyicisi
Her istek geldiğinde, sunucu CSRF jetonunun varlığını kontrol etmelidir. Bunu, tüm isteklerin geçtiği ana denetleyici [MainController] içinde yapacağız:
from flask_wtf.csrf import generate_csrf, validate_csrf
…
# isteği işleniyor
try:
# günlük kaydı
logger = Logger(config['parameters']['logsFilename'])
…
# yolun öğeleri alınır
params = request.path.split('/')
# eylem, yolun ilk öğesidir
action = params[1]
…
if config['parameters']['with_csrftoken']:
# csrf_token, yolun son öğesidir
csrf_token = params.pop()
# token'ın geçerliliği kontrol edilir
# csrf_token beklenenle aynı değilse bir istisna atılır
validate_csrf(csrf_token)
…
except ValidationError as exception:
# CSRF jetonu geçersiz
résultat = {"action": action, "état": 121, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
except BaseException as exception:
# diğer (beklenmeyen) istisnalar
résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
finally:
pass
# sonuca csrf_token eklenir
résultat['csrf_token'] = generate_csrf()
# müşteriye gönderilen sonuç günlüğe kaydediliyor
log = f"[MainController] {résultat}\n"
logger.write(log)
- 20. satır: [http://machine :port/chemin/action/param1/param2/…/csrf_token] türündeki isteğin URL'sinden CSRF jetonunu alıyoruz. Oturum jetonu her zaman URL'in son öğesidir;
- 23. satır: URL'ten alınan CSRF jetonunun, oturumun CSRF jetonuyla geçerliliği kontrol edilir. Geçerli değilse, [validate_csrf] işlevi [ValidationError] türünde bir istisna oluşturur (satır 27);
- 41. satır: CSRF jetonu, müşteriye gönderilen sonuca eklenir. jSON ve XML istemcileri buna ihtiyaç duyacaktır. Zira bu istemciler, sayfalarda yer alan bağlantılarda CSRF jetonunu içeren HTML sayfalarını almazlar. Dolayısıyla, bu jetonu sunucu tarafından gönderilen jSON veya XML sonucunda alacaklardır;
Not: 23. satırdaki [validate_csrf] işlevi, tam bir eşleşmeyi kontrol etmez. CSRF jetonu, [csrf_token] anahtarıyla oturumda saklanır. Testler, bir CSRF jetonunun oturum sırasında oluşturulmuş olması durumunda geçerli olduğunu gösteriyor gibi görünüyor. Dolayısıyla, örneğin tarayıcıda görüntülenen URL adresinde (/lister-simulations/xyz) manuel olarak [xyz] veya CSRF jetonunu, önceki bir işlem sırasında zaten alınmış olan [abc] jetonuyla değiştirirseniz, [/lister-simulations] işlemi başarılı olacaktır;
34.4. Tarayıcı ile testler
Şu şekilde:
- sunucuyu [with_csrftoken] ile [True] parametresiyle başlatın;
- bir tarayıcıyla URL ve [http://localhost:5000] adreslerini çağırın;

- [1]'te, CSRF jetonunu;
Simülasyon listesi elde edene kadar işlemleri tekrarlayalım:

Şimdi, id=1 olan simülasyonu silmek için URL [http://localhost:5000/supprimer-simulation/1/x] komutunu elle girelim. Ne olacağını görmek için kasıtlı olarak hatalı bir CSRF jetonu giriyoruz. Sunucunun yanıtı şöyledir:

Not 1: Burada kullanılan yöntemin, CSRF saldırılarını her zaman engellemek için yeterli olacağı kesin değildir. Saldırı şemasına geri dönelim:

[5] olarak indirilen JavaScript betiği, Alice’in kullandığı tarayıcının geçmişini okuyabiliyorsa, tarayıcı tarafından yürütülen URL'leri ve [/cible/csrf_token] gibi URL'leri ele geçirebilir. Böylece [csrf_token] oturum jetonunu ele geçirebilir ve [6-7] üzerinden saldırısını gerçekleştirebilir. Ancak tarayıcı, yalnızca komut dosyasının çalıştırıldığı tarayıcı penceresinin geçmişinin kullanılmasına izin verir. Dolayısıyla, Alice, A sitesiyle çalışmak için [1-2] penceresini ve Malorie’nin mesajını okumak için [3] penceresini aynı pencereyi kullanmazsa, CSRF saldırısı mümkün olmayacaktır.
34.5. Konsol istemcileri
Uygulamanın 14. sürümünü test etmenin bir başka yolu da 12. sürümdeki testleri alıp yeni sunucuya uyarlamaktır.

[impots/http-clients/09] dosyası, başlangıçta [impots/http-clients/07] dosyasının kopyalanmasıyla elde edilir. Daha sonra bu dosya değiştirilir.
Bir oturumu başlatan rotalara geri dönelim:
# uygulamanın kök dizini
@app.route('/', methods=['GET'])
def index() -> tuple:
# /init-session/html adresine yönlendirme
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:
# eylemle ilişkili denetleyici çalıştırılıyor
return front_controller()
Bu rotaların hiçbiri, jSON veya XML oturumunu başlatmak için uygun değildir:
- 2-5. satırlar: [/] rotası, HTML oturumunu başlatır;
- 8-11. satırlar: [/init-session] rotası, bilinmeyen bir CSRF jetonunu gerektirir;
Sunucuya yeni bir rota eklemeye karar veriyoruz:
# csrftoken olmadan oturum açma
@app.route('/init-session-without-csrftoken/<string:type_response>', methods=['GET'])
def init_session_without_csrftoken(type_response: str) -> tuple:
# /init-session/type_response adresine yönlendirme
return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()), status.HTTP_302_FOUND)
- 2. satır: yeni rota. Bu rota, CSRF jetonunu beklemiyor. Böylece önceki sürümdeki [/init-session] rotasına geri döndük;
- 4-5. satırlar: istemciyi yeniden yönlendiriyoruz (jSON, XML, HTML) parametrelerinde CSRF jetonunu içeren [/init-session] rotasına yönlendiriliyor;
Bu yeni rotayı bir tarayıcıyla deneyebilirsiniz:

([with_csrftoken=True] ile yapılandırılmış) sunucunun yanıtı şöyledir:

- [1]'te, sunucu [/init-session] yoluna, CSRF jetonu ile URL'te yeniden yönlendirilmiştir;
- [2]'te, CSRF jetonu, [csrf_token] anahtarıyla ilişkili sunucu tarafından gönderilen jSON sözlüğünde yer almaktadır;
İstemci koduna geri dönelim:

[config] yapılandırmasını şu şekilde değiştiriyoruz:
config.update({
# vergi mükellefleri dosyası
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
# sonuç dosyası
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
# hata dosyası
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
# günlük dosyası
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# vergi hesaplama sunucusu
"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",
}
},
# hata ayıklama modu
"debug": True,
# csrf_token
"with_csrftoken": True,
}
)
…
# init-session yolu
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'
- 31. satır: Bir boole değeri, istemciye, bağlantı kurduğu sunucunun CSRF jetonlarıyla çalışıp çalışmadığını gösterecektir;
- 37-40. satırlar: [init-session] eyleminin hizmet URL'i ayarlanır:
- sunucu CSRF jetonlarını kullanıyorsa, hizmet URL değeri [/init-session-without-csrftoken] olur;
- aksi takdirde URL hizmet kodu [/init-session] olur;
[/init-session-without-csrftoken] yolu sunulmuştur. Bu yol, jSON / XML istemcisinin CSRF jetonuna sahip olmadan sunucuyla oturum açmasına olanak tanır. Müşteri, bu jetonu sunucunun yanıtında bulacaktır.
Ardından, müşterinin [dao] katmanını uygulayan [ImpôtsDaoWithHttpSession] sınıfını değiştiriyoruz:

# ithalatlar
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):
# yapıcı
def __init__(self, config: dict):
# üst nesnenin başlatılması
AbstractImpôtsDao.__init__(self, config)
# yapılandırma öğelerinin depolanması
# genel yapılandırma
self.__config = config
# sunucu
self.__config_server = config["server"]
# hizmetler
self.__config_services = config["server"]['url_services']
# hata ayıklama modu
self.__debug = config["debug"]
# günlük kaydı
self.__logger = None
# çerezler
self.__cookies = None
# oturum türü (json, xml)
self.__session_type = None
# jeton CSRF
self.__csrf_token = None
# istek / yanıt aşaması
def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
# [method] : HTTP, GET veya POST yöntemi
# [url_service] : URL hizmet
# [data] : POST'in x-www-form-urlencoded biçimindeki parametreleri
# [json] : POST parametreleri (json formatında)
# [cookies]: isteğe eklenecek çerezler
# XML veya JSON oturumu olmalıdır, aksi takdirde yanıtı işleyemeyiz
if self.__session_type not in ['json', 'xml']:
raise ImpôtsError(73, "il n'y a pas de session valide en cours")
# URL hizmet oturumuna CSRF jetonu eklenir
if self.__csrf_token:
url_service = f"{url_service}/{self.__csrf_token}"
# isteğin yürütülmesi
response = requests.request(method,
url_service,
data=data_value,
json=json_value,
cookies=self.__cookies,
allow_redirects=True)
# hata ayıklama modu mu?
if self.__debug:
# günlük kaydedici
if not self.__logger:
self.__logger = self.__config['logger']
# günlüğe kaydediliyor
self.__logger.write(f"{response.text}\n")
# sonuç
if self.__session_type == "json":
résultat = json.loads(response.text)
else: # xml
résultat = xmltodict.parse(response.text[39:])['root']
# varsa yanıtta bulunan çerezleri alıyoruz
if response.cookies:
self.__cookies = response.cookies
# jetonu alınır CSRF
if self.__config['with_csrftoken']:
self.__csrf_token = résultat.get('csrf_token', None)
# durum kodu
status_code = response.status_code
# durum kodu 200'den farklıysa OK
if status_code != status.HTTP_200_OK:
raise ImpôtsError(35, résultat['réponse'])
# sonucu döndürüyoruz
return résultat['réponse']
def init_session(self, session_type: str):
# oturum türünü kaydedilir
self.__session_type = session_type
# önceki çağrılardan CSRF jetonunu silin
self.__csrf_token = None
# init-session eyleminin URL'ini talep ediyoruz
url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# isteğin yürütülmesi
self.get_response("GET", url_service)
…
- 38-92. satırlar: CSRF jetonunun yönetimi esas olarak [get_response] yönteminde gerçekleştirilir;
- 60. satır: Önemli nokta, [allow_redirects=True] parametresidir. Bu, parametrenin varsayılan değeridir ancak özellikle vurgulanması istenmiştir;
[with_csrftoken=True] modundayken:
- istemciler, sunucuyla iletişim kurmaya [/init-session_without_csftoken/type_response] yolunu çağırarak başlar;
- sunucu bu isteğe, [/init-session/type_response/csrf_token] rotasına bir yönlendirme ile yanıt verir;
- [allow_redirects=True] parametresi nedeniyle, bu yönlendirme [requests] istemcisi tarafından takip edilecektir;
- CSRF jetonu, 72. ve 74. satırlarda alınan sonuçta [csrf_token] anahtarıyla ilişkili olarak bulunacaktır;
[with_csrftoken=False] modundayken:
- (devam)
- istemciler, sunucuyla diyaloglarına [/init-session /type_response] yolunu çağırarak başlar;
- sunucu bu isteğe, [/init-session/type_response] rotasına bir yönlendirme ile yanıt verir;
- [allow_redirects=True] parametresi nedeniyle, bu yönlendirme istemci tarafından [requests] olarak takip edilecektir;
- 81-82. satırlarda alınacak CSRF jetonu yoktur. Dolayısıyla [self.__csrf_token] özelliği her zaman None olarak kalır (36. satır);
- 51-52. satırlar: sonraki tüm istekler için, CSRF jetonu, varsa, başlangıç rotasına eklenir;
- satır 81-82: Sunucu, istemciden gelen her yeni istek için oluşturduğu yeni jetonu, bir sonraki istek için satır 52'de geri gönderilmek üzere yerel olarak saklar;
Ayrıca, [init_session] yöntemi biraz değişir:
def init_session(self, session_type: str):
# oturum türü kaydediliyor
self.__session_type = session_type
# önceki çağrılardan CSRF jetonu siliniyor
self.__csrf_token = None
# init-session eyleminin URL'i isteniyor
url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# isteğin yürütülmesi
self.get_response("GET", url_service)
Burada, CSRF jetonu olmadan istemci/sunucu iletişimini başlatmak için [/init-session-without-csrftoken/<type-response>] rotasının oluşturulduğunu hatırlamak gerekir. Ancak, kodun 12. satırında çağrılan [get_response] yönteminin, [self.__csrf_token]'te depolanan CSRF jetonunu sistematik olarak URL hizmetinin sonuna eklediğini görmüştük. Bu nedenle kodun 6. satırında, varsa bu CSRF jetonu siliniyor.
Hepsi bu kadar. Test amacıyla şu komutu çalıştıracağız:
- [main, main2, main3] konsol istemcileri;
- [Test1HttpClientDaoWithSession] ve [Test2HttpClientDaoWithSession] test sınıfları;
[with_csrftoken] yapılandırma parametresini sırasıyla True ve ardından False olarak ayarlayacağız.

Aşağıda, [main json] istemcisinin [with_csrftoken=True] ile çalıştırılması sırasında elde edilen günlük örnekleri yer almaktadır:
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
Arka arkaya alınan CSRF jetonlarına bakıldığında, hepsinin farklı olduğu görülür.