Skip to content

34. تمرین عملی: نسخهٔ ۱۴

Image

پوشه [http-servers/09] در نسخه 14 با کپی کردن پوشه [http-servers/08] از نسخه 13 ایجاد می‌شود.

34.1. مقدمه

CSRF (فریب درخواست بین‌سایتی) یک تکنیک ربودن جلسه است. این مورد در ویکی‌پدیا (https://fr.wikipedia.org/wiki/Cross-site_request_forgery) به شرح زیر توضیح داده شده است:

فرض کنید آلیس مدیر یک انجمن است و از طریق یک سیستم مبتنی بر جلسه به آن وارد شده است. مالوری یکی از اعضای همان انجمن است و می‌خواهد یکی از پست‌های انجمن را حذف کند. از آنجایی که او مجوزهای لازم را در حساب کاربری خود ندارد، از طریق یک حمله از نوع CSRF از حساب آلیس استفاده می‌کند.
  1. مالوری موفق می‌شود لینکی را که امکان حذف پیام مورد نظر را برای او فراهم می‌کند، پیدا کند.
  2. مالوری پیامی حاوی یک تصویر شبه‌تصویری برای نمایش (که در واقع یک اسکریپت است) برای آلیس ارسال می‌کند. URL در تصویر، لینک اسکریپتی است که پست مورد نظر را حذف می‌کند.
  3. آلیس باید یک جلسه (سشن) باز در مرورگر خود برای سایتی که مالوری هدف قرار داده است، داشته باشد. این یک پیش‌نیاز برای موفقیت مخفیانه حمله است تا بدون ایجاد یک درخواست احراز هویت که آلیس را هشدار دهد، انجام شود. این جلسه باید مجوزهای لازم برای اجرای درخواست مخرب مالوری را داشته باشد. لازم نیست که یک زبانه مرورگر در سایت هدف باز باشد، یا حتی مرورگر در حال اجرا باشد. کافی است که جلسه فعال باشد.
  4. آلیس پیام مالوری را می‌خواند؛ مرورگر او از جلسهٔ باز آلیس استفاده می‌کند و درخواست احراز هویت تعاملی نمی‌کند. مرورگر تلاش می‌کند محتوای تصویر را بازیابی کند. در این حین، لینک فعال می‌شود و پیام را حذف می‌کند؛ سپس یک صفحهٔ وب متنی را به‌عنوان محتوای تصویر دریافت می‌کند. از آنجا که نوع تصویر مرتبط را تشخیص نمی‌دهد، تصویری نمایش نمی‌دهد و آلیس نمی‌داند که مالوری همین‌الان باعث شده است او پیامی را برخلاف میل خود حذف کند.

حتی با این توضیح، تکنیک CSRF دشوار است. بیایید یک نمودار بکشیم:

Image

  • در [1-2]، آلیس با انجمن (سایت A) ارتباط برقرار می‌کند. این انجمن برای هر کاربر یک جلسه (session) برقرار می‌کند. مرورگر آلیس این کوکی جلسه را به‌صورت محلی ذخیره کرده و هر بار که درخواست جدیدی به سایت A ارسال می‌کند، آن را بازمی‌فرستد؛
  • در [3]، مالوری پیامی را برای آلیس ارسال می‌کند. آلیس آن را با استفاده از مرورگر خود می‌خواند. پیامی که او می‌خواند در قالب HTML است و حاوی لینکی به تصویری در سایت B می‌باشد. در واقع، این لینک، لینکی به یک اسکریپت جاوااسکریپت است که به محض رسیدن به مرورگر آلیس اجرا می‌شود؛
  • سپس این اسکریپت جاوااسکریپت درخواستی به سایت A ارسال می‌کند. مرورگر آلیس به طور خودکار این درخواست را به همراه کوکی جلسه ذخیره‌شده در محلی (locally) ارسال می‌کند. حمله در اینجا رخ می‌دهد: مالوری موفق شده است با استفاده از اعتبارنامه‌های جلسه آلیس، درخواستی به سایت A ارسال کند. پس از آن، هر اتفاقی که بیفتد، حمله انجام شده است؛

برای مقابله با این نوع حمله، وب‌سایت A می‌تواند به شرح زیر اقدام کند:

  • در هر تبادل [1-2] با آلیس، سایت A کلیدی را ارسال می‌کند که از این پس با نام توکن CSRF نامیده می‌شود و آلیس باید آن را در درخواست بعدی خود بازگرداند. بنابراین، در هر درخواست، آلیس باید دو مورد اطلاعات را ارسال کند:
    • کوکی جلسه؛
    • توکن CSRF دریافت‌شده در پاسخ به آخرین درخواست او به سایت A؛

امنیت در همین‌جاست: در حالی که مرورگر کوکی جلسه را به‌طور خودکار به سایت A بازمی‌فرستد، این کار را برای توکن CSRF انجام نمی‌دهد. به همین دلیل، تبادل بین درخواست‌های ۶ و ۷ که توسط اسکریپت حمله انجام می‌شود، رد خواهد شد، زیرا درخواست ۶ توکن CSRF را ارسال نکرده است؛

وب‌سایت A می‌تواند توکن CSRF را به روش‌های مختلف برای اپلیکیشن HTML برای آلیس ارسال کند:

  • می‌تواند، با هر درخواست، صفحه‌ای HTML ارسال کند که در آن تمام لینک‌ها حاوی توکن CSRF هستند، برای مثال [http://siteA/chemin/csrf_token]. وقتی آلیس در درخواست بعدی روی یکی از این لینک‌ها کلیک می‌کند، سایت A به سادگی باید توکن CSRF را از URL در درخواست بازیابی کرده و صحت آن را تأیید کند. این کاری است که در اینجا انجام خواهد شد؛
  • برای صفحاتی که حاوی فرم هستند، می‌تواند فرم را با یک فیلد مخفی حاوی توکن CSRF ارسال کند. این توکن سپس هنگام ارسال صفحه توسط آلیس، به‌طور خودکار همراه با فرم ارسال خواهد شد. وب‌سایت A توکن CSRF را از بدنه درخواست بازیابی خواهد کرد؛
  • تکنیک‌های دیگری نیز ممکن است؛

34.2. پیکربندی

Image

ما دو مقدار بولین به پیکربندی [parameters] برنامه اضافه می‌کنیم:

  • [with_redissession]: وقتی روی True تنظیم شود، برنامه از یک جلسه Redis استفاده می‌کند. وقتی روی False تنظیم شود، برنامه از یک جلسه استاندارد Flask استفاده می‌کند؛
  • [with_csrftoken]: وقتی روی True تنظیم شود، URL برنامه حاوی یک توکن CSRF است؛

        # مدت زمان مکث نخ به ثانیه
        "sleep_time"0,
        # سرور Redis
        "with_redissession"True,
        "redis": {
            "host""127.0.0.1",
            "port"6379
        },
        # توکن CSRF
        "with_csrftoken"False,

34.3. پیاده‌سازی CSRF

ما تضمین می‌کنیم که وقتی:


config['parameters']['with_csrftoken']

برابر [True] باشد، برنامه صفحات وب را برای مرورگر مشتری ارسال می‌کند که لینک‌های آن حاوی توکن CSRF خواهند بود.

34.3.1. ماژول [flask_wtf]

توکن CSRF با استفاده از ماژول [flask_wtf] که ما آن را روی ترمینال PyCharm نصب می‌کنیم، پیاده‌سازی خواهد شد:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install flask_wtf
Collecting flask_wtf

34.3.2. قالب‌های نما

ما یک کلاس جدید را به قالب‌ها معرفی می‌کنیم:

Image

کلاس [AbstractBaseModelForView] به شرح زیر است:


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 ""
  • خط ۹: کلاس [AbstractBaseModelForView] رابط [InterfaceModelForView] را که توسط کلاس‌های مدل پیاده‌سازی شده است، پیاده‌سازی می‌کند؛
  • خطوط ۱۱–۱۳: متد [get_model_for_view] پیاده‌سازی نشده است؛
  • خطوط ۱۵–۲۰: متد [get_csrftoken] توکن CSRF را تولید می‌کند، اگر برنامه برای استفاده از آن‌ها پیکربندی شده باشد. بسته به مورد، این تابع یک توکن را که با / شروع می‌شود بازمی‌گرداند، در غیر این صورت یک رشته خالی. تابع [generate_csrf] از این جهت منحصر به فرد است که برای یک درخواست کلاینت مشخص، همیشه مقدار یکسانی تولید می‌کند. پردازش یک درخواست شامل اجرای تابع‌های مختلفی است. استفاده از [generate_csrf] در این تابع‌ها همیشه مقدار یکسانی تولید می‌کند. با این حال، در درخواست بعدی، توکن جدیدی به نام CSRF تولید می‌شود؛

تمام قالب‌های M برای نما V، توکن CSRF را به شکل زیر شامل خواهند شد:


class ModelForAuthentificationView(AbstractBaseModelForView):

    def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
        #داده‌های صفحه در یک قالب جای‌گذاری شده‌اند
        modèle = {}
        

        #توکن CSRF
        modèle['csrf_token'] = super().get_csrftoken(config)

        #قالب رندر می‌شود
        return modèle
  • هر کلاس قالب، کلاس پایه [AbstractBaseModelForView] را گسترش می‌دهد؛
  • خط ۸: توکن CSRF از کلاس والد درخواست می‌شود. نتیجه یا یک رشته خالی است یا رشته‌ای از شکل [/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-kSR_nslkndfT7AFVy2UDtdb8c]؛

34.3.3. دیدها

از آنچه همین حالا دیدیم، همهٔ نماهای V در مدل M خود توکن CSRF را خواهند داشت. بنابراین قادر خواهند بود از آن در لینک‌هایی که در خود دارند استفاده کنند. بیایید چند مثال را ببینیم:

قطعهٔ احراز هویت [v_authentification.html]


<!-- فرم HTML – مقادیر آن با استفاده از اکشن [authentifier-utilisateur] ارسال می‌شوند -->
<form method="post" action="/authentifier-utilisateur{{modèle.csrf_token}}">

    <!-- عنوان -->
    <div class="alert alert-primary" role="alert">
        <h4>Veuillez vous authentifier</h4>
    </div>


</form>
  • خط ۲: بر اساس آنچه به تازگی دیدیم، توکن URL برای ویژگی [action] به این صورت خواهد بود:

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

یا

[/authentifier-utilisateur]

بسته به اینکه آیا برنامه برای استفاده از توکن‌های CSRF پیکربندی شده باشد یا خیر؛

قطعه محاسبه مالیات [v-calcul-impot.html]


<!-- HTML فرم ارسال شد -->
<form method="post" action="/calculer-impot{{modèle.csrf_token}}">
    <!-- پیام در ۱۲ ستون روی پس‌زمینه آبی -->
    <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>

قطعه شبیه‌سازی [v-liste-simulations.html]


{% if modèle.simulations is undefined or modèle.simulations|length==0 %}
<!-- پیامی روی پس‌زمینه آبی -->
<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 %}
<!-- پیام روی پس‌زمینه آبی -->
<div class="alert alert-primary" role="alert">
    <h4>Liste de vos simulations</h4>
</div>

<!-- جدول شبیه‌سازی -->
<table class="table table-sm table-hover table-striped">
    
    <!-- بدنه جدول (داده‌های نمایش داده شده) -->
    <tbody>
    <!-- هر شبیه‌سازی با چرخش روی جدول شبیه‌سازی نمایش داده می‌شود -->
    {% for simulation in modèle.simulations %}

    <!-- نمایش یک سطر از جدول با ۶ ستون – تگ <tr> -->
    <!--ستون ۱: سربرگ ردیف (شماره شبیه‌سازی) – تگ <th scope='row'> -->
    <!--ستون ۲: مقدار پارامتر [marié] – تگ -->
    <!-- ستون ۳: مقدار پارامتر [enfants] - تگ -->
    <!-- ستون ۴: مقدار پارامتر [salaire] - تگ -->
    <!--ستون ۵: مقدار پارامتر [impôt] (مالیات) – تگ -->
    <!--ستون ۶: مقدار پارامتر [surcôte] – تگ -->
    <!-- ستون ۷: مقدار پارامتر [décôte] – برچسب -->
    <!-- ستون ۸: مقدار پارامتر [réduction] – tag -->
    <!--ستون ۹: مقدار پارامتر [taux] (برای مالیات) – تگ -->
    <!-- ستون ۱۰: لینک حذف شبیه‌سازی - تگ -->
    <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 %}

قطعه منو [v-menu.html]


<!-- منوی بوت‌استرپ -->
<nav class="nav flex-column">
    <!-- نمایش فهرستی از پیوندها 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. جاده‌ها

اکنون دو نوع مسیر وجود دارد، بسته به اینکه از توکن CSRF استفاده می‌کنند یا خیر:

Image

  • [routes_without_csrftoken] مسیرهایی بدون توکن CSRF هستند. این‌ها مسیرهای نسخه قبلی هستند؛
  • [routes_with_csrftoken] مسیرهایی هستند که از توکن CSRF استفاده می‌کنند.

در [routes_with_csrftoken]، مسیرها اکنون دارای یک پارامتر اضافی هستند، توکن CSRF:


# کنترل‌کنندهٔ جلویی
def front_controller() -> tuple:
    # درخواست به کنترلر اصلی ارسال می‌شود
    main_controller = config['mvc']['controllers']['main-controller']
    return main_controller.execute(request, session, config)

@app.route('/', methods=['GET'])
def index() -> tuple:
    # ارسال مجدد به /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:
    # اجرای کنترلر مرتبط با اکشن
    return front_controller()

#احراز هویت کاربر
@app.route('/authentifier-utilisateur/<string:csrf_token>', methods=['POST'])
def authentifier_utilisateur(csrf_token: str) -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

#محاسبه-مالیات
@app.route('/calculer-impot/<string:csrf_token>', methods=['POST'])
def calculer_impot(csrf_token: str) -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

#محاسبه دسته‌ای مالیات
@app.route('/calculer-impots/<string:csrf_token>', methods=['POST'])
def calculer_impots(csrf_token: str):
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

# فهرست شبیه‌سازی‌ها
@app.route('/lister-simulations/<string:csrf_token>', methods=['GET'])
def lister_simulations(csrf_token: str) -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

# حذف-شبیه‌سازی
@app.route('/supprimer-simulation/<int:numero>/<string:csrf_token>', methods=['GET'])
def supprimer_simulation(numero: int, csrf_token: str) -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

#پایان-جلسه
@app.route('/fin-session/<string:csrf_token>', methods=['GET'])
def fin_session(csrf_token: str) -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

#نمایش-محاسبه-مالیات
@app.route('/afficher-calcul-impot/<string:csrf_token>', methods=['GET'])
def afficher_calcul_impot(csrf_token: str) -> tuple:
    # اجرای کنترلر مرتبط با اقدام
    return front_controller()

# get-admindata
@app.route('/get-admindata/<string:csrf_token>', methods=['GET'])
def get_admindata(csrf_token: str) -> tuple:
    # کنترلر مرتبط با اقدام اجرا می‌شود
    return front_controller()

تمام مسیرها اکنون توکن CSRF را در پارامترهای خود دارند، از جمله مسیر [/init-session]. این بدان معناست که کلاینت نمی‌تواند با وارد کردن مستقیم URL [/init-session/html برنامه را راه‌اندازی کند، زیرا توکن CSRF وجود نخواهد داشت. آنها اکنون باید از طریق URL و [/] در خطوط ۷–۱۰ عبور کنند.

انتخاب مسیر در اسکریپت اصلی [main] انجام می‌شود:



# رشته اصلی دیگر به لاگ‌گیر نیاز ندارد
logger.close()

#اگر خطایی رخ داده باشد، فرآیند متوقف می‌شود
if erreur:
    sys.exit(2)

# وارد کردن مسیرهای برنامه وب
if config['parameters']['with_csrftoken']:
    import routes_with_csrftoken as routes
else:
    import routes_without_csrftoken as routes

#پیکربندی مسیرها
routes.config = config

# راه‌اندازی برنامه Flask
routes.execute(__name__)
  • خطوط ۹–۱۳: انتخاب مسیر بسته به اینکه آیا برنامه از توکن‌ها استفاده می‌کند یا خیر CSRF;

34.3.5. کنترل‌کننده [MainController]

برای هر درخواست، سرور باید وجود توکن CSRF را بررسی کند. ما این کار را در کنترلر اصلی [MainController] که همه درخواست‌ها را مدیریت می‌کند، انجام می‌دهیم:


from flask_wtf.csrf import generate_csrf, validate_csrf

       #پردازش درخواست
        try:
            # ثبت گزارش
            logger = Logger(config['parameters']['logsFilename'])

            …

            # بازیابی عناصر از مسیر
            params = request.path.split('/')

            # اقدام اولین عنصر است
            action = params[1]

            …

            if config['parameters']['with_csrftoken']:
                # csrf_token آخرین عنصر مسیر است
                csrf_token = params.pop()
                #اعتبار توکن بررسی می‌شود
                # اگر csrf_token با مورد انتظار مطابقت نداشته باشد، یک استثنا پرتاب خواهد شد
                validate_csrf(csrf_token)

            …

        except ValidationError as exception:
            #توکن CSRF نامعتبر
            résultat = {"action": action, "état"121"réponse"[f"{exception}"]}
            status_code = status.HTTP_400_BAD_REQUEST

        except BaseException as exception:
            # استثناهای دیگر (غیرمنتظره)
            résultat = {"action": action, "état"131"réponse"[f"{exception}"]}
            status_code = status.HTTP_400_BAD_REQUEST

        finally:
            pass

        # csrf_token به نتیجه اضافه می‌شود
        résultat['csrf_token'] = generate_csrf()

        # نتیجه ارسال‌شده به کلاینت ثبت می‌شود
        log = f"[MainController] {résultat}\n"
        logger.write(log)
  • خط ۲۰: ما توکن CSRF را از URL در درخواست از نوع [http://machine :port/chemin/action/param1/param2/…/csrf_token] بازیابی می‌کنیم. توکن جلسه همیشه آخرین عنصر URL است؛
  • خط ۲۳: اعتبار توکن CSRF که از URL بازیابی شده است، با توکن جلسه CSRF بررسی می‌شود. اگر نامعتبر باشد، تابع [validate_csrf] یک استثنا از نوع [ValidationError] را پرتاب می‌کند (خط ۲۷);
  • خط ۴۱: توکن CSRF در نتیجه ارسال‌شده به کلاینت گنجانده می‌شود. کلاینت‌های jSON و XML به این نیاز دارند. این به این دلیل است که این کلاینت‌ها صفحات HTML را که حاوی توکن CSRF در لینک‌های درون صفحات هستند، دریافت نمی‌کنند. بنابراین، آنها آن را در پاسخ jSON یا XML که توسط سرور ارسال می‌شود، دریافت خواهند کرد؛

توجه: تابع [validate_csrf] در خط 23 تطابق دقیق را بررسی نمی‌کند. توکن CSRF با کلید [csrf_token] در جلسه (session) ذخیره می‌شود. به نظر می‌رسد آزمایش‌ها نشان می‌دهند که توکن CSRF در صورتی معتبر است که در طول جلسه تولید شده باشد. بنابراین، اگر به صورت دستی، در URL نمایش داده شده در مرورگر، برای مثال (/lister-simulations/xyz)، شما توکن [xyz] را با توکن دیگری، [abc]، که قبلاً در یک اقدام قبلی دریافت شده است، جایگزین می‌کنید، اقدام [/lister-simulations] با موفقیت انجام خواهد شد؛

34.4. آزمایش‌ها با استفاده از مرورگر

ما:

  • سرور را با پارامترهای تنظیم‌شده از [with_csrftoken] تا [True] راه‌اندازی کنید؛
  • درخواست URL و [http://localhost:5000] را با استفاده از یک مرورگر ارسال کنید؛

Image

  • در [1]، توکن CSRF;

بیایید چند عملیات انجام دهیم تا فهرستی از شبیه‌سازی‌ها داشته باشیم:

Image

حالا، بیایید به‌صورت دستی URL [http://localhost:5000/supprimer-simulation/1/x] را وارد کنیم تا شبیه‌سازی با شناسه=1 را حذف کنیم. ما عمداً یک توکن نادرست، CSRF، وارد می‌کنیم تا ببینیم چه اتفاقی می‌افتد. پاسخ سرور به شرح زیر است:

Image

توجه ۱: قطعی نیست که روش به‌کاررفته در اینجا همیشه برای مقابله با حملات CSRF کافی باشد. بیایید به نمودار حمله بازگردیم:

Image

اگر اسکریپت جاوااسکریپت دانلود شده با نام [5] قادر به خواندن تاریخچه مرورگری باشد که آلیس استفاده می‌کند، توانایی بازیابی URL اجرا شده توسط مرورگر را خواهد داشت، از جمله URL مانند [/cible/csrf_token]. سپس قادر خواهد بود توکن جلسه [csrf_token] را بازیابی کرده و حمله خود را در [6-7] انجام دهد. با این حال، مرورگر تنها اجازه می‌دهد از تاریخچه پنجره‌ای که اسکریپت در آن در حال اجراست سوءاستفاده شود. بنابراین، اگر آلیس از همان پنجره‌ای که برای تعامل با سایت A ([1-2]) استفاده می‌کند، برای خواندن پیام مالوری ([3]) استفاده نکند، حمله (CSRF) امکان‌پذیر نخواهد بود.

34.5. کلاینت‌های کنسول

راه دیگر برای تست نسخه 14 برنامه، استفاده مجدد از تست‌های نسخه 12 و تطبیق آن‌ها با سرور جدید است.

Image

پوشه [impots/http-clients/09] در ابتدا با کپی کردن پوشه [impots/http-clients/07] ایجاد می‌شود. سپس تغییر داده می‌شود.

بیایید به مسیرهایی که یک جلسه را راه‌اندازی می‌کنند بازگردیم:


#ریشهٔ برنامه
@app.route('/', methods=['GET'])
def index() -> tuple:
    # ارسال مجدد به /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:
    # اجرای کنترلر مرتبط با اکشن
    return front_controller()

هیچ‌کدام از این مسیرها برای راه‌اندازی یک جلسه jSON یا XML مناسب نیستند:

  • خطوط ۲–۵: مسیر [/] یک جلسه HTML را راه‌اندازی می‌کند؛
  • خطوط ۸–۱۱: مسیر [/init-session] به توکن CSRF نیاز دارد که ما آن را نمی‌دانیم؛

ما تصمیم می‌گیریم یک مسیر جدید به سرور اضافه کنیم:


#init-session-without-csrftoken
@app.route('/init-session-without-csrftoken/<string:type_response>', methods=['GET'])
def init_session_without_csrftoken(type_response: str) -> tuple:
    # ارسال مجدد به /init-session/type_response
    return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()), status.HTTP_302_FOUND)
  • خط ۲: مسیر جدید. این مسیر به توکن CSRF نیاز ندارد. بنابراین ما به مسیر [/init-session] از نسخه قبلی بازگشته‌ایم؛
  • خطوط ۴–۵: ما کلاینت را هدایت می‌کنیم (jSON, XML, HTML) به مسیر [/init-sessionکه توکن CSRF را در پارامترهای خود دارد؛

می‌توانید این مسیر جدید را با استفاده از یک مرورگر امتحان کنید:

Image

پاسخ از سرور (که با [with_csrftoken=True] پیکربندی شده است) به شرح زیر است:

Image

  • در [1]، سرور به مسیر [/init-session] با توکن CSRF در URL هدایت شد؛
  • در [2]، توکن CSRF در دیکشنری jSON که توسط سرور ارسال شده، با کلید [csrf_token] مرتبط است؛

بیایید به کد کلاینت بازگردیم:

Image

ما پیکربندی [config] را به شرح زیر تغییر می‌دهیم:


   config.update({
        # ثبت‌نام مودیان مالیاتی
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        # فایل نتایج
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        # فایل خطاها
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        # فایل لاگ
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # سرور محاسبه مالیات
        "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",
            }
        },
        # حالت اشکال‌زدایی
        "debug"True,
        # csrf_token
        "with_csrftoken"True,
    }
    )

    # مسیر 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'
  • خط ۳۱: یک متغیر بولی به کلاینت نشان می‌دهد که آیا سرور مورد نظر با توکن‌های CSRF کار می‌کند یا خیر؛
  • خطوط ۳۷–۴۰: ما سرویس URL را برای اقدام [init-session] تنظیم می‌کنیم:
    • اگر سرور از توکن‌های CSRF استفاده کند، آنگاه سرویس URL برابر با [/init-session-without-csrftoken] است؛
    • در غیر این صورت، سرویس URL برابر با [/init-session] است؛

مسیر [/init-session-without-csrftoken] ارائه شده است. این مسیر به کلاینتی با jSON / XML اجازه می‌دهد تا بدون داشتن توکن CSRF، یک جلسه را با سرور آغاز کند. کلاینت این توکن را در پاسخ سرور پیدا خواهد کرد.

سپس کلاس [ImpôtsDaoWithHttpSession] را که لایه [dao] کلاینت را پیاده‌سازی می‌کند، اصلاح می‌کنیم:

Image


# واردات
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):

    # سازنده
    def __init__(self, config: dict):
        # ابتدای‌سازی والد
        AbstractImpôtsDao.__init__(self, config)
        # ذخیرهٔ عناصر پیکربندی
        # پیکربندی عمومی
        self.__config = config
        # سرور
        self.__config_server = config["server"]
        # خدمات
        self.__config_services = config["server"]['url_services']
        # حالت اشکال‌زدایی
        self.__debug = config["debug"]
        # لاگ‌گیر
        self.__logger = None
        # کوکی‌ها
        self.__cookies = None
        # نوع جلسه (json, xml)
        self.__session_type = None
        #توکن CSRF
        self.__csrf_token = None

    # مرحله درخواست/پاسخ
    def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        # [method]: روش HTTP, GET یا POST
        #[url_service]: سرویس URL
        # [data]: پارامترها برای POST در قالب x-www-form-urlencoded
        # [json]: پارامترهای POST در قالب JSON
        #[cookies]: کوکی‌هایی که باید در درخواست گنجانده شوند

        #شما باید یک جلسه (XML یا JSON) داشته باشید؛ در غیر این صورت، قادر به پردازش پاسخ نخواهید بود
        if self.__session_type not in ['json''xml']:
            raise ImpôtsError(73"il n'y a pas de session valide en cours")

        #توکن CSRF را به توکن سرویس URL اضافه کنید
        if self.__csrf_token:
            url_service = f"{url_service}/{self.__csrf_token}"

        #در حال اجرای درخواست
        response = requests.request(method,
                                    url_service,
                                    data=data_value,
                                    json=json_value,
                                    cookies=self.__cookies,
                                    allow_redirects=True)

        # حالت اشکال‌زدایی؟
        if self.__debug:
            # لاگ‌گیر
            if not self.__logger:
                self.__logger = self.__config['logger']
            #ثبت لاگ
            self.__logger.write(f"{response.text}\n")

        # نتیجه
        if self.__session_type == "json":
            résultat = json.loads(response.text)
        else:  # XML
            résultat = xmltodict.parse(response.text[39:])['root']

        #هر کوکی را از پاسخ بازیابی می‌کند، در صورت وجود
        if response.cookies:
            self.__cookies = response.cookies

        #بازیابی توکن CSRF
        if self.__config['with_csrftoken']:
            self.__csrf_token = résultat.get('csrf_token'None)

        #کد وضعیت
        status_code = response.status_code

        # اگر کد وضعیت 200 نباشد OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, résultat['réponse'])

        # نتیجه را بازگردانید
        return résultat['réponse']

    
    def init_session(self, session_type: str):
        # نوع جلسه را ثبت کنید
        self.__session_type = session_type

        # توکن CSRF را از فراخوانی‌های قبلی حذف کنید
        self.__csrf_token = None

        #درخواست URL برای اقدام init-session
        url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        # اجرای درخواست
        self.get_response("GET", url_service)

  • خطوط ۳۸–۹۲: پردازش توکن CSRF عمدتاً در داخل متد [get_response] انجام می‌شود؛
  • خط ۶۰: نکته کلیدی پارامتر [allow_redirects=True] است. این مقدار پیش‌فرض آن است، اما ما می‌خواستیم آن را برجسته کنیم؛

وقتی در حالت [with_csrftoken=True] است:

  • کلاینت‌ها دیالوگ خود را با سرور با فراخوانی مسیر [/init-session_without_csftoken/type_response] آغاز می‌کنند؛
  • سرور به این درخواست با هدایت به مسیر [/init-session/type_response/csrf_token] پاسخ می‌دهد؛
  • به دلیل پارامتر [allow_redirects=True]، این هدایت توسط کلاینت [requests] دنبال خواهد شد؛
  • توکن CSRF در نتیجهٔ بازیابی‌شده در خطوط ۷۲ و ۷۴ یافت خواهد شد، مرتبط با کلید [csrf_token]؛

وقتی در حالت [with_csrftoken=False] است:

  • (ادامه)
    • کلاینت‌ها دیالوگ خود را با سرور با فراخوانی مسیر [/init-session /type_response] آغاز می‌کنند؛
    • سرور به این درخواست با هدایت به مسیر [/init-session/type_response] پاسخ می‌دهد؛
    • به دلیل پارامتر [allow_redirects=True]، این هدایت توسط کلاینت [requests] دنبال خواهد شد؛
    • در خطوط ۸۱–۸۲ هیچ توکن CSRF برای بازیابی وجود ندارد. بنابراین ویژگی [self.__csrf_token] همچنان روی None تنظیم می‌ماند (خط ۳۶);
  • خطوط ۵۱–۵۲: برای تمام درخواست‌های بعدی، توکن CSRF، در صورت وجود، به مسیر اولیه اضافه می‌شود؛
  • خطوط ۸۱–۸۲: توکن جدید تولید شده توسط سرور برای هر درخواست جدید کلاینت به صورت محلی ذخیره می‌شود تا بتواند در خط ۵۲ با درخواست بعدی بازگردانده شود؛

علاوه بر این، متد [init_session] کمی تغییر می‌کند:


    def init_session(self, session_type: str):
        # نوع جلسه ثبت می‌شود
        self.__session_type = session_type

        #توکن CSRF از تماس‌های قبلی حذف می‌شود
        self.__csrf_token = None

        #درخواست URL برای اقدام init-session
        url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        # اجرای درخواست
        self.get_response("GET", url_service)

در اینجا مهم است به خاطر داشته باشیم که ما یک مسیر [/init-session-without-csrftoken/<type-response>] برای راه‌اندازی دیالوگ کلاینت/سرور بدون توکن CSRF ایجاد کردیم. با این حال، ما دیده‌ایم که متد [get_response] که در خط ۱۲ کد فراخوانی می‌شود، به طور سیستماتیک توکن CSRF را که در [self.__csrf_token] ذخیره شده است، به انتهای توکن سرویس URL اضافه می‌کند. به همین دلیل، در خط ۶ کد، این توکن CSRF در صورت وجود حذف می‌شود.

همین. برای اهداف آزمایشی، اجرا کنید:

  • کلاینت‌های کنسول [main, main2, main3];
  • کلاس‌های تست [Test1HttpClientDaoWithSession] و [Test2HttpClientDaoWithSession]؛

پارامتر پیکربندی [with_csrftoken] را به ترتیب روی True و سپس False تنظیم می‌کنیم.

Image

در اینجا مثالی از لاگ‌های به‌دست‌آمده هنگام اجرای کلاینت [main json] با [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

اگر به توکن‌های CSRF که پشت سر هم دریافت می‌شوند نگاه کنیم، می‌بینیم که همگی با هم متفاوت هستند.